UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识、如何优化 UI 渲染两部分内容。
UI 优化系列专题
- UI 渲染背景知识
《View 绘制流程之 setContentView() 到底做了什么?》
《View 绘制流程之 DecorView 添加至窗口的过程》
《深入 Activity 三部曲(3)View 绘制流程》
《Android 之 LayoutInflater 全面解析》
《关于渲染,你需要了解什么?》
《Android 之 Choreographer 详细分析》
- 如何优化 UI 渲染
《Android 之如何优化 UI 渲染(上)》
《Android 之如何优化 UI 渲染(下)》
优化是无止境的,Google 在 2012 年的 I/O 大会上宣布了 Project Butter 黄油计划,并且在 Android 4.1 中正式开启了这个机制。
Project Butter 主要包含两个组成部分:一个是 VSYNC,另一个是 Triple Buffering。
也正是从这时候开始,作为严重影响 Android 口碑的 UI 流畅性问题便得到了有效解决。今天我们就来聊一聊这两个组成部分的工作原理,不过在理解它们之前,需要先看下所涉及的另外两个概念:
1. 刷新率
表示屏幕在一秒内刷新画面的次数, 刷新率取决于硬件的固定参数,单位 HZ(Hz)。例如常见的 60 Hz,即每秒钟刷新 60 次。
逐行扫描
显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描显示,不过这一过程快到人眼无法察觉到变化,以 60 Hz 刷新率的屏幕为例,即 1000 / 60 ≈ 16ms。
2. 帧速率
表示 GPU 在一秒内绘制操作的帧数。例如电影采用 24 fps、Android 系统采用 60 fps,即一秒钟绘制 30 / 60 帧画面。更多内容参考《Why 60 fps》。
屏幕撕裂
现在,刷新频率和帧速率需要一起合作,才能使图形内容呈现在屏幕上,GPU 会获取图形数据进行绘制, 然后硬件负责把图像内容呈现到屏幕上,这一过程在应用程序的生命周期内一遍又一遍的发生。
不幸的是,刷新频率和帧率并不总是能够保持相对同步,如果你的帧速率实际比刷新率快,例如帧速率是 120 fps,显示器的刷新频率为 60 Hz。此时将会发生一些视觉上的问题。
由于显示器提取画面是从左到右,从上到下逐行扫描提取图像显示,这一过程需要 16ms(1000 / 60),当 GPU 利用一块内存区域写入帧数据时,从顶部开始新一帧覆盖前一帧,并立刻输出一行内容。当屏幕刷新时,它并不知道图像缓冲区的状态,因此它从 GPU 抓取的帧并不是完整的数据。也就是它有一半的前一帧和一半的当前帧,这种情况被称之为屏幕撕裂。
- 理想情况下,希望显示器在完成一帧绘制之后再从 GPU 获取到下一帧图像数据,但是由于帧率和刷新频率不一致导致显示器在绘图过程中,GPU 已经开始加载下一帧图像数据。屏幕撕裂是由于帧率和刷新频率不一致的情况导致。
解决该问题的方案是双缓冲,即 GPU 和显示器都有各自的工作缓冲区。GPU 始终将完成的一帧绘制数据写入到 Back Buffer,而显示器使用 Frame Buffer。当屏幕刷新时,Frame Buffer 并不会发生变化。Back Buffer 根据屏幕的刷新将数据 copy 到 Frame Buffer,这便是 VSYNC 的用武之地。
在 Android 4.1 之前,Android 使用双缓冲机制。怎么理解呢?一般来说,同一个 View Hierarchy 内的 View 都会共用一个 Window,也就是共用一个 Surface。
而每个 Surface 都会有一个 BufferQueue 缓存队列,但是这个队列会由 SurfaceFlinger 管理,通过匿名共享内存与 App 应用层交互。
整个流程如下:
每个 Surface 对应的 BufferQueue 内部都有两个 Graphic Buffer,一个用于绘制一个用于显示。系统会把内容先到离屏缓冲区(OffScreen Buffer),在需要显示时,才把离屏缓冲区的内容通过 Swap Buffer 复制到 Front Graphic Buffer 中。
这样 SurfaceFlinger 就拿到了某个 Surface 最终要显示的内容,但是同一时间我们可能会有多个 Surface。这里面可能是不同应用的 Surface,也可能是同一个应用里面类似 SurfaceView 和 TextureView,它们都会有自己单独的 Surface。
这个时候 SurfaceFlinger 把所有 Surface 要显示的内容统一交给 Hardware Composer,它会根据位置、Z-Order 顺序等信息合成为最终屏幕需要显示的内容,而这个内容会交给系统的帧缓冲区 Frame Buffer 来显示(Frame Buffer 是非常底层的,可以理解为屏幕显示的抽象)。
Android 一直使用 VSYNC 来阻止屏幕撕裂,对于 Android 4.0,CPU 可能会因为在忙其他的事情,导致没来得及处理 UI 绘制。所以从 4.1 开始 VSYNC 则更进一步,VSYNC 脉冲现在用于开始下一帧的所有处理。
VSYNC 类似于时钟中断,每收到 VSYNC 中断,CPU 会立即准备 Buffer 数据,由于大部分显示设备刷新频率都是 60Hz(一秒刷新 60 次),也就是说一帧数据的准备工作都要在 16ms(1000/60≈16)内完成。
这样应用总是在 VSYNC 边界上开始绘制,而 SurfaceFlinger 总是在 VSYNC 边界上进行合成。这样便可以消除卡顿,并提升图形的视觉表现。
Triple Buffering
如果理解了双缓冲机制的原理,那就非常容易理解什么是三缓冲区了。如果只有两个 Graphic Buffer 缓存区 A 和 B,如果 CPU/GPU 绘制过程较长,超过了一个 VSYNC 信号周期,因为缓冲区 B 中的数据还没有准备完成,所以只能继续展示 A 缓冲区的内容,这样缓冲区 A 和 B 都分别被显示设备和 GPU 占用,CPU 则无法准备下一帧的数据。
如果再提供一个缓冲区,CPU、GPU 和显示设备都能使用各自的缓冲区工作,互不影响。简单来说,三缓冲机制就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度利用空闲时间,带来的坏处是多使用了一个 Graphic Buffer 所占用的内存。
从图中可以看出,缓冲区 B 花费的时间太长,并且正在使用 A 来显示当前帧。不过这次,系统不是在重复的缓冲区中浪费时间,而是创建一个 C 缓冲区,并开始处理下一帧。三重缓冲降低了 jank 的进一步加剧。
三缓冲并不总是存在,通常情况下仅运行一个双缓冲区,但是当发生延迟等情况时,第三个缓冲区就会出现,以便降低延迟的加剧。对于 VSYNC 信号和 Triple Buffering 更详细的介绍,可以参考《Project Butter - How it works and What it added?》。
Android 渲染框架非常庞大,而且演进的非常快。感兴趣的朋友可以进一步阅读下面的参考资料。
- Understanding VSYNC
- 2018 Google I/O:Drawn out: how Android renders
- Google I/O 2012:For Butter or Worse
- 官方文档:Android 图形架构
- Android Project Butter 分析
扩展阅读
- 关于 UI 渲染,你需要了解什么?
- Android 之如何优化 UI 渲染(上)
- 深入 Activity 三部曲(3)之 View 绘制流程
- Android 之你真的了解 View.post() 原理吗?