UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识、如何优化 UI 渲染两部分内容。
UI 优化系列专题
- UI 渲染背景知识
《View 绘制流程之 setContentView() 到底做了什么?》
《View 绘制流程之 DecorView 添加至窗口的过程》
《深入 Activity 三部曲(3)View 绘制流程》
《Android 之 LayoutInflater 全面解析》
《关于渲染,你需要了解什么?》
《Android 之 Choreographer 详细分析》
- 如何优化 UI 渲染
《Android 之如何优化 UI 渲染(上)》
《Android 之如何优化 UI 渲染(下)》
现在我们已经很少能够听到关于 Android UI 卡顿的话题了,这得益于 Google 长期以来对 Android 渲染性能的重视,基本每次 Google I/O 都会花很多篇幅讲这一块。随着时间的推移,Android 系统一直在不断进化、壮大,并且日趋完善。
其中,Google 在 2012 年的 I/O 大会上宣布了 Project Butter 黄油计划,那个曾经严重影响 Android 口碑的 UI 流程性问题,首先在这得到有效的控制,并且在 Android 4.1 中正式开启了这个机制。
Project Butter
Project Butter 对 Android Display 系统进行了重构,引入了三个核心元素,即 VSYNC、Triple Buffer 和 Choreographer。
其中 VSYNC 是理解 Project Butter 的核心。接下来,我们就围绕 VSYNC 开始介绍 Project Butter 对 Android Display 系统做了哪些优化。
VSYNC
VSYNC 最初是由 GPU 厂商开发的一种,用于防止屏幕撕裂的技术方案,全称 Vertical Synchronization,该方案很早就已经被广泛应用于 PC 上。我们可以把它理解为一种时钟中断。
1. 前世
VSYNC 是一种图形技术,它可以同步 GPU 的帧速率和显示器的刷新频率,所以在理解 VSYNC 产生的原因及其作用之前,我们有必要先来了解下这两个概念。
- 刷新频率(Refresh Rate)
表示屏幕在一秒内刷新画面的次数, 刷新频率取决于硬件的固定参数,单位 Hz(赫兹)。例如常见的 60 Hz、144 Hz,即每秒钟刷新 60 次或 144 次。
逐行扫描
显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描显示,不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例,即 1000 / 60 ≈ 16ms。
- 帧速率 (Frame Rate)
表示 GPU 在一秒内绘制操作的帧数,单位 fps。例如在电影界采用 24 帧的速度足够使画面运行的非常流畅。而 Android 系统则采用更加流程的 60 fps,即每秒钟绘制 60 帧画面。更多内容参考《Why 60 fps》。
屏幕撕裂
现在,刷新频率和帧率需要一起合作,才能使图形内容呈现在屏幕上,GPU 会获取图形数据进行绘制, 然后硬件负责把图像内容呈现到屏幕上,这一过程在应用程序的生命周期内一遍又一遍的发生。
如上图,CPU / GPU 生成图像的 Buffer 数据,屏幕从 Buffer 中读取数据刷新后显示。理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。不幸的是,刷新频率和帧率并不总是能够保持相对同步,如果帧速率实际比刷新率快,例如帧速率是 120 fps,显示器的刷新频率为 60 Hz。此时将会发生一些视觉上的问题。
当 GPU 利用一块内存区域写入一帧数据时,从顶部开始新一帧覆盖前一帧,并立刻输出一行内容。当屏幕刷新时,此时它并不知道图像缓冲区的状态,因此从缓冲区抓取的帧并不是完整的一帧画面(绘制和屏幕读取使用同一个缓冲区)。此时屏幕显示的图像会出现上半部分和下半部分明显偏差的现象,这种情况被称之为 “tearing”(屏幕撕裂)。
- 发生 “tearing” 现象的根源是由于帧速率与刷新频率不一致导致。
Double Buffer(双重缓存)
那如何防止 “tearing” 现象的发生呢?由于图像绘制和读取使用的是同一个缓冲区,所以屏幕刷新时可能读取到的是不完整的一帧画面。解决方案是采用 Double Buffer。
Double Buffer(双缓冲)背后的思想是让绘制和显示器拥有各自的图像缓冲区。GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,Back Buffer 根据屏幕的刷新将图形数据 copy 到 Frame Buffer,这便是 VSYNC 的用武之地。
- 注意,VSYNC 信号负责调度从 Back Buffer 到 Frame Buffer 的交换操作,这里并不是真正的数据 copy,实际是交换各自的内存地址,可以认为该操作是瞬间完成。
在 Android 4.1 之前,Android 便使用的双缓冲机制。怎么理解呢?一般来说,在同一个 View Hierarchy 内的不同 View 共用一个 Window,也就是共用同一个 Surface。
每个 Surface 都会有一个 BufferQueue 缓存队列,但是这个队列会由 SurfaceFlinger 管理,通过匿名共享内存机制与 App 应用层交互。
整个流程如下:
每个 Surface 对应的 BufferQueue 内部都有两个 Graphic Buffer,一个用于绘制一个用于显示。系统会把内容先绘制到离屏缓冲区(OffScreen Buffer),在需要显示时,才把离屏缓冲区的内容通过 Swap Buffer 复制到 Front Graphic Buffer 中。
这样 SurfaceFlinge 就拿到了某个 Surface 最终要显示的内容,但是同一时间我们可能会有多个 Surface。这里面可能是不同应用的 Surface,也可能是同一个应用里面类似 SurfaceView 和 TextureView,它们都会有自己独立的 Surface。
这个时候 SurfaceFlinger 把所有 Surface 要显示的内容统一交给 Hardware Composer,它会根据位置、Z-Order 顺序等信息合成为最终屏幕需要显示的内容,而这个内容会交给系统的帧缓冲区 Frame Buffer 来显示(Frame Buffer 是非常底层的,可以理解为屏幕显示的抽象)。
Android 一直使用 VSYNC 来防止屏幕画面发生撕裂现象
2. 今生
但是 UI 绘制任务可能会因为 CPU 在忙别的事情,导致没来得及处理。所以从 Android 4.1 开始, VSYNC 则更进一步,现在 VSYNC 脉冲信号开始用于下一帧的所有处理。
Project Butter 首先对 Android Display 系统的 SurfaceFlinger 进行了改造,目标是提供 VSYNC 中断。每收到 VSYNC 中断后,CPU 会立即准备 Buffer 数据,由于大部分显示设备刷新频率都是 60 Hz(一秒刷新 60 次),也就是说一帧数据的准备工作都要在 16ms 内完成。
这样应用总是在 VSYNC 边界上开始绘制,而 SurfaceFlinger 总是在 VSYNC 边界上进行合成。这样可以消除卡顿,并提升图形的视觉表现。
Triple Buffer(三重缓存)
如果理解了双缓冲机制的原理,那就非常容易理解什么是三缓冲区了。如果只有两个 Graphic Buffer 缓冲区 A 和 B,如果 CPU / GPU 绘制过程较长,超过一个 VSYNC 信号周期。
由上图可知:
在第二个 16 ms 时间段内,Display 本应该显示 B 帧,但却因为 GPU 还在处理 B 帧,导致 A 帧被重复显示。
同理,在第二个 16 ms 时间段内,CPU 无所事事,因为 A Buffer 被 Display 在使用。 B Buffer 被 GPU 在使用。注意,一旦过了 VSYNC 时间点,CPU 就不能被触发处理绘制工作了。
为什么 CPU 不能在第二个 16ms 处理绘制工作呢?原因是只有两个 Buffer,缓冲区 B 中的数据还没有准备完成,所以只能继续展示 A 缓冲区的内容,这样缓冲区 A 和 B 都分别被显示设备和 GPU 占用,CPU 则无法准备下一帧的数据。如果再提供一个缓冲区,CPU、GPU 和显示设备都能使用各自的缓冲区工作,互不影响。
简单来说,三重缓冲机制就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。
由上图可知:
在第二个 16ms 时间段,CPU 使用 C Buffer 完成绘图工作,虽然还是会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。
注意:是不是 Buffer 越多越好呢?这个是否定的,Buffer 正常还是两个,当出现 Jank 后三个足以。
Choreographer
Choreographer 也是 Project Butter 计划新增的机制,用于配合系统的 VSYNC 中断信号。它本质是一个 Java 类,如果直译的话为舞蹈指导,这是一个极富诗意的表达,看到这个词不得不赞叹设计者除了 Coding 之外的广泛视野。舞蹈是有节奏的,节奏使舞蹈的每个动作更加协调和连贯;视图刷新也是如此。
Choreographer 可以接收系统的 VSYNC 信号,统一管理应用的输入、动画和绘制等任务的执行时机。Android 的 UI 绘制任务将在它的统一指挥下,井然有序的完成。业界一般通过它来监控应用的帧率。
Choreographer 的构造方法:
private Choreographer(Looper looper, int vsyncSource) {
// 当前线程的Looper
mLooper = looper;
// 创建该Looper的Handler
mHandler = new FrameHandler(looper);
// 是否开启VSYNC,开启VSYNC后将通过FrameDisplayEventReceiver接受
// VSYNC脉冲
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
// 计算一帧的时间
// Android手机屏幕采用60Hz的刷新频率
// 这里是纳秒 ≈16000000ns 还是16ms
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
// 创建一个CallbackQueu的数组,默认为4
// CallbackQueue中存放要执行的输入、动画、遍历绘制等任务
// 也就是 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// b/68769804: For low FPS experiments.
setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}
Choreographer 是线程单例的,而且必须要和一个 Looper 绑定,因为其内部有一个 Handler 需要和当前绘制线程的 Looper 绑定。
DisplayEventReceiver 是一个 abstract class,在其构造方法内会通过 JNI 创建一个 IDisplayEventConnection 的 VSYNC 的监听者。
另外 DisplayEventReceiver 中包含两个非常重要的方法:一个用于需要绘制任务时,申请 VSYNC 信号的 scheduleVsync 方法,另一个用于接收 VSYNC 信号的 onVsync 方法。FrameDisplayEventReceiver 是 DisplayEventReceiver 的唯一实现类,并重写 onVsync 方法用于通知 Choreographer。
Choreographer 的主要功能是,当收到 VSYNC 信号时,去调用通过 postCallback 设置的回调方法。目前一共定义了四种类型的回调,它们分别是:
CALLBACK_INPUT:优先级最高,和输入事件处理有关;
CALLBACK_ANIMATION:优先级其次,和 Animation 的处理有关;
CALLBACK_TRAVERSAL:优先级最低,和 UI 等空间绘制有关;
CALLBACK_COMMIT:最后执行,和提交任务有关(在 API Level 23 添加)。
优先级的高低和处理顺序有关。当收到 VSYNC 信号时,Choreographer 将首先处理 INPUT 类型的回调,然后 ANIMATION 类型,最后才是 TRAVERSAL 类型。
另外,Android 在 4.1 还对 Handler 机制进行了略微改造,使之支持 Asynchronous Message(异步消息) 和 Synchronization Barrier(同步屏障)。一般情况下同步消息和异步消息的处理方式并没有什么区别,只有在设置了同步屏障时才会出现差异。同步屏障为 Handler 消息机制增加了一种简单的优先级关系,异步消息的优先级要高于同步消息。简单点说,设置了同步屏障之后,Handler 只会处理异步消息。
以 View 的绘制流程为例:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 同步屏障,阻塞所有的同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 将 UI 绘制任务发送到 Choreograhper
// 注意mTraversaRunnable是一个Runnable对象
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
// ...
}
}
scheduleTraversals 首先禁止了后续消息的处理能力,一旦设置了消息队列的 postSyncBarrier,所有非 Asynchronous 的消息将被停止派发。
UI 绘制任务设置了 CALLBACK 类型为 TRAVERSAL 类型的任务,即 mTraversalRunnable:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
//开始执行绘制遍历
doTraversal();
}
}
Choreographer 的 postCallback 方法将会申请一次 VSYNC 中断信号,通过 DisplayEventReceiver 的 scheduleVsync 方法。当 VSYNC 信号到达时,便会回调 Choreographer 的 doFrame 方法,内部会触发已经添加的回调任务:
// 回调 INPUT 任务
doCallbacks(Choreographer.CALLBACK_INPUT, mframeTimeNanos);
// 回调 ANIMATION
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 回调 View 绘制任务 TRAVERSAL
doCallbacks(Choreographer,CALLBACK_TRAVERSAL, frameTimeNanos);
// API Level 23 新增,COMMIT
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
此时 UI 绘制任务 doTraversal 方法被回调,即在 Android 4.1 之后, UI 绘制任务被放置到了 VSYNC 中断处理中了。Choreographer 确实做到了统一协调管理 UI 的绘制工作。有关 Choreographer 更详细的分析,可以参考《Android 之 Choreographer 详细分析》。
总结
在从根本解决 Android UI 不流畅的问题上,Project Butter 黄油计划率先迈出了最重要一步,Android 的渲染性能也确实有了很大改善。
不过优化是无止境的,Google 在后续版本中又引入了一些比较大的改变,例如 Android 5.0 的 RenderThread,Android 将所有的绘制任务都放到了该线程,这样即便主线程有耗时的操作也可以保证动画流畅性。
关于 UI 渲染所涉及的内容非常多,而且 Android 渲染框架演进的非常快,文章最后也会附上一些扩展资料,便于更好的学习理解。
文中如有不妥或有更好的分析结果,欢迎您的留言或指正。文章如果对你有帮助,请留个赞吧。
扩展阅读
- 关于 UI 渲染,你需要了解什么?
- Android 之 Choreographer 详细分析
- Android 之如何优化 UI 渲染(上)
- 深入 Activity 三部曲(3)之 View 绘制流程
- 三重缓冲:为什么我们喜欢它
其他系列专题
- Android 存储优化系列专题
- Android 之权限管理只防君子不防
- Android 之不要滥用 SharedPreferences(上)
- Android 存储选项之 SQLite 优化那些事儿