前言
对于操作系统而言,图形显示部分是非常重要的一个模块,用来承载用户交互与内容展示,因此本文其实无法对Android的显示系统做非常详尽的分析,因为如果那样的话可能需要一个系列,而且对于一般开发者而言并没有这样的必要,因此本文主要从View的显示作为切入点来展开显示系统的基本原理以及它的运行机制,力求深入浅出,帮助大家对它有比较深入的理解。
本文分析依赖的Android源码版本主要是Android13(或以上)
关于View的显示
- 对于Android开发者而言,大家都接触过Android的View的开发,那么一个View是如何绘制并显示到设备的屏幕上呢?
+ 当然是ondraw方法中使用canvas进行绘制的,然后把绘制出的像素数据传递到显示系统中
- 那么canvas是什么?从哪里来的呢?
+ canvas我们称之为画布,通过canvas进行绘制操作之后,最终绘制的像素数据会渲染到屏幕上;而canvas则是通过Surface调用lockCanvas(rect)锁住绘制区域之后获得的。在我们绘制完成之后会释放掉。
- 那么什么是Surface?
+ 现在,我们可以说进入正题了,能够理解Surface,对于我们理解Android呃显示系统非常有帮助,因为**它是显示系统中的一个关键部分,而且是开发者可能接触到的那个部分。**
+ Surface称作表面,但实际是汇集该window所产生的渲染数据,然后统一传递到显示模块中。[stackoverflow](https://stackoverflow.com/questions/4576909/understanding-canvas-and-surface-concepts)中有一个关于surface,canvas的概念解释对surface的描述也很贴切
Android显示系统
我们先对Android显示系统做一个结构概括:那就是显示系统是一个多对一的生产消费模型架构的系统,数据生产端是View,camera,Opengl ES等。消费端一般是由surfaceflinger掌控并负责把渲染数据合成为一个帧,并发送到显示控制器(屏幕)。
或许这张图更加纯粹
生产者一般包括:
- Android View,通过canvas绘制产生图像
- media player 解码视频数据播放
- Camera 摄像头产生图像
- Opengl ES
而消费者主要是SurfaceFlinger。
生产者和消费者之间存在一个缓冲队列,用于发送数据和消费数据。但是对于生产者而言,其实并不直接与缓冲队列接触,而是通过Surface,所有的生产者都是通过Surface提供的接口输入渲染数据。或许我们可以简单的理解,对于数据生产方而言,Surface是渲染数据的汇集地。
接下来我们以Android view的绘制体系为例来看看显示系统中渲染数据是如何产生并且输入到缓冲队列中的。
渲染数据如何产生
关于Android View的绘制体系,其实指的就是在View Tree中通过onDraw回调,使用canvas逐个进行绘制的体系。
1 | typescript复制代码@Override |
因为前文已经问到了canvas的问题,确定是由Surface产生的,那么接下来问题就转移到Surface身上了,想要弄清楚渲染数据如何产生,就要先弄懂Surface的创建和使用细节,我们从代码层面来解释下为什么说Surface是一个重要的结构。
surface的创建过程
上层视角
与Surface伴随的类叫SurfaceController,这是要对Surface进行管理的一个类,我们能够在ViewRootImpl中看到这两个类的对象。而Surface的创建依赖于SurfaceController的创建,因此我们不得不先分析SurfaceController的创建过程。
先依次查看Surface和SurfaceController的类的主要结构
1 | scss复制代码public class Surface implements Parcelable{ |
Surface在Java层的结构中有一个mNativeObject属性,顾名思义就是指cpp层的surface对象,SurfaceController也是类似的情况。
1 | ini复制代码// Handle to an on-screen Surface managed by the system compositor |
我们可以在ViewRootImpl中找到mSurface,mSurfaceControl,它们都是是使用默认构造方法创建的对象,像这样涉及底层能力的类,一般是有一个cpp层的对象来实现真实的功能和交互的,因此实际上默认构造出来的对象还只是一个壳子,因此我们需要看它的cpp层是何时被创建出来的。
SurfaceControl的创建
1 | java复制代码// viewrootimpl.java |
relayoutWindow最终触发了跨进程调用到WMS.relayoutWindow中。然后进一步调用了createSurfaceControl方法,创建了一个SurfaceControl
1 | csharp复制代码// WindowManagerService.java |
总代码上看,实现逻辑是:WindowStateAnimator先通过构造方法创建一个的SurfaceControl对象,然后通过复制,把该对象复制到outSurfaceControl中(也就是ViewRootImpl中mSurfaceControl)。
我们先看创建SurfaceControl的构造函数所调用的native函数的详情
1 | arduino复制代码//frameworks/base/core/jni/android_view_SurfaceControl.cpp |
native层的实现逻辑主要是:通过SurfaceComposerClient这个surfaceflinger连接类,先在远端的surfaceflinger创建一个Surface(实际是Layer),然后再创建一个SurfaceControl对象返回,此时
SurfaceControl是一个完整对象(既有Java层对象也有cpp层对象)。
接着回到Java层,查看第二步getSurfaceControl的复制逻辑:
1 | arduino复制代码 |
创建了SurfaceControl之后,接着调用getSurfaceControl进行复制,复制时会调用nativeCopyFromSurfaceControl这个native方法, 把cpp层的SurfaceControl也一并复制过来。
WindowStateAnimator中创建了一个完整的SurfaceControl,然后也把自己复制给ViewRootImpl的SurfaceControl中(无论是Java层还是native层),至此SurfaceControl的创建过程结束了。
Surface的创建过程
接下来回到ViewRootImpl中,后面的代码执行如下:
1 | scss复制代码private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, |
正常情况下,mSurfaceControl是合法的,接下来无论是useBLAST是否为真,都可以创建Surface。
第一种方法
第一种情况调用copyFrom:
1 | java复制代码// Surface.java |
复制方法比较简单,就是从SurfaceControl获取一个Surface,然后更新一下自身。我们重点看一下nativeGetFromSurfaceControl这个native方法的执行逻辑是怎样的。
1 | arduino复制代码// frameworks/base/core/jni/android_view_Surface.cpp |
我们发现创建cpp层面的Surface之前,会先创建一个队列BLASTBufferQueue,而队列内部有生产者操作类(IGraphicBufferProducer)和消费者操作类BufferQueueConsumer,正应了我们说到的生产消费模型的说法。
创建BBQSurface时,我们发现IGraphicBufferProducer类型的producer作为构造参数被传入了,这也印证了我们说的,Surface是生产者渲染数据汇集地。
第二种方法
1 | markdown复制代码/******************************文件分割线******************************************/ |
从上面的截取的代码可以看到即使是先创建Java层的BlastBufferQueue,最终也还是殊途同归,通过native层的BlastBufferQueue来创建一个Surface。
默认情况下,代码应该是第二种方法创建Surface。
创建阶段出现大量的类,我们用一个示意图来总结一下他们之间的关系:
android View的绘制过程
分析完了Surface的创建过程之后,我们再次回到View的绘制过程,从View的显示过程中理解Surface的作用。
我们都知道View的绘制需要canvas,而canvas则是通过ViewRootImpl内部的Surface来获取。
1 | scss复制代码//ViewRootImpl.java |
绘制过程可以简单描述为三步:
- 通过Surface获得一个有效的Canvas
- view通过canvas来进行绘制
- Surface释放并发送canvas
我们可以按照这三部步骤来分析
获得有效的Canvas
我们可以先简单看一下Canvas类的情况
1 | ini复制代码 |
canvas由于涉及到底层的图像绘制,那么和surface一样,在cpp层也有一个对应的类实现底层功能,这个类的句柄就是mNativeCanvasWrapper。
1 | markdown复制代码 |
我们可以观察到surface获取和释放canvas的方法都调用到了native层面,而canvas通过构造函数创建对象时也会创建一个cpp层的对象并获得句柄。我们看看canvas的构造
1 | markdown复制代码//frameworks/base/libs/hwui/jni/android_graphics_Canvas.cpp |
最终在cpp层创建了SkiaCanvas。此时传入的SkBitmap引用是空的,没有任何有效信息。
我们接着往native层看lockCanvas的实现。
1 | cpp复制代码static jlong nativeLockCanvas(JNIEnv* env, jclass clazz, |
lockCanvas主要分为三步:1,申请并锁定一块内存,2,获取native层的canvas对象,3把buffer设置进canvas。
我们先来看第一步的细节:
1 | ini复制代码 |
获取一块图形绘制缓冲区,包含绘制所需的一些信息。
第二步,获取native层的SkiaCanvas
1 | markdown复制代码//frameworks/base/libs/hwui/apex/include/android/graphics/canvas.h |
通过从Java层传入的Canvas引用,获得native层创建好的SkiaCanvas。
然后是第三步
1 | arduino复制代码 |
把获取的buffer转换为一个SkBitmap,此时bitmap中有有效信息,然后把Bitmap设置进SkiaCanvas并替换原来的Bitmap,接下来canvas就在这个bitmap上进行绘制。
setBuffer既用于设置buffer,也用于解除buffer,后面释放提交canvas时还会使用
通过Canvas进行绘制
绘制过程主要在发生View的体系中,根据View的需要自行进行绘制操作,Java层的部分应当是我们最熟悉的部分了。
当然所有的绘制API都通过JNI调用到了native层的SkiaCanvas中,然后绘制在SKBitmap上。这些都由skia图像绘制引擎提供具体实现逻辑。在此我们不进行深究了(精力有限)
释放并发送Canvas
假设绘制区域完成了绘制操作,那么接下来就是释放并提交canvas了。Java层通过nativeUnlockCanvasAndPost调用到native层,我们看其具体实现
1 | arduino复制代码//frameworks/base/core/jni/android_view_Surface.cpp |
里面的函数前面大多都讲过, canvas.setBuffer(nullptr,..),nullptr会转换为一个空的skBitmap,然后替换SkiaCanvas里面原有的SkBitmap。相当于让canvas与原来的buffer分离。
1 | ini复制代码// frameworks/native/libs/gui/Surface.cpp |
至此,View的绘制的详细过程我们也搞清楚了:ViewRootImp通过Surface从缓冲队列中获得一块可用于绘制的buffer,然后把buffer绑定在canvas中,Android View使用该canvas进行绘制,产生的渲染数据也最终保存在buffer中,绘制完毕,通过Surface清除canvas与buffer的绑定关系,并把buffer发送到缓冲队列中,完成了生产端渲染数据的生产过程。
小结
在渲染数据生产过程中,Surface的低位非常重要,我们文章开头提到的那些生产者(view/Camera/media player…)都离不开它,Surface提供了操作接口来完成生产端的数据输入过程。在消费端, 大部分情况下是通过SurfaceFlinger把渲染数据的用在屏幕显示来完成消费的。但是也有其他情况,比如也可以被视频编码器进行消费(后续关于Android音视频开发的文章可能会提到)。
总之Surface给图形数据生产者提供了了一个数据输入的标准接口,我们会在大量的系统模块中看到它的身影。
文章到这里显示原理差不多只讲到一半,生产消费模型只讲到了生产端,消费端还没有涉及,还有缓冲队列的具体操作,这个留在后续文章进行研究。
本文转载自: 掘金