注册

Android 渲染系列-绘制流程总览

前言

谈到Android的渲染,可能会想到测量、布局、绘制三大流程。但我们的view到底是如何一步一步显示到屏幕的?App的CPU/GPU渲染到底是什么?OpenGL/Vulkan/skia是什么? surfaceFlinger和HAL又是什么呢?

带着这些问题,我们今天就深入的去学习Android绘制的整个流程吧。

参考分层思想,我们大概把整个渲染分为App层和SurfaceFlinger层,先讲各层都做什么工作,后面在把二者联系起来。

相关概念

  1. Vsync信号

由系统设备产生。假设在60HZ的屏幕上,屏幕就会每16ms进行一次扫描,在两次扫描中间会有一个间隔,此时系统就会发出Vsync信号,来通知APP(Vsync-app)进行渲染,SurfaceFlinger(Vsync-sf)来进行swap缓冲区进行展示。因此,只要App的渲染过程(CPU计算+GPU绘制)不超过16ms,画面就会显得很流畅。

说明:

  • 如果系统检测到硬件支持,则Vysnc信号由硬件产生,否则就由软件模拟产生。这个了解即可。
  • Vsync offset机制: Vsync-app、Vsync-sf并不是同时通知的,Vsync-sf会相对晚些,但对于我们App开发者来说,即可认为约等于同时发生。
  1. OpenGL、Vulkan、skia的关系
  • OpenGL: 是一种跨平台的3D图形绘制规范接口。OpenGL EL则是专门针对嵌入式设备,如手机做了优化。
  • Vulkan: 跟OpenGL相同功能,不过它同时支持3D、2D,比OpenGL更加的轻量、性能更高。
  • skia: skia是图像渲染库,2D图形绘制自己就能完成。3D效果(依赖硬件)由OpenGL、Vulkan、Metal支持。它不仅支持2D、3D,同时支持CPU软件绘制和GPU硬件加速。Android、flutter都是使用它来完成绘制。
  1. GPU和OpenGL什么关系

OpenGL是规范,GPU就是该规范具体的设备实现者。

  1. Surface 与 graphic buffer

插入一个问题: 一个Android程序有多少个window??

应用本身+系统(menu+statusBar)+dialog+toast+popupWindow。

Android的一个window对应一个surface(请注意: 一个surface不一定对应window。如surfaceView), 一个surface对应一个BufferQueue。 进程可以同时拥有多个surface,如使用surfaceView(封装了单独的surface,且在单独的子线程操作)。

canvas 是通过surface.lockCnavas得到(最终调用JNI的framework层的surface.lock方法获取graphic buffer)。

surface通过dequeue拿到graphic buffer,然后进行渲染绘制,渲染完成后回到BufferQueu队列,最后通知surfaceFlinger来消费。

  1. SurfaceFlinger 是什么?

可以认为它是协调缓冲区数据和设备显示的协调者。 Vsync信号、三倍缓冲、缓冲区的合成操作都是由它来控制。

1 Android渲染演变

了解Android系统对渲染的不断优化历史,对于理解渲染很有帮助。

  • Android 4.1

引入了project butter黄油计划:Vsync、三倍缓冲、choreography编舞者。

  • android 5.0

引入了RenderThread线程(该线程是系统在framework层维护),把之前CPU直接操作绘制指令(OpenGL/vulkan/skia)部分,交给了单独的渲染线程。减少主线程的工作。即使主线程卡住,渲染也不受影响。

  • Android7.0 引入了Vulkan支持。 OpenGL是3D渲染API,VulKan是用来替换OpenGL的。它不仅支持3D,也支持2D,同时更加轻量级。

2 App做了什么(重点)

从invalidate()(它会在onVsync信号来的时候(也就是下一帧)触发onDraw()方法)方法调用开始来分析整个过程。Activity的显示最终会调用requestLayout方法,关于Activity的启动过程可以自行查阅(后续有空在写篇文章~)。

invalidate()让 drawing cache(绘制缓存)无效,也就是所谓的标脏,所以才会要重新进行绘制。 requestLayout()方法跟invalidate()调用的是同一个方法:scheduleTraversals(),如下:

ViewRootImpl.java

void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

viewRootImpl的invalidate()方法会postCalback到choreography。

choreography是在viewRootImpl创建的的时注册了监听系统的vsync信号。

当onVsync回调下一帧的时候,就会执行choreography的doFrame()方法,然后执行callback,调用 viewRootImpl的performTraversal()--doTraversal()方法,从而执行onMeasure()、onLayout()、onDraw()三大流程。

那UI线程如何与RenderThread交互呢? 什么时候把绘制好的数据交给SurfaceFlinger呢?

image.png

onMeasure()、onLayout()计算出view的大小和摆放的位置,这都是UI线程要做的事情。

  1. 在draw()方法中进行绘制,但此时是没有真正去绘制。而是把绘制的指令封装为displayList,进一步封装为RendNode,在同步给RenderThread。
  2. RenderThread通过 dequeue() 拿到graphic buffer(surfaceFlinger的缓冲区),根据绘制指令直接操作OpenGL的绘制接口,最终通过GPU设备把绘制指令渲染到了离屏缓冲区graphic buffer。
  3. 完成渲染后,把缓冲区交还给SurfaceFlinger的BufferQueue。SurfaceFlinger会通过硬件设备进行layer的合成,最终展示到屏幕。

image.png

以上流程也体现了生产者与消费者模式:

image.png

生产者: APP,再深入点就是canvas->surface。

消费者:SurfaceFlinger

BufferQueue 的大小一般是3。

  • 一块缓冲区用来被SurfaceFlinger交由设备展示
  • 一块用来App绘制缓冲数据
  • 还有一块,如果App绘制超过一帧时间16ms的时候,当下一帧vsync到来,其中两块都已经被占用,所以要用到第三块,避免此次vsync信号CPU和GPU处于空闲(因为如果空闲的话,下下帧就会出现jank)。

SurfaceFlinger 做了什么

SurfaceFlinger是显示合成系统。在应用程序请求创建surface的时候,SurfaceFlinger会创建一个Layer。Layer是SurfaceFlinger操作合成的基本单元。所以,一个surface对应一个Layer。

当应用程序把绘制好的GraphicBuffer数据放入BufferQueue后,接下来的工作就是SurfaceFlinger来完成了。

image.png

说明:

系统会有多个应用程序,一个程序有多个BufferQueue队列。SurfaceFlinger就是用来决定何时以及怎么去管理和显示这些队列的。

SurfaceFlinger请求HAL硬件层,来决定这些Buffer是硬件来合成还是自己通过OpenGL来合成。

最终把合成后的buffer数据,展示在屏幕上。

最后,附上官方完整渲染架构:

image.png

0 个评论

要回复文章请先登录注册