为了账号安全,请及时绑定邮箱和手机立即绑定

从零开始仿写一个抖音App——Android绘制机制以及Surface家族源码全解析

标签:
Android

大家好,新的一年又正式开始了,笔者在这里给大家拜个晚年。最近写的文章需要涉及到的知识很多,所以更新比较慢,望大家海涵。

本篇文章分为以下章节,读者可以按需阅读

  • 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屏幕刷新机制

webp

图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,queuedequeue

    • 2.消费者:它也可以使用两个 api,acquirerelease

webp

图2:BufferQueue.png

  • 2.图像内存的生产者:

    • 1.Surface:Surface 是 BQ 的生产者。当我们使用 lockCanvasunlockCanvasAndPost 的时候,就是先从 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 层的东西。

webp

图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 树。

    • webp

      图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的创建

webp

图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 中的内容。

webp

图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


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消