背景
Android4.1之前,UI不流畅的问题非常的明显,Google决定在后续的每个版本都进行一个重要问题的优化,4.1的重要优化就是提高流畅度,整个项目称之为黄油计划,整个过程对渲染体系几乎是进行了重构。
这里先简单的介绍一下黄油计划的几个核心设计:
-
通过将Vsync引入到局部系统中去实现稳定的帧率
-
增加三缓冲(Triple Buffer)进一步保证帧率的稳定
-
编舞者(Choreographer)配合Vsync实现事件输入、动画、绘制等任务。
-
同步屏障 保障UI渲染的优先级。
为什么要学习这古老的设计?
黄油工程大概是在2012年诞生的,都2024年了,为什么还要学习黄油工程? 学了黄油工程有什么用?
优雅的设计总是相同的,可以复用在其他场景,后面我会分享一个参考黄油工程的实际案例。
技术解读
这里会从VSync局部系统的引入、三缓冲、编舞者、消息屏障四个方面逐步解读。
局部系统加入 VSync
未加入VSync之前
(图中display上的数字显示的是具体的哪一帧,比如第一个周期显示的是帧1,第2个周期因为帧2还没生成,最终还是现实的帧1,这时候就发生了丢帧,这一帧我们就称之为jank帧)
从图中可以看出,由于CPU没有时钟的控制,有时候在一个VSync内,可能会渲染2帧,有时候一帧也不会渲染,导致的结果就是在5个时钟周期内生产了5帧,最终只渲染了3帧(1/3/4这几帧)。
GPU/CPU 加入 Vsync 同步
控制每一帧的CPU、GPU时间跟 上一张图一致,看看最终的效果。
从图中可以看出,同样的CPU跟GPU的使用率,5个时钟周期产生了5帧,渲染了4帧。
如果GPU跟CPU的执行时长超过一个时钟周期的话(比如刷新率是60帧,每个周期是16ms,每一帧CPU+GPU的执行为18ms),使用VSYNC+双缓冲问题就严重了。
从图中可以看出,几乎会丢失一半的帧,如果刷新率是60帧的话,最终只有30帧。如果发生了这种情况,最终效果可能还不如黄油工程之前,于是,官方又引入了三缓冲。
加入三缓冲
从图中可以清晰的看出,只有第一帧渲染失败,后续的每一帧都渲染成功
编舞者(Choreographer)
编舞者是什么?
编舞者的作用是承上启下,用来接收系统层VSync信号,接收到信号后,通知处理。
这里有三个核心的类去完成一个VSync周期的渲染。
类 | 作用 |
---|---|
FrameDisplayEventReceiver | 接收VSync信号,发送异步消息,调用 Choreographer 的doFrame方法。 |
Choreographer | 监听修改UI的各种任务,收到FrameDisplayEventReceiver的回调后,逐个回调回去,比如调用ViewRootImp的doTransval()。 |
ViewRootImp | 这是View树最顶层的一个ViewParent,但它只是实现了ViewParent的接口,但实际上并不是一个View,可以将其理解为一个桥梁,控制view的绘制流程以及与其他进程通信。 |
Choreographer 最核心的代码是持有一个CallbackQueue数组,数组大小一般是5。
-
第1个是 CALLBACK_INPUT
-
第2个是 CALLBACK_ANIMATION
-
第3个是 CALLBACK_INSETS_ANIMATION
-
第4个是 CALLBACK_TRAVERSAL
-
第5个是 CALLBACK_COMMIT
public final class Choreographer {
...
private final CallbackQueue[] mCallbackQueues;
public static final int CALLBACK_COMMIT = 4;
private static final int CALLBACK_LAST = CALLBACK_COMMIT;
public Choreographer() {
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
}
...
}
编舞者的工作时序图
这里以一个button设置文字为text1,点击事件设置文字为text2为例。(这里省略了一些细节,比如布局如果是自适应,修改文字会触发requestLayout)
从图中第二个VSync周期可以看出,input事件的回调会影响后续的回调。这里的回调顺序优先级分别为,Input事件,Animation,InsetsAnimation,Traversal、Commit。
同步屏障
同步屏障是什么?
同步屏障是一个特殊的Message,它的Target为null,使用方式跟普通Message一样。
目前Message有三种:
消息类型 | 说明 |
---|---|
同步消息 | 普通的消息,平常我们调用 handler.post() 方法生成的就是同步消息。 |
异步消息 | 普通消息调用 msg.setAsynchronous(true); 就会变成异步消息。在没有同步屏障的情况下,跟同步消息一样。 |
同步屏障 | Target 为null,不能自己创建,只要是通过handler post的方法,最终都会默认设置一个target。如果要创建同步屏障,必须调用MessageQueue 的 postSyncBarrier,这是一个private方法,业务层想要使用的话,可以通过反射调用。 |
当这个Message加入到队列之后,后续的同步消息都不会执行,相当于是给同步消息亮起了红灯,给异步消息开启了绿色通道。
目前只有在 ViewRootImp scheduleTraversals 的时候会去调用同步屏障,在doTraversal 的时候 remove 掉同步屏障。
class ViewRootImp{
...
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
...
}
class MessageQueue {
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
public void removeSyncBarrier(int token) {
// Remove a sync barrier token from the queue.
// If the queue is no longer stalled by a barrier then wake it.
synchronized (this) {
Message prev = null;
Message p = mMessages;
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
if (p == null) {
throw new IllegalStateException("The specified message queue synchronization "
+ " barrier token has not been posted or has already been removed.");
}
final boolean needWake;
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
p.recycleUnchecked();
// If the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
}
直播相关实践
多缓冲采集编码推流
这里的技术方案参考了 Vsync 三缓冲的设计理念,帧率取得了较大提升。
原有架构
多缓冲架构架构
通用性卡顿解决方案
具体原理:通过插入同步屏障提高Vsync的响应时间。