一般情况下,一个布局写好以后,使用 Activity#setContentView 调用该布局,这个 View tree 就创建好了。Activity#setContentView 其实是通过 LayoutInflate 来把布局文件转化为 View tree 的(反射)。需要注意的是,LayoutInflate 虽然可以帮助创建 View tree,但到这里也仅是以单纯的对象数据存在,这个时候是无法正确的获取 View 的 GUI(Graphical User Interface 图形用户界面)的相关属性的,如大小、位置和渲染状态。
View tree 生成的最后一步就是把根节点送到 ViewRootImpl#setView 中,之后就会进入渲染流程,入口方法是 ViewRootImpl#requestLayout,之后是 ViewRootImpl#scheduleTraversals,最后调用的是 ViewRootImpl#performTraversals,View tree 的渲染流程全都在这里,也就是常说的 measure、layout、draw。View体系与自定义View(三)—— View的绘制流程
以下为 View 的绘制流程/视图添加到 Window 的过程:
总结:文本数据(xml)—> 实例数据(java) —> 图像数据 bitmap,bitmap 才是屏幕(硬件)所需的数据。
在 ViewRootImpl#drawSoftware 方法中创建一个 Canvas 对象,然后进入 View#draw 流程,Canvas 才是实际制作图像的工具,比如如何画点,如何画线,如何画文字、图片等等。
Canvas 对象是从哪里来的呢?
// /frameworks/base/core/java/android/view/ViewRootImpl.java
public final Surface mSurface = new Surface();
private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
Surface surface = mSurface;
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff,
int yoff, boolean scalingRequired,
Rect dirty, Rect surfaceInsets) {
// Draw with software renderer.
final Canvas canvas;
try {
canvas = mSurface.lockCanvas(dirty);
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
} catch (IllegalArgumentException e) {
mLayoutRequested = true; // ask wm for a new surface next time.
return false;
}
try {
...
mView.draw(canvas);
...
}finally {
...
}
}
// /frameworks/base/core/java/android/view/View.java
public void draw(@NonNull Canvas canvas) {
...
}
protected void dispatchDraw(@NonNull Canvas canvas) { }
// /frameworks/base/core/java/android/view/ViewGroup.java
protected boolean drawChild(@NonNull Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
一个 Canvas 对象从 ViewRootImpl 传给 View,View 的各个方法(draw、dispatchDraw 和 drawChild)都只接收 Canvas 对象,每个 View 都要把其想要展示的内容传递到 Canvas 对象中。
Canvas 对象的来源有两个:
因此,draw 的触发逻辑也有两条:
硬件加速绘制是从 Android 4.0 开始支持的,在此之前走的都是软件渲染,也就是从 ViewRoot(Android 4.0 之前是 ViewRoot,之后才是 ViewRootImpl)中持有的 Surface 直接创建 Canvas,然后交给 View 去做具体的绘制。
Skia(二维)、OpenGL(三维)是专门用来制作图形的(Bitmap),可以直接对 GPU 进行调用处理。底层的图像库很多,Android 选择的是这两个。
软件绘制使用的是 Skia 库,是一款能在低端设备,如手机上呈现高质量的 2D 跨平台图形框架,Chrome、Flutter 内部使用的都是 Skia 库。需要注意的是,软件绘制使用的是 Skia 库,但这并不代表 Skia 库不支持硬件加速,从 Android 8 开始,我们可以使用 Skia 进行硬件加速,Android 9 开始默认使用Skia 进行硬件加速。
在处理 3D 场景时,通常使用 OpenGL ES。在 Android 7.0 中添加了对 Vulkan 的支持。Vulkan 的设计目标是取代 OpenGL,Vulkan 是个相当低级别的 API,并且提供了并行的任务处理。除了较低的 CPU 的使用率,VulKan 还能够更好的在多个 CPU 内核之间分配工作。在功耗、多核优化提升会图调用上有非常明显的优势。
Skia、OpenGL、Vulkan 的区别:
除了屏幕,UI 渲染海要依赖两个核心的硬件,CPU 和 GPU:
在没有 GPU 的时代,UI 的绘制任务都是由 CPU 完成的,也就是说,CPU 除了负责逻辑运算、内存管理还要负责 UI 绘制,这就导致 CPU 的任务繁重,性能也会受到影响。
CPU 和 GPU 在结构设计上完全不同,如下所示:
从上图可以看出,CPU 的控制器较为复杂,而 ALU 数量较少,因此 CPU 更擅长各种复杂的逻辑运算,而不擅长数学尤其是浮点运算。而 GPU 的设计正是为了实现大量的数学运算。GPU 的控制器比较简单,但包含大量的 ALU,GPU 中的 ALU 使用了并行设计,且具有较多的浮点运算单元,可以帮助我们加快 Rasterization(栅格化)操作。
栅格化将 UI 组件拆分到显示器上的不同像素上进行显示。UI 组件在绘制到屏幕之前都要经过 Rasterization(栅格化)操作,是绘制 Button、Shape、Path、String、Bitmap 等显示组件最基础的操作。这是一个非常耗时的操作,GPU 的引入就是为了加快栅格化。
因此,硬件绘制的思想就是 CPU 将 XML 数据转换成实例对象,然后将 CPU 不擅长的图形计算交由 GPU 去处理,由 GPU 完成绘制任务,以便实现更好的性能(CPU 和 GPU 是制图者)。
Android 4.0 开始引入硬件加速机制,但是还有一些 API 是不支持硬件加速的,需要进行手动关闭。
虽然引入了硬件加速机制,加快了渲染的时间,但是对于 GUI(Graphical User Interface 图形用户界面)的流畅度、响应度,特别是动画这一块的流畅程度和其他平台(如 Apple)差距仍然是很大的。一个重要的原因就在于,GUI 整体的渲染缺少协同。最大的问题在于动画,动画要求连续不断的重绘,如果仅靠客户端来触发,帧率不够,由此造成的流畅度也不好。
于是在 Android 4.1 中引入了 Choreographer 以及 Vsync 机制来解决这个问题。
Google 在 2012 年的 I/O 大会上宣布了 Project Butter 黄油计划,并且在 Android 4.1 中正式开启这个机制。Project Butter 主要包含三个组成部分:
其中,VSync(Vertical Synchronization) 是理解 Project Butter 的核心。
帧率 vs 屏幕刷新频率:
对于一个特定的设备来说,帧率和屏幕刷新速率没有必然的关系。但是两者需要协同工作,才能正确的获取图像数据并进行绘制。
屏幕并不是一次性的显示画面的,而是从左到右(行刷新,水平刷新,Horizontal Scanning)、从上到下(屏幕刷新,垂直刷新,Vertiacl Scanning)逐行扫描显示,不过这一过程快到人眼无法察觉。以 60Hz 的刷新频率的屏幕为例,即 1000/60 ≈ 16ms,16ms 刷新一次。
如果上一帧的扫描没有结束,屏幕又开始扫描下一帧,就会出现扫描撕裂的情况:
因此,GPU 厂商开发出了一种防止屏幕撕裂的技术方案 —— Vertical Synchronization,即 VSync,垂直同步信号或时钟中断。VSync 是一个硬件信号,它和显示器的刷新频率相对应,通常是 60Hz,每当屏幕完成一次垂直刷新,VSync 信号就会被发出,作为显示器和图形引擎之间时间同步的标准,其本质意义在于保证界面的流畅性和稳定性。
Choreographer(编舞者)根据 VSync 信号来对 CPU/GPU 进行绘制指导,协调整个渲染过程,对于输入事件响应、动画和渲染在时间上进行把控,以保证流畅的用户体验。
Choreographer 的作用:
Choreographer 使用了以下几种机制来实现流畅的界面渲染:
其实这个 Choreogarpher 这个类本身并不会很复杂,简单来说它就是负责定时回调,主要方法有 postFrameCallback 和 removeFrameCallback,FrameCallback 是个比较简单的接口:
// /frameworks/base/core/java/android/view/Choreographer.java
public interface FrameCallback {
public void doFrame(long frameTimeNanos);
}
以下是 Android 渲染的整体架构:
Android 渲染的整体架构可以分为以下几部分:
整个图像渲染系统就是采用了生产者-消费者模式,屏幕渲染的核心,是对图像数据的生产和消费。生产和消费的对象是 BufferQueue 中的 Buffer。
无论开发者使用哪种 API,一切内容都会渲染到 Surface(Canvas —> Surface) 上。Surface 表示缓冲队列中的生产方,缓冲队列通常被 SurfaceFlinger 消耗。如 draw 方法会把绘制指令通过 Canvas 传递给 framework 层的 RenderThread 线程。RenderThread 线程通过 surface.dequeue 的到缓冲区 graphic buffer,然后在上面通过 OpenGL 来完成真正的渲染命令,把缓冲区交还给 BufferQueue 队列中。
在没有引入 Vsync 的时候,屏幕显示图像的工作流程是这样的:
如上图所示,CPU/GPU 将需要绘制的数据存放在图像缓冲区中,屏幕从图像缓冲区中获取数据,然后刷新显示,这是典型的生产者-消费者模型。
理想的情况是帧率(GPU)和刷新频率(屏幕)相等,每绘制一帧,屏幕就显示一帧。而实际情况是,二者之间没有必然的联系,如果没有锁来控制同步,很容易出现问题。如当帧率大于刷新频率时,屏幕还没有刷新到 n-1 帧的时候,GPU 已经生成第 n 帧了,屏幕刷新的时候绘制的就是第 n 帧数据,这个时候屏幕上半部分显示的是第 n 帧数据,屏幕的下半部分显示的是第 n-1 帧之前的数据,这样显示的图像就会出现明显的偏差,也就是“tearing”,如下所示:
这里的双缓存和计算机组成原理中的“二级缓存”不是一回事。
为了解决单缓存的 tearing 问题,双缓存和 VSync 应运而生。双缓存的模型如下所示:
两个缓存分别为 Back Buffer 和 Frame Buffer。GPU 向 Back Buffer 中写数据,屏幕从 Frame Buffer 中读数据。VSync 信号负责调用 Back Buffer 到 Frame Buffer 的复制操作,可以认为该复制操作在瞬间完成。
在双缓冲模式下,工作流程是这样的:在某个时间点,一个屏幕刷新周期完成,进入短暂的刷新空白期。此时,VSync 信号产生,先完成复制操作,然后通知 CPU/GPU 绘制下一帧图像。复制操作完成后屏幕开始下一个刷新周期,即将刚复制到 Frame Buffer 的数据显示到屏幕上。
在双缓冲模型下,只有当 VSync 信号产生时,CPU/GPU 才会开始绘制。这样,当帧率大于刷新频率时,帧率就会被迫跟刷新频率保持同步,从而避免“tearing”现象。
需要注意的是,当 VSync 信号发出时,如果 CPU/GPU 正在生产帧数据,此时不会发生复制操作。当屏幕进入下一个刷新周期时,就会从 Frame Buffer 中取出“老”数据,而非正在产生的帧数据,即两个刷新周期显示的是同一帧数据,这就是“掉帧”现象(Dropped Frame,Skipped Frame,Jank)。
双缓存的缺陷在于,当 CPU/GPU 绘制一帧的时间超过 16ms 时,就会产生 Jank。
如下图所示,A、B 和 C 都是 Buffer。蓝色代表 CPU 生成的 Display List,绿色代表 GPU 执行 Display List 中的命令生成帧,黄色代表生成帧完成:
如果有第三个 Buffer 能让 CPU/GPU 在这个时候继续工作,那就完全可以避免第二个 Jank 产生了。
于是有了三缓存:
工作原理同双缓冲类似,只是多了一个 Back Buffer。
需要注意的是,第三个缓存并不是总存在的,只有当需要的时候才会创建。之所以这样,是因此三缓存会显著增加用户输入到显示的延迟时间。如上图,帧 C 是在第 2 个刷新周期产生的,却是在第 4 个周期显示的。
SurfaceFlinger 是图像数据的消费者。在应用程序请求创建 Surface 的时候,SurfaceFlinger 会创建一个 Layer。Layer 是 SurfaceFlinger 操作合成的基本单元。所以,一个 Surface 对应一个 Layer。当应用程序把绘制好的 Graphic Buffer 数据放入 BufferQueue 后,接下来的工作就是 SurfaceFlinger 来完成了。
SurfaceFlinger 的作用主要是接收 Graphic Buffer,然后交给 HWComposer 或者 OpenGL 做合成,合成完成后,SurfaceFlinger 会把最终的数据提交给 FrameBuffer。