Android渲染系列(2)之如何渲染UI

1,872 阅读15分钟

本文主要介绍Android如何渲染的一些上屏细节

🔥面试官的小抄 面试进阶一网打尽,可能是东半球最好的面试资料

了解更多 加uestc_xsf(备注掘金进群) 技术交流、获取学习资料

背景知识

页面往往是一个activity承载,在讲这篇文章之前首先了解下actvity、windows、view这些基本知识 image.png PhoneWindow的构建是一个非常重要的过程,应用启动显示的内容装载到其内部的mDecor,Activity(PhoneWindow)要能接收控制也需要mWindowManager发挥作用。ViewRootImpl是应用进程运转的发动机,可以看到ViewRootImpl内部包含mView、mSurface、Choregrapher,mView代表整个控件树,mSurfacce代表画布,应用的UI渲染会直接放到mSurface中,Choregorapher使得应用请求vsync信号,接收信号后开始渲染流程。

Window

Android窗口主要分为两种:

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

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

但也有例外,比如悬浮窗口虽然与activity相关联,但并不是PhoneWindow,直接调用通过WindowManager.addView()添加。如果我们想给所有的应用都加一个比如最大、最小化、关闭的导航条,那只需更改mDecorView即可(Android N为支持多窗口将DecorView从PhoneWindow中分离成一个单独的文件)。

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

信息流

安卓系统上的TextView,Checkbox,RecyclerView这些View都是怎么被转成一个个用户能看到的像素渲染上屏的?希望你在看完这篇文章里后,对这个流程可以有个大概的理解

图形的渲染抽象来看就是Buffer的生产和Buffer的消费,生产和消费的对象,是 BufferQueue 里的 Buffer, image.png

需要通过以下2个核心机制来保证

  • Vsync

其中 VSYNC_APP——> 来驱动 Choreographer 开始收集buffer VSYNC_SF——>来驱动 SurfaceFlinger,开始消费buffer

更多精彩内容关注公众号 Android茶话会

image.png

image.png

  1. vsync_app 来驱动 Choreographer 接收和处理 App 的各种更新消息和回调,比如集中处理 Input(主要是 Input 事件的处理) 、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作) ,判断卡顿掉帧情况,记录 CallBack 耗时等
  2. 我们把视图变化的信息,同步到RenderThread渲染处理
  3. 图形系统会font和back交换buffer,把准备好的buffer做composer操作,最后上屏显示

一个简单的例子

点击RecyclerView其中一个Item变成绿色,这里面发生了什么 image.png 首先在Vsync过后,UI Thread处理这个input事件时,调用itemClicked方法,其中会调用setBackgroundColor方法,在 View.java 中该方法会调用invalidate()。

invalidate(重绘)

在Item2调用**invalidate()这个方法会一路向上传递,最后调用到ViewRootImpl.**invalidateChild()这个方法。这代表一次Traversal会在稍后进行。

ViewRootImpl是View中的最高层级,属于所有View的根(但ViewRootImpl不是View,只是实现了ViewParent接口),实现了View和WindowManager之间的通信协议,实现的具体细节在WindowManagerGlobal这个类当中 更多精彩内容关注公众号 Android茶话会

image.png

image.png

Traversal(遍历)

遍历进行渲染一帧的所有阶段。具体地说,它测量视图的大小,布局设置视图位置和大小,绘制视图,所有这些都被称为遍历。

android.view.ViewRootImpl#performTraversals

主要包含以下几个核心过程

Measure

确定每个View大小。测量遍历在 measure(int, int) 中实现,是 View 树的自上而下遍历。在递归过程中,每个 View 都会将维度规范下推到布局树。在测量遍历结束时,每个 View 均存储了其测量值。

Layout

决定每个View在哪里。第二次遍历发生在 layout(int, int, int, int) 中,也是自上而下遍历。在此次遍历中,每个父级负责使用测量遍历中计算的尺寸来定位其所有的子级。

以上2个阶段在这个简单点击例子中仅仅是没有发生变化 这里不再赘述

Draw

绘制从布局的根节点开始,需要测量并绘制布局树。系统通过遍历布局树并渲染与无效区域相交的每个 View 来处理绘制。反过来,每个 ViewGroup 负责请求绘制其每个子级(使用 draw() 方法),而每个 View 负责绘制其本身。由于布局树已经过系统预先遍历,这意味着父级将在它们的子级之前(即后面)进行绘制,而其同级会按照它们在布局树中出现的顺序进行绘制。

// ViewRootImpl.java
void performTraversal() {
    // 省略一系列判断和调用后...
    preformDraw();
}

image.png

image.png

此外硬件绘制还引入了一个 DisplayList 的概念,

DisplayList

每个 View 内部都有一个DisplayList,当某个 View 需要重绘时,将它标记为 Dirty。当需要重绘时,仅仅只需要重绘一个 View 的 DisplayList,而不是像软件绘制那样需要向上递归。这样可以大大减少绘图的操作数量,因而提高了渲染效率

Display List 是一个缓存绘制命令的 Buffer,Display List 的本质是一个缓冲区,它里面记录了即将要执行的绘制命令序列。 Display List 是视图的基本绘制元素,包含元素原始属性(位置、尺寸、角度、透明度等),对应 Canvas 的 drawXxx()方法。 类似于左边图中所示一样,onDraw会被DisplayList所表示。ViewTree中每一个View都会有对应的DisplayList来代表。

本例子中,点击Item2,触发onDraw之后,DisplayList开启了如下的操作 image.pngimage.png

Sync

在Java层(UI Thread),我们收集组合所有信息,然后把这些信息sync给Native层,由RenderThread用GPU渲染。 image.png Damage Area: 这个概念类似于Dirty Region,意思是需要被重新绘制的区域,在我们这个例子中就是item2的区域。 Upload non-HW Bitmaps: 把non-HW Bitmaps上传到GPU的RAM里,此时一帧刚刚开始,有比较充足的时间。相反的是,

Hardward Bitmap没有这步操作,Hardward Bitmap是一种新的位图配置,是在Android Oh中添加的 通常当你有一个位图时我们必须在Java端分配内存,然后在需要绘制的时候 我们必须在GPU上复制位图,这在文本时间上是昂贵的,它使我们的RAM数量增加了一倍。使用Oreo中可用的硬件位图,它已经存在GPU一侧了 如果你不打算修改这个位图,从内存角度来看确实一个非常有效的存储位图的方式

RenderThread

Android 5.0 (Lollipop)引入的,此线程只在Native层跟GPU交互,在Java层没有任何调用。渲染之前,我们生成DisplayList,然后我们把这些信息sync给GPU。它是串行执行的,但是RenderThread在这之中能够以原子操作(Atomic Operations)形式执行 例如波纹动画动画,矢量动画等。这些渲染转移到RenderThread来分担之后, UI Thread就可以在idle的时候做些别的事,比如RecyclerView的prefech机制就是设计在此时发生

DLOps (Display List Operations)

在GPU得到 Display List 后,DL 会转换成 DLOps。注意这个变成绿色的Fill操作,经过一系列**优化重排序(Optimization, Reordering and Batching)**后,它的位置被提到上面跟其他的Fill放在一起。优化(Optimization)包括将View setAlpha(),setHarewareLayer()这些操作的指令移到最前面执行,避免在GPU内进行昂贵的State变更操作。

重排序(Reordering)意思是当存在一系列的drawText(), drawRect()之类指令穿插存在时,相同的指令会被放在相邻的顺序执行。

而Batching表示一个drawText call就可以把整个屏幕上的需要drawText的地方全部做完。

image.png

image.png

Clip Reject

在 Clip Reject中,我们判断哪些操作是必须的,换句话说哪些操作是相关脏区(Damaged Area)的。例如下图中,只有右边的三个操作是跟把item2背景变绿这个效果相关的。所以我们只需要执行右边三个DLOps。

image.png

image.png

接下来我们需要拿到缓冲区,虽然这里写的是Get Buffer,但实际情况是buffer不是申请来的,有关GPU的操作一执行,SurfaceFlinger就会分配Buffer过来,让我们执行这些操作。 image.png 然后我们发出一系列GL指令来做画背景,画线,复制bitmap之类的实际操作。当这些操作全部结束时,我们会通知SurfaceFlinger去swap buffer。这时这一帧就完成了,它将被显示在屏幕上。 同时,在 SurfaceFlinger 和 HardwareCompositor 中会进行Surface合成。Status Bar,System Bar 和 Content在这里会合成在一起,然后展示在屏幕上。

image.png

image.png

复杂的例子

下面我们来看一个稍微复杂点的滑动的例子。滑动这个RecyclerView,里面所有的ViewHolder都会变更位置,继续滑动,会出现新的ViewHolder。我们可以将这个过程分成两个阶段:1. 仅滑动和 2. 新的Item出现。 image.png

阶段一:仅仅滑动

vsync又来了,我们要处理Input事件了。在ACTION_DOWN时我们仅仅记录初始值在哪里。在ACTION_MOVE,我们计算滑动距离Delta,触发invalidateViewProperty(),这个方法相比于invalidate()会快很多,因为我们只需要在 DisplayList 中改一下它的属性,在这例子中就是改一下相关DisplayList 的 DL Props 中的 Translation

image.png 和上面的例子类似,在item处调用invalidateViewProperty()会一路传递到root,再最后运行scheduleTraversals()进行Traversal image.png 在Draw阶段,performDraw()会被调用。但是在这个例子中,performDraw()需要做的工作简单很多,因为DisplayList本身没有改变,变的只是其中的属性。后面的流程就和上面那个例子一样了,Sync 到 RenderThread 然后进行类似的操作,这里不再赘述了

image.png

image.png

阶段二:新Item出现

到此阶段1哪些帧都渲染完了,用户还在滑动,现在新的Item, item6将要出现在屏幕上。在Input阶段,RecyclerView判断到了这点,于是addView(),添加item6。在这个方法里会调用到requestLayout()。 image.png

RequestLayout

重新Measure/Layout的过程

和Invalidation()类似,requestLayout()也会向上传递到根结点。但是它是从Parent开始的。传到DecorView后,scheduleTraversals()这个方法又被调用到

// ViewRootImpl
void performTraversals() {
 performMeasure();
    performLayout();
}

image.png

image.png

Measure是从上到下进行的,在这个过程中,所有View的大小都被计算出来了。 image.png Layout也是从上到下进行,这里在measure过程中保存的View的大小数据会被取出,然后对每个View进行放置。 image.png

但是,ReyclerView对这种情况有做优化,这些requestLayout的工作实际上不会走。RV对自己的parent和children都有足够的信息,它会直接改变children的位置而不是做这些繁复的操作。

Composition

image.png 在上面的两个例子中,都略过了composition这一步具体里面发生了什么。在这节我们会进行进一步解释,SurfaceFlinger和HardwareCompositor是怎么把不同的window合成,然后展示到屏幕上的。在我们开始前,我们需要先搞清楚下面这三个概念:BufferQueue, Producer, Consumer

BufferQueue

BufferQueue就是有若干个Buffer的Queue。Graphic Buffer存在在这里。一般会有1~3个Buffer,取决于setBufferQueue时的配置。 image.png

Producer

  • 就和其他所有生产消费者模型一样,它负责生产内容,具体一点,在这里它生产要被展示在屏幕上的数据。
  • 调用dequeBuffer()来从BufferQueue获得队首Buffer。这时它可以直接在Buffer写入Pixel数据,或调用OpenGL,或者使用Canvas。
  • 当内容生产完之后,调用queueBuffer()将这个Buffer还给BufferQueue,放到队尾。

Comsumer

  • 消费者要消费数据来展示到屏幕上。
  • 调用acquireBuffer()拿到BufferQueue中第一个可用的(一般在队尾)Buffer,读取里面的数据。
  • 内容消费完成后,调用releaseBuffer()给放回队首。

Create Window

当我们创建一个Window时,例如activity,dialog,popup window等。在Producer侧,WindowManager会在内部创建个Window对象(这里会创建ViewRootImp来关联view的操作,surface也是跟window一一绑定)。而在Consumer侧,SurfaceFlinger会管理各个Surface相对应的生成个Layer对象(layer的东西也太多了)。Layer是系统组件之一,它创建和管理BufferQueue。之后在App中会生成一个Surface对象 image.png

Surface

  • Surface 对应了一块屏幕缓冲区,每个 Window 对应一个 Surface,任何 View 都是画在 Surface 上的,传统的 View 共享一块屏幕缓冲区。
  • 所有的绘制必须在 UI 线程中进行。
  • 我们不能直接操作 Surface 实例,要通过 SurfaceHolder,在 SurfaceView 中可以通过 getHolder() 方法获取到 SurfaceHolder 实例。

SurfaceView

简单的说 SurfaceView 就是一个有 Surface 的 View,SurfaceView 控制这个 Surface 的格式和尺寸以及绘制位置。 SurfaceView的实现原理相当于在Window的Surface打个洞,漏出SurfaceView的Surface。他们两个的Surface是完全相互独立的存在。 image.png

Surface Texture

Consumer是OpenGL。SurfaceTexture 会创建 BufferQueue 和 Surface。 image.png

TextureView

  • 创建SurfaceTexture
  • RenderThread是Consumer,Producer可以自己选择。
  • 就像个功能更强大的ImageView一样,更新的更快速。

在Android O或N之前的版本上,TexutreView是比SurfaceView更推荐使用的,因为一方面可以享受更快速的渲染,另一方面可以避免SurfaceView的种种问题,比如两个Window造成的效率折损,渲染不同步导致画面割裂等。但是这些问题已经都被解决了,在18年谷歌I/O大会上更推荐在新版本的安卓上使用SurfaceView,而不是TextureView。

回到我们本节的主题Composition上。 我们在应用中创建很多window,每个都有自己的layer,SurfaceFlinger来收集这些layer,SurfaceFlinger其实不是直接跟Display显示器交互,而是跟Hardware Composer沟通(HWC),它是一个硬件抽象层,我们通常为了省电,避免GPU直接在屏幕上合成显示 image.png

Hardware Composer HAL

是一个硬件抽象层用于确定通过可用硬件来合成缓冲区的最有效方法

作为 HAL,其实现是特定于设备的,而且通常由显示硬件原始设备制造商 (OEM) 完成。

当您考虑使用叠加平面时,很容易发现这种方法的好处,它会在显示硬件(而不是 GPU)中合成多个缓冲区。 例如,假设有一部普通 Android 手机,其屏幕方向为纵向,状态栏在顶部,导航栏在底部,其他区域显示应用内容。每个层的内容都在单独的缓冲区中。您可以使用以下任一方法处理合成:

  • 将应用内容渲染到暂存缓冲区中,然后在其上渲染状态栏,再在其上渲染导航栏,最后将暂存缓冲区传送到显示硬件。
  • 将三个缓冲区全部传送到显示硬件,并指示它从不同的缓冲区读取屏幕不同部分的数据。

后一种方法可以显著提高效率。 显示处理器性能差异很大。叠加层的数量(无论层是否可以旋转或混合)以及对定位和重叠的限制很难通过 API 表达。为了适应这些选项,HWC 会执行以下计算:

  1. SurfaceFlinger 向 HWC 提供一个完整的Layer列表,并询问“您希望如何处理这些层?”
  2. HWC 的响应方式是将每个层标记为FrameBuffer或OVERLAY。
  3. SurfaceFlinger 会处理(计算的当前显示设备的脏区域DirtyRegion等工作)所有OVERLAY,将输出buffer传送到 HWC,并让 HWC 处理其余部分。

举个例子:SurfaceFlinger告诉HWC现在有3个Layer你想怎么办。HWC一看,第一个简单,OVERLAY就可以,第二个第三个我处理不了,需要GPU先处理下才行,就标记成FrameBuffer。 image.png 标记成FrameBuffer的这两层会被先行处理,处理这两层时,需要再添加一层来放output,被绿箭头指着的这层就是output,叫做Scratch Layer(叫什么名不重要,后面不会出现,只是为了避免混淆) image.png image.png 现在,剩下这两层Layer会被SurfaceFlinger用set()传到HWC,之后被渲染到屏幕上。感兴趣的大家可以用这个命令打印出SurfaceFlinger的大量信息。

adb shell dumpsys SurfaceFlinger

到这里,整个渲染的流程就已经串联完了。我们应用的 UI 是如何变成屏幕上的像素的,相信大家看到这里已经有了一个基本的概念。了解这些的工作原理可以帮助我们弄清楚如何为App获得最佳性能。

参考