版本比较

密钥

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。
目录
stylenone

来源:https://zhuanlan.zhihu.com/p/261169653

当我们点击“知乎”这个应用后,它是怎么在屏幕上显示出来的?

这个问题困扰我很久了,当我刚接触显示的时候,大约是十年前的事情了,当时我连Framebuffer都不了解。尽管与显示芯片接触的越来越久,但萦绕在我心头的疑惑也并没有减少,此时大部分时间都与硬件交互,对上层的理解仍是糊里糊涂。

我当时就挺希望有人能从头到尾的介绍一下整个显示流程,可惜网上并没有这样的教程,实际接触到的同事基本分管单个模块,从上而下也很难说清。感谢曾经在联芯的日子,那时候空闲时间多了不少。大约2017年左右,使我有时间整理了一份60页的文档,这份文档如今也留在了原公司,当然本系列文章会按照PPT的形式介绍大概流程

从事Android显示相关领域已经很多年了,如今越来越偏离这个领域,总心有不忍,不愿完全离开这个方向,所以一直也比较关注这个领域的发展。

背景知识

为了更好的了解整个框架,我们先了解一下背景知识;因为很多人并不太了解Graphics与Display的区别,主要原因还在于PC时代的影响

PC显卡与移动端GPU的区别

上面是ARM给出的视频流显示过程,主要介绍AFBC的带宽压缩技术。但我们给出这张图的主要目的是为了说明PC上的显卡与移动端GPU的区别。在PC上这整个流程都在显卡内完成。

PC的显卡 = 移动端GPU + Display Processor + Video Processor

PC上Linux系统显示流程在2012年就已全部采用drm框架了,如下图,从框架中我们能看出drm中renderX节点就是服务于GPU(应用有权限,内容端),而ControlDX是服务于DisplayProcessor(系统权限,控制端)。

Android在2018年左右也采用了这个框架,但移动端这个区分在硬件上早就是存在的。后面我们会常用到内容端与控制端这2个概念。

说句题外话,游戏对于Graphics(GPU)要求比较高,而除了Adreno、Mali少数的几个能提供GPU平台的公司(它们也同时提供DP芯片),更多的手机公司基本都是从事Display的工作。

Android系统特点

我们知道Android是采用的Client-Server架构,而不是微内核架构。如上图,应用与硬件交互都需要经过binder委托给系统服务。但是有个例外——

GPU是用户进程唯一可以直接操作的硬件

这主要是为了性能考虑,毕竟绘图的99%的工作都由GPU完成。

在了解了背景知识后,我们从上至下了解整个框架。如“Android上的FreeSync——显示延时”那一篇文章提到的,Android的显示是三段式的。

关于这个系列我们将分为上中下三篇——上篇只介绍绘制部分,也就是内容端;中篇是承上启下的SurfaceFlinger、Hwcomposer(合成), 下篇主要讲解硬件相关的DisplayProcessor以及显示接口如LCD(显示)。

显示流程

下图是显示一帧所经过的流程,可以看出来经过5个主要模块——

  • Application--Renderer

  • Frameworks/SurfaceFlinger

  • Hwcomposer

  • Display Driver

  • Display Device

为了避免概念分歧,本文主要讲解绘制,也就是内容端,当然Frameworks/SurfaceFlinger属于承上启下,不可避免也会涉及到。

一、Application/Activity/View

关于这个概念网上已经有了很多,我们只简单介绍一下。

  • Application包括4大组件:Activity、Service、Broadcast、ContentProvider

  • Activity委托View负责显示:

    • TextView、EditView、Button、Custom View

    • Custom main view:setContentView(R.layout.main_activity)

View的3个重要方法

  1. Measure:系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。

显示大小,应用可调,由服务端WMS最终计算确定(控制端)

  1. Layout:根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。

显示位置,应用可调,由服务端WMS最终计算确定(控制端)

  1. Draw:确定好位置后,就将这些控件绘制到屏幕上。

显示内容,应用决定,此时才会用到GPU(内容端)

所以,我们也可以理解,为什么onCreate里面的setContentView的时候,实际是没有分配Buffer的,真正分配Buffer的时候是始于Draw的时候。

最重要的View——DectorView

Android窗口主要分为两种:

  1. 应用窗口——PhoneWindow

一个activity有一个主窗口,弹出的对话框也有一个窗口,Menu菜单也是一个窗口。在同一个activity中,主窗口、对话框、Menu窗口之间通过该activity关联起来。

和应用相关的窗口表示类是PhoneWindow,其继承于Window,针对手机屏幕做了一些优化工作,里面核心的是mDecorView这个变量,mDecorView是一个顶层的View,窗口的添加就是通过WindowManager.addView()把该mDecorView添加到WindowManager中。

  1. 公共界面的窗口

如最近运行对话框、关机对话框、状态栏下拉栏、锁屏界面等。这些窗口都是系统级别的窗口,不从属于任何应用,和activity没有任何关系。这种窗口没有任何窗口类来封装,也是直接调用WindowManager.addView()来把一个view添加到WindowManager中。

至此流程如下(具体细节不再展开,图片来自之前一个同事,借用一下)

二、Framework的控制端与内容端

控制端

WindowManagerService (服务端,与绘制无关

  • 窗口的状态、属性,如大小,位置;(WindowState,与上面的DectorView一一对应)

  • View增加、删除、更新

  • 窗口顺序

  • Input Event 消息收集和处理等(与绘制无关,可不用关心)

SurfaceFlinger(服务端,与绘制无关

  • Layer的大小、位置(Layer与上面的WindowState一一对应)

  • Layer的增加、删除、更新;

  • Layer的zorder顺序

内容端——绘制

framework/base: Canvas

  • SoftwareCanvas (skia/CPU)

  • HardwareCanvas (hwui/GPU)

framework/base: Surface

  • 区别于WMS的Surface概念,与Canvas一一对应,内容生产者

framework/native:

  • Surface: 负责分配buffer与填充(由上面的Surface传下来)

  • SurfaceFlinger

    • Layer数据已填充好,与上面提到的Surface同样是一一对应

    • 可见Layer这个概念即是控制端又是内容端

    • 当然,SF更重要的是合成,后文会继续讲解

至此流程如下(具体细节不再展开)

三、概念澄清

在这里各种Surface,Window,View、Layer实在是太乱了,下面就区分一下这些概念。

Window -> DecorView-> ViewRootImpl -> WindowState -> Surface -> Layer 是一一对应的。
一般的Activity包括的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的,即有对应的Window和WindowState。
一个应用程序窗口分别位于应用程序进程和WMS服务中的两个Surface对象有什么区别呢?
虽然它们都是用来操作位于SurfaceFlinger服务中的同一个Layer对象的,不过,它们的操作方式却不一样。具体来说,就是位于应用程序进程这一侧的Surface对象负责绘制应用程序窗口的UI,即往应用程序窗口的图形缓冲区填充UI数据,而位于WMS服务这一侧的Surface对象负责设置应用程序窗口的属性,例如位置、大小等属性。
这两种不同的操作方式分别是通过C++层的Surface对象和SurfaceControl对象来完成的,因此,位于应用程序进程和WMS服务中的两个Surface对象的用法是有区别的。之所以会有这样的区别,是因为绘制应用程序窗口是独立的,由应用程序进程来完即可,而设置应用程序窗口的属性却需要全局考虑,即需要由WMS服务来统筹安排

四、软件绘制

软件绘制就是使用CPU去绘制,Skia之前就是一个CPU实现的2D库,当然现在也用GPU去实现了。

硬件加速就是使用GPU去绘制,这看起来似乎天经地义,其实Android并不是一开始就支持的,而是在Android 3.0之后才开启了默认硬件加速,如果你的应用target API < 14,还是默认使用软件绘制。

但在Android 3.0之前,GPU也是存在的,其意义何在?

1)虽然UI是默认软件绘制的,但是并不是说所有的应用都不是硬件绘制的,即使在Android3.0之前,BootAnimation也是一个典型的GPU应用;Camera的后处理特效;还有大部分游戏都是硬件加速的。
2)SurfaceFlinger是用来合成Layer的,而且是用GPU合成Layer的,这也需要GPU的功能,只需2D模块就够了。其实在早期的GPU上,比如imagination,vivant都是有这个2D模块的,尽管现在都已经取消了,甚至这2个GPU已经消失在大众的视野了。
3)在Android3.0之前,你如果没有GPU硬件,那么SF是否就无法启动呢?遇到BootAnimation是否就挂掉了呢?这到也不是,当时Android给了一个模拟GPU的libGLES_android库,但是只支持固定管线Opengles 1.1,主要用于调试用。合成Layer是没有什么问题,BootAnimation也没啥问题。有问题的是复杂的游戏等,所以GPU还是得标配。

软件绘制的典型场景

  1. 鼠标---直接调用Surface->lock

  2. Mali对于小layer的旋转

  3. Target API < 14等应用强制使用软件绘制的情形

软件绘制的Buffer管理

软件绘制流程比较简单

在Android应用程序进程这一侧,每一个窗口都关联有一个Surface。每当窗口需要绘制UI时,就会调用其关联的Surface的成员函数lock获得一个Canvas,其本质上是向SurfaceFlinger服务dequeue一个Graphic Buffer。
Canvas封装了由Skia提供的2D UI绘制接口,并且都是在前面获得的Graphic Buffer上面进行绘制的,这个Canvas的底层是一个bitmap,也就是说,绘制都发生在这个Bitmap上。绘制完成之后,Android应用程序进程再调用前面获得的Canvas的成员函数unlockAndPost请求显示在屏幕中,其本质上是向SurfaceFlinger服务queue一个Graphic Buffer,以便SurfaceFlinger服务可以对Graphic Buffer的内容进行合成,以及显示到屏幕上去。

如何确定是否软件绘制

CPU负载会相对高;
Systrace无RenderThreader线程;
Systrace中无eglSwapBuffer等opengl api (opengl在Android尚无无软件实现)
Dumpsys gfxinfo pid 得不到相应的绘制信息

如何强制使用软件或硬件加速

其实我们真正有可能用到的是关闭或开启硬件加速,比如在调试的过程中看下是否是GPU的兼容性引起的问题。

  1. 针对应用

在Android中,可以在四个不同层次上打开或关闭硬件加速:
Application :< application android:hardwareAccelerated=“false”>
Activity:< activity android:hardwareAccelerated=" false ">
Window:getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); //此为打开,
View:view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
在这四个层次中,应用和Activity是可以选择的,Window只能打开,View只能关闭
设置targetSDKVersion
在apk的AndroidManifest中,如果指定了minSDKVersion&targetSDKVersion=7,会使得应用无法使用硬件加速进行绘图。如果targetSDKVersion<14或者不设置都是默认软件绘图。

  1. 针对整个系统

关闭硬件加速:
BoardConfigVendor.mk file :Set the USE_OPENGL_RENDERER attribute to False.
使能硬件加速:
Settings→Developer options→Force GPU rendering: This forces the apps to use the GPU for 2D drawing.

五、硬件绘制

硬件绘制相对比较复杂,在这里只简单带过。

硬件buffer管理与软件绘制基本是一致的。

硬件加速渲染和软件渲染一样,在开始渲染之前,都是要先向SurfaceFlinger服务dequeue一个Graphic Buffer。不过对硬件加速渲染来说,这个Graphic Buffer会被封装成一个ANativeWindow,并且传递给Open GL进行硬件加速渲染环境初始化。
在Android系统中,ANativeWindow和Surface可以是认为等价的,只不过是ANativeWindow常用于Native层中,而Surface常用于Java层中。 Open GL获得了一个ANativeWindow,并且进行了硬件加速渲染环境初始化工作之后,Android应用程序就可以调用Open GL提供的API进行UI绘制了,绘制出来内容就保存在前面获得的Graphic Buffer中。
当绘制完毕,Android应用程序再调用libegl库(一般由第三方提供)的eglSwapBuffer接口请求将绘制好的UI显示到屏幕中,其本质上与软件渲染过程是一样的。

Android自带的libGLES_android如下,但实际使用的GPU驱动mali或者adreno稍有不同,mali的是先dequeueBuffer再queueBuffer。adreno完全闭源。

代码块
languagejava
EGLBoolean egl_window_surface_v2_t::swapBuffers() {
…………
nativeWindow->queueBuffer(nativeWindow, buffer, -1);//此处也有等待fence
buffer = 0;

// dequeue a new buffer
int fenceFd = -1;
if (nativeWindow->dequeueBuffer(nativeWindow, &buffer, &fenceFd) == NO_ERROR) {
sp<Fence> fence(new Fence(fenceFd));
if (fence->wait(Fence::TIMEOUT_NEVER)) {
nativeWindow->cancelBuffer(nativeWindow, buffer, fenceFd);
return setError(EGL_BAD_ALLOC, EGL_FALSE);
}
…………
}

整个流程如下(首帧):

MainThread和RenderThread的分离
在Android 5.0之前,Android应用程序的Main Thread不仅负责用户输入,同时也是一个OpenGL线程,也负责渲染UI。通过引进Render Thread,我们就可以将UI渲染工作从Main Thread释放出来,交由Render Thread来处理,从而也使得Main Thread可以更专注高效地处理用户输入,这样使得在提高UI绘制效率的同时,也使得UI具有更高的响应性。

对于上层应用而言,UI thread仍然是Main Thread,它并不清楚Render Thread的存在,而对于SurfaceView,UI thread不是Main Thread,而是重新启用了一个新的线程

在Android应用程序窗口中,每一个View都抽象为一个Render Node,而且如果一个View设置有Background,这个Background也被抽象为一个Render Node。这是由于在OpenGLRenderer库中,并没有View的概念,所有的一切可绘制的元素都抽象为一个Render Node。
每一个Render Node都关联有一个Display List Renderer。这里又涉及到另外一个概念——Display List。Display List是一个绘制命令缓冲区。也就是说,当View的成员函数onDraw被调用时,我们调用通过参数传递进来的Canvas的drawXXX成员函数绘制图形时,我们实际上只是将对应的绘制命令以及参数保存在一个Display List中。接下来再通过Display List Renderer执行这个Display List的命令,这个过程称为Display List Replay。
引进Display List的概念有什么好处呢?主要是两个好处。
第一个好处是在下一帧绘制中,如果一个View的内容不需要更新,那么就不用重建它的Display List,也就是不需要调用它的onDraw成员函数。
第二个好处是在下一帧中,如果一个View仅仅是一些简单的属性发生变化,例如位置和Alpha值发生变化,那么也无需要重建它的Display List,只需要在上一次建立的Display List中修改一下对应的属性就可以了,这也意味着不需要调用它的onDraw成员函数。这两个好处使用在绘制应用程序窗口的一帧时,省去很多应用程序代码的执行,也就是大大地节省了CPU的执行时间。

虽然从Android3.0开始到Android8.0,每一次都会对硬件绘制升级,也显示了硬件绘制的重要地位,但基本原理没有变,还是利用了DisplayList。

简单的TextView的DisplayList命令是这样的

代码块
languagejava
Save 3
DrawPatch
Save 3
ClipRect 20.00, 4.00, 99.00, 44.00, 1
Translate 20.00, 12.00
DrawText 9, 18, 9, 0.00, 19.00, 0x17e898
Restore
RestoreToCount 0

DisplayList最后的GPU命令如下,如果是游戏通常直接调用下面类似的命令,这样效率更高。

代码块
languagejava
glUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
glBindBuffer(target = GL_ARRAY_BUFFER, buffer = 0)
glGenBuffers(n = 1, buffers = [3])
glBindBuffer(target = GL_ELEMENT_ARRAY_BUFFER, buffer = 3)
glBufferData(target = GL_ELEMENT_ARRAY_BUFFER, size = 24576, data = [ 24576 bytes ], usage = GL_STATIC_DRAW)
glVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0xbefdcf18)
glVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0xbefdcf20)
glVertexAttribPointerData(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 48)
glVertexAttribPointerData(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 48)
glDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 72, type = GL_UNSIGNED_SHORT, indices = 0x0)
glBindBuffer(target = GL_ARRAY_BUFFER, buffer = 2)
glBufferSubData(target = GL_ARRAY_BUFFER, offset = 768, size = 576, data = [ 576 bytes ])
glDisable(cap = GL_BLEND)
glUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 33.0, 0.0, 1.0])
glVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x300)
glVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x308)
glDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 54, type = GL_UNSIGNED_SHORT, indices = 0x0)
eglSwapBuffers()

总结

GPU是应用绘制的主力, 我们称之为内容端,也就是真正的Buffer;而控制端则由DisplayProcessor完成;无论是Frameworks中的WMS还是SF都是在解耦这两个功能

但是你可能会觉得奇怪,SF中用GPU的地方也很多啊,这属于内容端还是控制端呢?SF中GPU是绘制还是合成?

显示系统中还有另外一个让人混淆的地方就是谁来绘制,谁来合成,谁来显示的问题。这与上面的内容端与控制端又属于2个维度,其中我们可以认为绘制属于内容端,而显示属于控制端,而合成则介于两者之间。详见下一篇文章。

硬件绘制流程更详细版本:沧浪之水:Android GPU硬件加速渲染流程(上)