一、概述
在 Android 开发过程中,我们经常谈到布局优化、卡顿优化时,通常都知道要减少布局层级、避免主线程做耗时操作等等,这样可以减少丢帧,如果丢帧比较严重的话就会发生明显的卡顿现象。这篇文章我们来详细分析 Android 屏幕刷新机制,便于优化我们的项目。
二、基础概念
在一个典型的显示系统中,一般包括 CPU、GPU、显示器三个部分, CPU 负责计算帧数据,把计算好的数据交给 GPU ,GPU 会对图形数据进行渲染,渲染好后放到 buffer (图像缓冲区)里缓存起来,然后显示器负责把 buffer 里的数据呈现到屏幕上。
1. 屏幕刷新频率
一秒内屏幕刷新的次数(一秒内显示了多少帧的图像),单位 Hz(赫兹),如常见的 60 Hz。刷新频率取决于硬件的固定参数(不会变的)。
2. 逐行扫描
显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例,这一过程即 1000 / 60 ≈ 16ms。
3. 帧率
表示 GPU 在一秒内绘制操作的帧数,单位 fps。例如在电影界采用 24 帧的速度足够使画面运行的非常流畅。而 Android 系统则采用更加流程的 60 fps,即每秒钟 GPU 最多绘制 60 帧画面。帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,屏幕刷新的还是 buffer 中的数据,即 GPU 最后操作的帧数据。
4. 双缓冲机制
如果使用单缓冲很容易造成画面撕裂,因为屏幕刷新频是固定的,比如每16.6ms 从 buffer 取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。但是 CPU/GPU 写数据是不可控的,所以会出现 buffer 里有些数据根本没显示出来就被重写了,即 buffer 里的数据可能是来自不同的帧的数据, 当屏幕刷新时,此时它并不知道 buffer 的状态,因此从 buffer 抓取的帧并不是完整的一帧画面,即出现画面撕裂。
因此使用双缓冲机制就可以完美解决画面撕裂的问题,双缓冲让绘制和显示器拥有各自的 buffer,GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。
5. VSync
如是 Back buffer 准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。
当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing 的状况。
VSync (垂直同步)是 VerticalSynchronizatio n的简写,它利用 VBI 时期出现的 vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。
三、Android 屏幕刷新机制
3.1 Android 4.1 之前的问题
在 Android4.1 之前,屏幕刷新也遵循上面介绍的 双缓冲 + VSync 机制。如下图:

以时间的顺序来看下将会发生的过程:
-
Display 显示第0帧数据,此时 CPU 和 GPU 渲染第1帧画面,且在Display显示下一帧前完成
-
因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧
-
接着第2帧开始处理,是直到第2个VSync快来前才开始处理的。
-
第2个 VSync 来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为 “Jank” ,即发生了丢帧。
-
当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个 VSync 进行缓存交换再显示。
上面的问题是多显示了一次第一帧画面,原因是第2帧的 CPU/GPU 计算没能在 VSync 信号到来前完成 。
我们知道,双缓存的交换是在 Vsync 到来时进行,交换后屏幕会取 Frame buffer 内的新数据,那么此时的 Back buffer 就可以供 GPU 准备下一帧数据了。 如果 Vsync 到来时 CPU/GPU 就开始操作的话,是有完整的16.6ms 的时间来处理数据的,这样应该会基本避免jank的出现了(除非CPU/GPU计算超过了16.6ms)。 那如何让 CPU/GPU 计算在 Vsync 到来时进行呢?
3.2 Drawing With VSync
为了优化显示性能,Google在Android 4.1系统中对 Android Display 系统进行了重构,实现了 Project Butter(黄油工程):系统在收到 VSync pulse 后,将马上开始下一帧的渲染。即一旦收到 VSync 通知(16ms 触发一次),CPU和GPU 立刻开始计算然后把数据写入 buffer。如下图

CPU/GPU 根据 VSYNC 信号同步处理数据,可以让 CPU/GPU 有完整的16ms 的时间来处理数据,减少了 jank 。
问题又来了,如果界面比较复杂,CPU/GPU 的处理时间较长,超过了16.6ms呢?如下图:

-
在第二个时间段内,但却因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。
-
而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个 signal 的来临。于是在这一过程中,有一大段时间是被浪费的。
-
当下一个 VSync 出现时,CPU/GPU 马上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。
为什么 CPU 不能在第二个 16ms 处理绘制工作呢?
原因是只有两个 buffer,Back buffer 正在被 GPU 用来处理B帧的数据, Frame buffer 的内容用于 Display 的显示,这样两个 buffer 都被占用,CPU 则无法准备下一帧的数据。 那么,如果再提供一个buffer,CPU、GPU 和显示设备都能使用各自的 buffer 工作,互不影响。
3.3 三缓冲
三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,缺点是多使用的一个 Graphic Buffer 所占用的内存。

-
第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。
-
注意在第3段中,A帧的计算已完成,但是在第4个 VSync 来的时候才显示,如果是双缓冲,那在第三个VSync就可以显示了。
三缓冲有效利用了等待 VSync 的时间,减少了 Jank,但是带来了延迟。 所以,是不是 Buffer 越多越好呢?这个是否定的,Buffer 正常还是两个,当出现 Jank 后三个足以。
以上就是Android屏幕刷新的原理了。
四、Choreographer
上面讲到,Google 在 Android 4.1 系统中对 Android Display 系统进行了优化:在收到 VSync pulse 后,将马上开始下一帧的渲染。本节就来讲 "drawing with VSync" 的实现——Choreographer。
-
Choreographer,意为 舞蹈编导、编舞者。在这里就是指对CPU/GPU 绘制的指导 —— 收到VSync信号 才开始绘制,保证绘制拥有完整的16.6ms,避免绘制的随机性。
-
Choreographer 是用来协调动画、输入和绘图的计时
-
通常应用层不会直接使用Choreographer,而是使用更高级的API,例如动画和View绘制相关的ValueAnimator.start()、View.invalidate()等。
-
业界一般通过Choreographer来监控应用的帧率。
学习 Choreographer 可以帮助理解每帧运行的原理,也可加深对 Handler 机制、View 绘制流程的理解,这样再去做 UI 优化、卡顿优化,思路会更清晰。
4.1 源码分析
Activity 启动流程走完 onResume 方法后,会进行 Window 的添加。Window 添加过程会调用 ViewRootImpl 的 setView() 方法,setView() 方法会调用 requestLayout() 方法来请求绘制布局,requestLayout() 方法内部又会走到 scheduleTraversals() 方法,最后会走到 performTraversals() 方法,接着到了我们熟知的测量、布局、绘制三大流程了。
另外,查看源码发现,当我们使用 ValueAnimator.start()、View.invalidate() 时,最后也是走到 ViewRootImpl 的scheduleTraversals() 方法。(View.invalidate() 内部会循环获取 ViewParent 直到 ViewRootImpl 的invalidateChildInParent() 方法,然后走到 scheduleTraversals() ,可自行查看源码 )。
因此:所有 UI 的变化都是走到 ViewRootImpl 的 scheduleTraversals() 方法。
说明:源码基于Android API 30 进行分析。
接下来我们从 ViewRootImpl 的 scheduleTraversals 方法开始分析,代码如下所示:
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
// 用来保证同一时间多次更改只会刷新一次。
mTraversalScheduled = true;
// 添加同步屏障,屏蔽同步消息,保证VSync信号到来立即执行绘制
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// mTraversalRunnable 是 TraversalRunnable 的实例,最终走到 run 方法,然后调用 doTraversal 方法。
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 开始View绘制的三大流程
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
主要逻辑说明:
-
首先使用 mTraversalScheduled 字段保证同时间多次更改只会刷新一次。
-
然后向当前线程的消息队列 MessageQueue 中添加了同步屏障,这样就屏蔽了正常的同步消息,保证 VSync 到来后立即执行绘制,而不是要等前面的同步消息。后面会具体分析同步屏障和异步消息的代码逻辑。

本文围绕Android屏幕刷新机制展开,先介绍基础概念,如屏幕刷新频率、帧率、双缓冲机制等。接着分析Android 4.1前后屏幕刷新机制的问题及优化,引入三缓冲。还讲解了Choreographer协调绘制的原理及源码,最后对Handler消息类型进行分析。
1433

被折叠的 条评论
为什么被折叠?



