大家好,新的一年又正式开始了,笔者在这里给大家拜个晚年。最近写的文章需要涉及到的知识很多,所以更新比较慢,望大家海涵。
本篇文章分为以下章节,读者可以按需阅读
1.Android绘制机制概览
2.Android绘制机制源码分析
3.Surface家族源码全解析
4.总结
阅读须知
1.进入微信公众号 世界上有意思的事 发送消息:Android绘制机制以及Surface家族源码全解析,即可获取本文的 pdf 版。
2.本文分析的源码版本是 Android 7.0,建议结合源码阅读本文
3.推荐一个 Android 源码阅读网站:Android 源码阅读网站
4.因为很多 java 层和 c++ 层的类命名都相同,所以后续例如:Surface.lockCanvas 表示 java 层的调用,Surface::lockCanvas 表示 c++ 层的调用
5.本文的一些缩写:SF——>SurfaceFlinger、WMS——>WindowManagerService、MessageQueue——>MQ、GL——>OpenGL、BQ——>BufferQueue、ST——>SurfaceTexture、TV——>TextureView、SV——>SurfceView
6.本文是视频编辑SDK开发的重要前置知识,建议读透
一、Android绘制机制概览
在深入各种源码之前,我们需要了解 Android 绘制机制的整体架构,以及一些概念
1.Android屏幕刷新机制
图1:屏幕刷新.jpg
图1就是 Android 屏幕显示的抽象示意图,这里我来解释一下:
1.首先图的横轴是时间,纵轴从下到上分别表示:CPU 处理、GPU 处理、屏幕显示,这三个步骤也就是我们写的代码到图像显示在屏幕上的流程。
2.我们都知道要让手机不卡顿一个显而易见的标准就是:屏幕上每隔一定的 ms 就能显示下一帧图像。在这里这个时间由底层控制,也就是图中两个 VSync 的间隔时间——16ms。
3.Android 中引入了下面这些特性来保证屏幕上的数据每隔 16ms 来刷新一次。
1.一个固定的脉冲信号——VSync,这个东西由底层保证,每一次 VSync 信号来了 CPU 就开始运行绘制代码(例如运行 View.draw 之类的方法),当 CPU 的数据准备好了,就将这些数据交给 GPU 让其在一块内存缓冲区上进行图像的绘制。当 GPU 绘制好了就将图像显示到屏幕上。
2.三缓冲,图中的 A、B、C 表示的是三块内存缓冲区。因为不是每次 CPU、GPU 的数据处理都能在16ms 内完成的,所以为了不浪费时间而且不丢弃之前做完的工作。CPU、GPU 的工作会依次反应在 A、B、C 三块内存缓冲区中。而屏幕每次都取当前已经准备好的内存缓冲区。三缓冲较双缓冲的问题就是:我们的操作最终显示到屏幕上的时候会延迟16ms,这可能也是 Android 不如 ios ”跟手“的一个原因
4.由我们可以得出两个简单的结论:
1.ui线程太忙了,使得 CPU 16ms 内没有处理好数据会导致丢帧。
2.需要绘制的图像太复杂,导致 GPU 16ms 没有绘制好图像也会导致丢帧。
2.Android图像绘制方式
问大家一个问题:平时我们开发过程中可以用哪些工具在屏幕上绘制图像?大家一定可以回答出很多东西:View、Drawable、xml、SV、GLSurfaceView、TV、Canvas等等。其实 Android 上面一共只有两种常用的绘图机制,上面列举出来的东西都是由这两种机制演变而来的。这一节我就简单归纳介绍一下。
Android 的两种常用绘图机制:
1.Skia图形库:Skia官网,Skia是一个开源的二维图形库,提供各种常用的API,并可在多种软硬件平台上运行。在没开启硬件加速的时候用到 Canvas 的地方在底层都会调用到 Skia 库中去。在上面我们列举的方式里面:View、Drawable、xml、SV、Canvas 最终使用的都是 Skia 库。另外 Skia 最终是使用 CPU 来对图像进行最终绘制,所以效率比较低。
2.GL:GL 是用于渲染 2D、3D 矢量图形的跨语言、跨平台的应用程序编程接口。在开启硬件加速的时候用到 Canvas 的地方最终会调用到 GL ES 库中。没开启硬件加速的时候上面我们列举的方式中:GLSurfaceView、TV 最终使用的是 GL ES,另外 GL 使用的是 GPU 来对图像进行绘制,所以效率比较高。
3.在后续的章节里我会从源代码上分析上面各种绘图方式的调用链。
4.其实在 7.0 之后 Android 中添加了 Vulkan。Vulkan 是用于高性能 3D 图形的低开销、跨平台 API。与 GL ES 一样,Vulkan 提供多种用于在应用中创建高质量的实时图形的工具。不过目前很少用到,所以本篇文章中我们不讨论它。
3.Android绘制中的生产者和消费者
android 的绘制机制中存在着一系列生产者和消费者,这一节我将介绍一下这个机制中相关的概念。
1.BQ:如图2所示,BQ 是一个存储着内存块的队列。
1.acquire:当需要一块已经绘制完成的内存再对其进行处理的时候,可以从队列的头部拿出一块内存。
2.release:当对图像内存处理完毕的时候,可以将内存重置然后放回队列的尾部
1.dequeue:需要一块内存来绘制图像的时候,可以从队列的尾部拿出一块内存进行绘制。
2.queue:当图像绘制完毕的时候,可以将该内存添加到队列的头部
1.生产者:它可以使用两个 api,queue 和 dequeue。
2.消费者:它也可以使用两个 api,acquire 和 release。
图2:BufferQueue.png
2.图像内存的生产者:
1.Surface:Surface 是 BQ 的生产者。当我们使用 lockCanvas 和 unlockCanvasAndPost 的时候,就是先从 BQ 中取出一块内存然后调用 Canvas/GL 的 api 对内存进行绘制,最后将内存放回 BQ 中。
2.我们知道了 Surface 是生产者,那么像 View、SV、GLSurfaceView 这些间接或者直接用到了 Surface 的东西就都是生产者了。
3.图像内存的消费者:
1.SF:它有自己的进程,在 Android 系统启动的时候就像其他各种 Service 一样被创建了。它的作用是接受来自多个来源的内存缓冲区,对它们进行合成,然后发送到显示设备。大多数应用通常在屏幕上有三个层:屏幕顶部的状态栏、底部或侧面的导航栏以及应用的界面。所以应用的 Surface 生产的内存就会被它所消耗。
2.ST:这个东西是常常用在 TV 中。它可以在我们的应用中使用。它在创建的时候会建立一个自己的 BQ。我们可以通过 ST 来创建一个 Surface 然后通过 Surface 向 BQ 中提供图像内存。此时 ST 就可以消耗这些图像内存。它可以使用 GL 来对这些被消耗的图像内存进行二次处理,然后让这些被处理之后的图像内存在通过 GLSurfaceView 之类的东西显示到屏幕上。
二、Android绘制机制源码分析
这一章我们来从源码上分析 View 是如何绘制到屏幕上面的,前面的 measure、layout、draw 等等 framework 层的东西我不会着重分析,我主要分析 cpp 层的东西。
图3:Android绘制机制.png
其实源码的主要流程都在图3中,我下面讲的东西算是对图3的补充和说明。另外强烈建议结合 Android 源码阅读本章节。
1.首先我们的入口是 ViewRootImpl.scheduleTraversals。看过源码的同学应该知道,类似 invalidate、requestLayout 等等需要改变 View 的显示的操作,最终都会层层向上调用最终调用到这个方法上。
1.postCallback 根据 scheduleFrameLocked——>scheduleVsyncLocked——>scheduleVsyncLocked 的调用链,最终会调用到 DisplayEventReceiver.nativeScheduleVsync 向native 层申请下一个 VSync 信号。
1.在这个方法里主要调用到的方法就是 Choreographer.postCallback 这个方法传入了 mTraversalRunnable,表示在某个时间点会调用这个 Runnable 中的 run()。我们在第一章中讲过 VSync 的相关知识,而 Choreographer 就是 VSync 在代码层面的实现。
2.16ms 后 VSync 信号会从 native 层向上触发,这里是想 ui 的 loop 中添加了一个 msg。这样的话调用链就是:DisplayEventReceiver.dispatchVsync——>DisplayEventReceiver.onVsync——>Choreographer.doFrame。
1.这样就会调用到 ViewRootImpl.performTraversals 中去。我想大家应该对这个方法很熟悉,这个方法就是调用 measure、layout、draw 的方法。已经分析烂了的东西这里我就不说了。
2.我们直接看 ViewRootImpl.draw 方法,这里会有两种绘制方式。就像我们在第一章中说的那样。如果没有开启硬件加速那么就使用 Skia 库以 CPU 来绘制图像,这里的入口方法是 drawSoftware。如果开启了硬件加速那么就是用 GL ES 来以 GPU 绘制图像,这里入口方法是 ThreadedRenderer.draw。
1.硬件加速绘制的五个阶段:
2.硬件加速绘制代码分析:
1.APP在UI线程使用 Canvas 递归构建 GL 渲染需要的命令及数据
2.CPU 将准备好的数据共享给 GPU
3.CPU 通知GPU渲染,这里一般不会阻塞等待GPU渲染结束,因为效率很低。CPU 通知结束后就返回继续执行其他任务。当然使用 glFinish 可以阻塞执行。
4.swapBuffers,并通知 SF 图层合成
5.SF 开始合成图层,如果之前提交的GPU渲染任务没结束,则等待GPU渲染完成,再合成(Fence机制),合成依然是依赖GPU
1.run() 这里会先调用 syncFrameState,这个方法主要是用于同步 java 层的各种数据。
2.run() 中将数据同步完成之后,就会调用 CanvasContext.draw,这个方法主要有三个操作:
3.当所有的绘制操作都通过 GL 提交给了 GPU 的时候,如果前面数据同步失败了,那么这个时候需要唤醒 ui thread。
1.调用 pushStagingDisplayListChanges 同步当前 RenderNode.cpp 的属性,也就是把 mStagingDisplayListData 赋值给 mDisplayListData
2.调用 prepareSubTree 递归处理子 RenderNode.cpp。
3.这里会有一个同步成功和同步失败的问题,一般来说这里的数据都会同步成功的。但是在 RenderNode::prepareSubTree 中会有一个步骤是把 RenderNode 用到的 Bitmap 封装成纹理,一旦这里 Bitmap 太大或者数量太多那么同步就会失败。注意这里同步失败只是会与 Bitmap 有关,其他的 DrawOp 数据无论如何都会同步成功的。
1.第一步是先用 mLayers::apply 来同步数据,这个 mLayers 在 java 层的体现是 TV。这里的分析我们会在第三章中着重分析,这里先略过。
2.第二步是调用 CanvasContext::prepareTree 来将前面在 java 层构建的 DrawOp 树同步到 c++ 层,以便后续运行 OpengGL 的命令。这里关键的调用链是:CanvasContext::prepareTree——>RenderNode::prepareTree——>RenderNode::prepareTreeImpl。由前面我们可以知道 RenderNode.java 已经构建了一个 DrawOp 树。但是之前只是调用 RenderNode::setStagingDisplayList 暂存在 RenderNode::mStagingDisplayListData 中的。因为 java 层在运行过程中还会出现多次meausre、layout的,还有数据还可能发生改变。所以当走到这里的时候数据已经确定了,所以可以开始同步数据。prepareTreeImpl 同步数据主要有三个步骤:
3.如果这里同步成功了的话,那么 ui thread 就会被唤醒,反之则暂时不唤醒。
1.mEglManager::beginFrame,其实是标记当前上下文,并且申请绘制内存,因为一个进程中可能存在多个window,也就是多个 EglSurface,那么我们首先需要标记处理哪个,然后向 SF 申请内存,这里 EglSurface 是生产者,SF 是消费者。
2.根据前面的 RenderNode 的 DrawOp 树,递归调用 OpenGLRender 中的 GL API,进行绘制。
3.通过 swapBuffers 将绘制好的数据提交给 SF 去合成,值得注意的是此时可能 GPU 并没有完成当前的渲染任务,但是为了提高效率,这里可以不用阻塞渲染线程。
1.这个方法最终会调用到 c++ 层的 RenderProxy::syncAndDrawFrame 方法。在了解这里的调用链之前。我先介绍一下 Android 5.0 之后出现的渲染线程的概念,再来讲解调用链。
1.首先渲染线程的实现类是 RenderThread.cpp 它是和 ui 线程类似,是一个事件驱动的 Loop。它的也有队列,队列中储存着 DrawFrameTask.cpp 对象。RenderProxy.cpp 是 RenderThread.cpp 给 java 层的代理。ThreadRender 所有关于渲染线程的请求都会交给 RenderProxy.cpp 然后由它向 RenderThread.cpp 提交 task。
2.了解了渲染线程的概念,我们再来讲讲调用链:syncAndDrawFrame——> DrawFrameTask::drawFrame——>DrawFrameTask::postAndWait。这里做的事情很简单,向 RenderTheas.cpp 的队列中插入一个 DrawFrameTask.cpp,然后阻塞当前的 ui 线程。
1.这个方法的第一个调用链是这样的 updateViewTreeDisplayList——>View.updateDisplayListIfDirty——>View.draw。如图4,这里聪明的同学一看就知道是一个递归操作。View 的 draw 会递归到子 View 中。然后各个 View 会调用 Canvas 的 api 将绘制操作储存在 Canvas 中。那么这里的 Canvas 是怎么来的呢?其实在每个 View 创建的时候内部会创建一个 RenderNode 。这个对象可以创建一个 DisplayListCanvas 来作为 Canvas 给各个 View 在绘制的时候使用。而每个子 View 调用 draw(Canvas, ViewGroup, long) 的时候都会得到 parentView 传递下来的 DisplayListCanvas,然后在本 View.draw(Canvas) 调用结束之后,将DisplayListCanvas 的操作储存到本 View 的 RenderNode 中。最后调用 parentView 的 DisplayListCanvas.drawRenderNode 将本 View 的 RenderNode 存入 parentView 的 RenderNode 中。如此递归,最终将所有绘制操作存入 RootView 的 RenderNode 中。至此 RootView 中就包含了一个 DrawOp 树。
图4:硬件加速下的Canvas绘制流程.png
2.我们回到 updateRootDisplayList 这里后续就是将 RootView 的 DrawOp 树 交给 ViewRootImpl 的 RenderNode 方便后面进行操作。
1.我们先看 draw() 里面调用的 updateRootDisplayList:
2.再回到 draw() 中,这里下一个调用的重要的方法是 nSyncAndDrawFrame:
3.ui 线程被阻塞之后,渲染线程会调用到上一次插入到队列里的 DrawFrameTask.run 方法。
1.drawSoftware:这个方法比较简单就是创建一个 Surface,然后 lockCanvas 得到一个 Canvas,然后在各个层级的 View 中调用 Canvas 最终调用 Skia 库来在 Surface 上提供的图像内存中绘制图像,画完之后调用 unlockCanvasAndPost 来提交图像内存。这里更详细的流程我会在第三章中分析 Surface 的时候分析。
2.ThreadedRenderer.draw:这个方法是硬件加速下的图像绘制入口,里面最终都是调用到 GL 的 api。在深入之前我们先了解一下硬件加速绘制的几个阶段:
1.到了 doFrame 这里就会调用前面 postCallback 中传入的 mTraversalRunnable 的 run()。我们知道 run() 中会调用 ViewRootImpl.doTraversal 方法。
三、Surface家族源码全解析
上一章我们讲了 Android 的整个绘制机制,但是其中涉及到 Surface 的部分都是简单带过。所以这一章我就来解析一下 Surface 家族中各个成员的源码。
1.Surface的创建与绘制
(1).Surface的创建
图5:Surface 创建.png
**这里我们以 View 的创建流程为例,讲述一下 Surface 在这个过程中的创建流程,Surface 的创建流程如图5所示。 **
1.首先我们需要知道的是 ViewRootImpl 在创建的时候会使用 new Surface 来创建一个 java 层的空壳。这个空壳会在后面被初始化。
2.然后入口的调用链是这样的:ViewRootImpl.performTraversals——>ViewRootImpl.relayoutWindow——>WindowManagerService.relayoutWindow——>WindowManagerService.createSurfaceControl。
1.nativeCreate 的第一步是创建一个 SurfaceComposerClient.cpp 它其实是 SF 所在进程的代理,我们可以通过这个类对 SF 进行操作。在调用 new SurfaceComposerClient.cpp 的构造函数之后,首先会触发 onFirstRef,这里面则会使用 ComposerService::getComposerService() 获取 SF 的服务。然后远程调用 ComposerService::createConnection 也就是 SF::createConnection 来创建一个 ISurfaceComposer 作为 SurfaceComposerClient 与 SF 进程交互的接口,这里用到的是 Binder 机制。
2.SurfaceComposerClient 创建完毕之后,就可以调用 SurfaceComposerClient::createSurface——>Client::createSurface 来向 SF 进程发送创建 Surface 的请求了。SF 进程也是事件驱动模式,所以 Client::createSurface 中调用 SF::postMessageSync 发送了一个调用 SF::createLayer 方法的消息给事件队列。 而 createLayer 最终会调用 createNormalLayer 中。这个方法会返回一个 IGraphicBufferProducer 给 SurfaceControl.cpp。记得我们在前面讲的 生产者——消费者 模式吗?这里的 IGraphicBufferProducer 就是 SF 的 BQ 分离出来的生产者,我们后续就可以通过这个 IGraphicBufferProducer 向 SF 的 BQ 中添加通过 Surface 绘制好的图像内存了。
1.createSurfaceControl 这个方法里首先会 WindowStateAnimator.createSurfaceLocked——>new WindowSurfaceController——>new SurfaceControlWithBackground——>new SurfaceControl——>SurfaceControl.nativeCreate——>android_view_SurfaceControl::nativeCreate 来创建一个SurfaceControl.cpp 这个东西是初始化 Surface 的参数。
2.回到 createSurfaceControl 这里创建了一个 SurfaceControl.java 之后,下一步就是初始化 Surface.java。这里就比较简单了,就是通过调用链:SurfaceController.getSurface(Surface)——>Surface.copyFrom(SurfaceControl)——>Surface.nativeCreateFromSurfaceControl——>SurfaceControl::getSurface。将 SurfaceControl.cpp 中的 IGraphicBufferProducer 作为参数创建一个 Surface.cpp 交给 Surface.java。
(2).Surface的绘制
我们在第二章里面说到 View 的绘制根据是否硬件加速分为,软件绘制与硬件绘制两种。当时我们分析了硬件绘制,软件绘制略过了。其实软件绘制与硬件绘制的区别就在于是使用 CPU 进行绘制计算还是使用 GPU 进行绘制计算。这一小节的 Surface 绘制其实就是软件绘制,也就是 ViewRootImpl.drawSoftware 中的内容。
图6:Surface 绘制.png
我们都知道 Surface 可以通过 lockCanvas 和 unlockCanvasAndPost 这两个 api 来再通过 Canvas 来绘制图像,这一节我就通过这两个 api 来讲讲 Surface 的绘制流程,整个流程如图6所示。
1.首先我们从 lockCanvas 这个入口开始调用链是:lockCanvas——>nativeLockCanvas——>Surface::lock——>Surface::dequeueBuffer,这里最终会使用我们在 Surface 创建的时候得到的 BufferQueueProducer(IGraphicBufferProducer) 来想 SF 请求一块空白的图像内存。得到了图像内存之后,将内存传入 new SkBitmap.cpp 中创建对应对象,一共后面使用 Skia 库绘制图像。这里的 SkBitmap.cpp 会被交给 SkCanvas.cpp 而 SkCanvas.cpp 对象就是 Canvas.java 在 c++ 层的操作对象。
作者:何时夕
链接:https://www.jianshu.com/p/dec355e3afd5
共同学习,写下你的评论
评论加载中...
作者其他优质文章