Android硬件渲染流程
- 一.渲染流程
- 1.VSync信号的监听
- 2.VSync信号触发绘制
- 二.渲染原理
- 1.画布的获取
- 1.1 画布的创建
- 1.2 渲染指令列表的创建
- 2.绘制与渲染指令
- 2.1 矩形的绘制
- 2.2 硬件渲染指令
- 2.3 节点的绘制
- 3.绘制的提交
- 3.1 绘制结果的保存
- 3.2 绘制结果的获取
- 4.层级的构建
- 4.1 绘制结果的更新
- 4.2 构建的分发
- 4.3 构建数据的保存
- 5.渲染与渲染管线
- 5.1 节点的更新
- 5.2 节点的渲染
- 三.总结
- 1.硬件渲染
- 2.绘制流程
- 2.1 渲染指令
- 2.2 对应关系
- 3.渲染流程
- 3.1 渲染指令列表的获取
- 3.2 渲染层级的构建
- 3.3 渲染管线的渲染
一.渲染流程
1.VSync信号的监听
在Android中,App的渲染流程是从ViewRootImpl开始的。在回调Activity的onResume方法后,会调用ViewRootImpl的requestLayout方法触发页面中View的测量与绘制。
在requestLayout方法中,首先会调用checkThread方法检查当前线程是否为UI线程,如果不是,则抛出异常。接下来会调用scheduleTraversals方法。
在ViewRootImpl的scheduleTraversals方法中,主要做了两件事:
1)向主线程的消息队列发送一个同步信息屏障。
2)提交callbackType类型为CALLBACK_TRAVERSAL的TraversalRunnable。
2.VSync信号触发绘制
当VSync信号到来时,会执行TraversalRunnable的run方法,该方法内部会调用ViewRootImpl的doTraversal方法。
在ViewRootImpl的doTraversal方法中,主要做了两件事:
1)移除主线程消息队列的同步信息屏障。
2)调用performTraversals方法。
在ViewRootImpl的performDraw方法中,会调用draw方法。在ViewRootImpl的draw方法中,如果开启了硬件渲染,就会从mAttachInfo的mThreadedRenderer中获取ThreadedRenderer。并调用ThreadedRenderer的draw方法。
在ThreadedRenderer的draw方法中主要做了两件事:
1)从DecorView开始递归构建DisplayList。
2)唤醒Render线程对DisplayList进行渲染。
在ThreadedRenderer的updateRootDisplayList中主要做了四件事:
1)从DecorView开始向下分发draw方法,递归构建DisplayListOp。
2)获取最顶层的RecordingCanvas。
3)通过DecorView获取最终的RenderNode并绘制到RecordingCanvas上。
4)将DisplayListOp填充到Native层的RootRenderNode中。
二.渲染原理
1.画布的获取
在硬件渲染中,每个View都有一个RenderNode。当调用RenderNode的beginRecording方法时,内部会调用RecordingCanvas的静态方法obtain获取RenderNode。
在RecordingCanvas的静态方法obtain中,会创建RecordingCanvas。
在RecordingCanvas的构造方法中,主要做了两件事:
1)创建Native层Canvas并返回对应的地址。
2)调用父类的构造方法对返回的地址进行保存。
1.1 画布的创建
RecordingCanvas的nCreateDisplayListCanvas方法对应的Native实现为android_graphics_DisplayListCanvas的android_view_DisplayListCanvas_createDisplayListCanvas函数。
在android_view_DisplayListCanvas_createDisplayListCanvas函数中,主要做了三件事:
1)获取Native层的RenderNode。
2)创建Canvas。
3)返回Canvas对应的地址。
在Canvas::create_recording_canvas方法中,会创建SkiaRecordingCanvas。
在SkiaRecordingCanvas的构造方法中,会调用initDisplayList方法,初始化DisplayList。
1.2 渲染指令列表的创建
在SkiaRecordingCanvas的initDisplayList方法中,主要做了三件事:
1)清除上一帧的SkiaDisplayList,如果SkiaDisplayList为空,则再创建一个SkiaDisplayList。
2)绑定SkiaDisplayList与RecordingCanvas。
3)将RecordingCanvas保存到SkiaCanvas中。
在SkiaDisplayList的attachRecorder方法中,会将SkiaDisplayList中的SkiaDisplayData与RecordingCanvas绑定。
2.绘制与渲染指令
2.1 矩形的绘制
在硬件绘制过程中,当调用Canvas的drawRect方法时,在Canvas的drawRect方法中,会调用nDrawRect方法。
Canvas的nDrawRect方法对应的Native实现为android_graphics_Canvas的drawRect函数。在drawRect函数中,主要做了两件事:
1)获取Native层Canvas。
2)通过Canvas绘制矩形。
SkiaRecordingCanvas继承自SkiaCanvas。这里的drawRect方法在SkiaRecordingCanvas的父类SkiaCanvas中实现。
在SkiaCanvas的drawRect方法中,会调用SkCanvas的drawRect方法。
在SkCanvas的drawRect方法中,会调用子类RecordingCanvas的onDrawRect方法。
在RecordingCanvas的onDrawRect方法中,会调用之前保存的DisplayListData的drawRect方法。在DisplayListData的drawRect方法中,会创建一个DrawRect指令并保存。
2.2 硬件渲染指令
在DisplayListData中,所有的绘制指令都存储在一块连续的内存中。
在Android中,所有的绘制指令都继承了Op。Op是一个结构体,在Op中有两个字段,type表示指令的类型,skip表示指令的长度。
struct Op {
uint32_t type : 8;
uint32_t skip : 24;
};
在DisplayListData的push方法中,主要做了四件事:
1)计算当前指令的长度。
2)判断所有的绘制指令是否超过内存最大容量,如果超过最大容量,则进行扩容,重新分配内存,每次扩容增加4096个字节。
3)计算当前绘制指令在内存中的位置,并在指定位置处创建当前指令的实例对象。
4)为当前绘制指令的type和skip赋值。
2.3 节点的绘制
在ThreadedRenderer的updateViewTreeDisplayList方法中,会调用View的updateDisplayListIfDirty方法。
在View的updateDisplayListIfDirty方法中,主要做了三件事:
1)从自身的RenderNode中获取RecordingCanvas。
2)通过Flag判断,如果是ViewGroup且自身不用绘制,则分发子View去绘制,否则直接绘制。
3)结束绘制。
在View的draw方法中,首先会绘制自身。然后对子View进行绘制。dispatchDraw方法在View中为空实现,如果一个View不是ViewGroup,那么dispatchDraw方法不会对任何View进行绘制分发。
View的dispatchDraw方法在ViewGroup中被重写。如果一个View是ViewGroup,那么dispatchDraw方法会对子View进行绘制分发。在ViewGroup的dispatchDraw方法中,会调用drawChild方法。
在ViewGroup的drawChild方法中,会调用View的draw方法。这里的draw方法是View中一个重载的draw方法,只在ViewGroup中调用。
在View重载的draw方法中,主要做了三件事:
1)判断是否开启硬件渲染。
2)如果开启硬件渲染,则对当前View构建DisplayList,保存到当前View的RenderNode并返回。
3)如果当前View的DisplayList不为空,则将当前View的RenderNode绘制到父View的Canvas上。
在RecordingCanvas的drawRenderNode方法中,会调用nDrawRenderNode方法。
RecordingCanvas的nDrawRenderNode方法对应的Native层实现为android_graphics_DisplayListCanvas的android_view_DisplayListCanvas_drawRenderNode函数。
在android_view_DisplayListCanvas_drawRenderNode函数中,主要做了三件事:
1)根据地址获取Native层Canvas。
2)根据地址获取View对应的Native层的RenderNode。
3)将RenderNode绘制到Canvas上。
根据硬件渲染中Canvas的创建过程可以知道,这里的Canvas实际上是SkiaRecordingCanvas。
在SkiaRecordingCanvas的drawRenderNode方法中,主要做了两件事:
1)将RenderNode封装成RenderNodeDrawable,保存到SkiaDisplayList中用于记录。
2)对封装好的RenderNodeDrawable进行绘制。
SkiaRecordingCanvas继承自SkiaCanvas。这里的drawDrawable方法在SkiaRecordingCanvas的父类SkiaCanvas中实现。
在SkiaCanvas的drawDrawable方法中,会调用SkCanvas的drawDrawable方法。
在SkCanvas的drawDrawable方法中,会调用子类RecordingCanvas的onDrawDrawable方法。
在RecordingCanvas的onDrawDrawable方法中,会调用之前保存的DisplayListData的drawDrawable方法。在DisplayListData的drawDrawable方法中,会创建一个DrawDrawable指令并保存。
3.绘制的提交
3.1 绘制结果的保存
在RenderNode的endRecording方法中,主要做了两件事:
1)停止记录,标记DisplayList可用。
2)释放RecordingCanvas。
在RecordingCanvas的finishRecording方法中,会调用nFinishRecording方法。
RecordingCanvas的nFinishRecording方法对应的Native实现为android_graphics_DisplayListCanvas的android_view_DisplayListCanvas_finishRecording函数。
在android_view_DisplayListCanvas_finishRecording函数中,主要做了三件事:
1)获取Canvas,实际获取的是SkiaRecordingCanvas。
2)获取RenderNode。
3)调用SkiaRecordingCanvas的finishRecording结束绘制。
在SkiaRecordingCanvas的finishRecording方法中,主要做了三件事:
1)暂停标记并获取SkiaDisplayList。
2)将SkiaDisplayList封装成DisplayList,DisplayList是对SkiaDisplayListWrapper的重命名,SkiaDisplayListWrapper会保存SkiaDisplayList。
3)将DisplayList保存到RenderNode中。
3.2 绘制结果的获取
在DisplayList构建完成后, 会调用ThreadedRenderer的syncAndDrawFrame方法唤醒Render线程进行渲染。在ThreadedRenderer的syncAndDrawFrame中,会调用nSyncAndDrawFrame方法。
ThreadedRenderer的nSyncAndDrawFrame方法对应的Native实现为android_graphics_HardwareRenderer的android_view_ThreadedRenderer_syncAndDrawFrame函数。
在android_view_ThreadedRenderer_syncAndDrawFrame函数中,主要做了两件事:
1)获取RenderProxy。
2)调用RenderProxy的syncAndDrawFrame方法。
在RenderProxy的syncAndDrawFrame方法中,会调用DrawFrameTask的drawFrame方法。在DrawFrameTask的drawFrame方法中,会调用postAndWait方法。
在DrawFrameTask的postAndWait方法中,主要做了三件事:
1)对DrawFrameTask的run方法进行封装。
2)将封装后的对象添加到RenderThread的队列中。
3)UI线程进入阻塞状态。
当RenderTread执行任务时,会调用DrawFrameTask的run方法。在DrawFrameTask的run方法中,主要做了三件事:
1)获取UI线程构建的DisplayList。
2)唤醒UI线程。
3)根据DisplayList进行绘制。
4.层级的构建
在DrawFrameTask的syncFrameState方法中,主要做了两件事:
1)处理硬件加速层,如TextureView的绘制。
2)构建TreeInfo。
在CanvasContext的prepareTree方法中,主要做了两件事:
1)保存LayerUpdateQueue到TreeInfo中,为后续后构建TreeInfo做准备。LayerUpdateQueue用于保存待绘制的RenderNode。
2)遍历RenderNode构建TreeInfo。
在RenderNode的prepareTreeImpl方法中,主要做了三件事:
1)获取DisplayList。
2)通过DisplayList分发子节点构建TreeInfo。
3)将完成构建的当前RenderNode保存到LayerUpdateQueue中。
4.1 绘制结果的更新
在RenderNode的pushStagingDisplayListChanges方法中,会调用syncDisplayList方法,对DisplayList进行锁定保存。
在RenderNode的syncDisplayList方法中,主要做了三件事:
1)遍历mStagingDisplayList中保存的RenderNode,对RednerNode的引用加1。mStagingDisplayList是用于暂存本次构建好的DisplayList的变量。
2)遍历mDisplayList中保存的RenderNode,对RednerNode的引用减1,并清空mDisplayList中对SkiaDisplayList的引用。mDisplayList是用于保存下次待渲染的DisplayList的变量。
3)将本次构建好的DisplayList保存到下次待渲染的DisplayList。
DisplayList是对SkiaDisplayListWrapper的重命名。在SkiaDisplayListWrapper的updateChildren方法中,会调用SkiaDisplayList的updateChildren方法。
在SkiaDisplayList的updateChildren方法中,主要做了三件事:
1)遍历获取RenderNodeDrawable。
2)从RenderNodeDrawable中获取RenderNode。
3)将RenderNode作为参数,执行参数中传入的function方法。
4.2 构建的分发
DisplayList是对SkiaDisplayListWrapper的重命名。在SkiaDisplayListWrapper的prepareListAndChildren方法中,会调用SkiaDisplayList的prepareListAndChildren方法。
在SkiaDisplayList的prepareListAndChildren方法中,主要做了四件事:
1)遍历获取RenderNodeDrawable。
2)从RenderNodeDrawable中获取RenderNode。
3)对TreeInfo中的属性进行更新。
4)将RenderNode和TreeInfo作为参数,执行参数中传入的function方法。
这里传入的function是RenderNode的prepareTreeImpl方法,这样就实现了子RenderNode构建TreeInfo。
4.3 构建数据的保存
在RenderNode的pushLayerUpdate方法中,会对当前的RenderNode进行保存。
5.渲染与渲染管线
在Android中,渲染管线有两种:SkiaOpenGLPipeline和SkiaVulkanPipeline,底层实现分别对应着OpenGL和Vulkan。下面所有的IRenderPipeline以SkiaOpenGLPipeline为例。
在CanvasContext的draw方法中,主要做了三件事:
1)获取可绘制的缓存。
2)使用GPU按照绘制指令绘制界面。
3)将绘制好的图形缓冲通过Binder交给SurfaceFlinger进行合成与显示,即上帧。
在SkiaOpenGLPipeline的draw方法中,主要做了两件事:
1)创建SkSurface指针并初始化。
2)开始渲染。
SkiaOpenGLPipeline的renderFrame方法在SkiaOpenGLPipeline的父类SkiaPipeline中实现。
在SkiaPipeline的renderFrame方法中,主要做了四件事:
1)获取SkCanvas。
2)处理发生变化的Layer,更新对应RenderNode。
3)对所有的renderNode进行绘制。
4)释放SkCanvas。
5.1 节点的更新
在SkiaPipeline的renderLayersImpl方法中,主要做了三件事:
1)遍历LayerUpdateQueue,获取Entry,并从Entry中获取RenderNode.
2)从RenderNode中获取SkCanvas。
3)将RenderNode分装成RenderNodeDrawable,并绘制到SkCanvas上。
RenderNodeDrawable继承自SkDrawable。在RenderNodeDrawable的draw方法中,会调用onDraw方法,onDraw方法在RenderNodeDrawable被重写。在RenderNodeDrawable的onDraw方法中,会调用forceDraw方法。在RenderNodeDrawable的forceDraw方法中,会调用drawContent方法。
在RenderNodeDrawable的drawContent方法中,主要做了两件事:
1)从RenderNode中获取DisplayList。
2)绘制DisplayList。
在SkiaDisplayList的draw方法中,会调用DisplayListData的draw方法。
在DisplayListData的draw方法中,会调用map方法。在DisplayListData的map方法中,主要做了五件事:
1)计算绘制指令内存中绘制指令的终止区域。
2)从绘制指令内存的起始位置进行遍历。
3)获取绘制指令的类型和长度。
4)根据指令的类型,调用指令的绘制方法。
5)根据指令的长度,计算下一个绘制指令的位置。
Array<Fn>是模版生成的代码,其中的模版参数Fn代表不同类型的绘制指令的draw方法的调用。以DrawRect指令为例,当ptr指针指向DrawRect指令时,会根据指令类型从Array<Fn>取出对应DrawRect的draw方法调用的Fn,当调用Fn时,会触发DrawRect的draw方法的执行。
在DrawRect的draw方法中,会通过SkCanvas的draw方法完成绘制。
5.2 节点的渲染
在SkiaPipeline的renderFrameImpl方法中,主要做了两件事:
1)对RenderNode进行遍历,将RenderNode封装成RenderNodeDrawable。
2)对RenderNodeDrawable进行渲染。
与renderLayerImpl不同,renderFrameImpl绘制到缓存对应的SkCanvas上,而不是自身的SkCanvas上。
三.总结
1.硬件渲染
Android硬件渲染分成两个部分:渲染指令列表的构建和渲染指令列表的渲染,分别对应着ThreadedRenderer的updateRootDisplayList方法和syncAndDrawFrame方法。即绘制过程和渲染过程是分开的。
绘制过程发生在UI线程,渲染过程发生在Render线程。
2.绘制流程
在硬件渲染中,每个View对应着一个RenderNode。每个RenderNode中保存着对应的SkiaDisplayList。
硬件的绘制过程,本质上是将待绘制的数据封装成对应的硬件渲染指令,保存到SkiaDisplayList中。子View绘制到完成后,父View会对子View的RenderNode进行绘制,将子View的RenderNode绘制指令保存到SkiaDisplayList中,形成绘制的层级结构。
画布的获取本质上是对SkiaDisplayList的创建与初始化。
绘制的提交本质上是将构建好的SkiaDisplayList保存到RenderNode中。
2.1 渲染指令
在Android中,所有的绘制指令都继承了Op。Op是一个结构体,在Op中有两个字段,type表示指令的类型,skip表示指令的长度。
struct Op {
uint32_t type : 8;
uint32_t skip : 24;
};
所有的渲染指令都保存到DisplayListData中,DisplayListData本质上是一块连续的内存。
2.2 对应关系
SkiaRecordingCanvas继承自SkiaCanvas,RecordingCanvas继承自SkCanvas。
RecordingCanvas负责管理硬件渲染指令和DisplayListData、操作硬件渲染指令在DisplayListData上的填充。SkiaRecordingCanvas是对RecordingCanvas的封装,负责DisplayListData的大小分配与初始化、SkiaDisplayList的管理和子View的RenderNode更新保存。
RecordingCanvas直接操作DisplayListData,SkiaRecordingCanvas直接操作SkiaDisplayList。SkiaDisplayList是对DisplayListData的屏蔽封装。
3.渲染流程
硬件的渲染过程分成三部分:渲染指令列表(SkiaDisplayList)的获取、渲染层级(TreeInfo)的构建、渲染管线(IRenderPipeline)的渲染。
3.1 渲染指令列表的获取
渲染指令列表的获取过程会阻塞UI线程,在获取完成后会唤醒UI线程。
3.2 渲染层级的构建
渲染层级的构建过程本质上是从DecorView的RootRenderNode开始向下判断哪些子View的RenderNode发生了变化,并把变化的RenderNode保存到TreeInfo中。
3.3 渲染管线的渲染
在Android中,渲染管线有两种:SkiaOpenGLPipeline和SkiaVulkanPipeline,底层实现分别对应着OpenGL和Vulkan。
渲染管线的渲染过程分成两部分:RenderNode的更新渲染和RenderNode的最终渲染。
渲染管线在渲染过程中会将RenderNode封装成RenderNodeDrawable。RenderNode的更新渲染本质上就是在RenderNode自身的SkCanvas上绘制。RenderNode的最终渲染本质上就是在生产消费模型对应的SkCanvas上绘制。