今天我们来品一品Choreographer,这个东西翻译成中文是 编舞师 的意思,为啥要这么叫呢?品完你就知道了
开胃菜:初识Choreographer
Choreographer的由来——黄油计划
引入 Vsync 之前的 Android 版本,渲染一帧相关的Message中间是没有间隔的,上一帧绘制完,下一帧的 Message 紧接着就开始被处理。这样的问题就是,帧率不稳定,可能高也可能低,如果在该处理绘制的时候CPU去处理其他任务了,就会造成丢帧,会有很明显的卡顿感,如下图👇
为了解决UI卡顿的问题,Google 在 2012 年的 I/O 大会上宣布了 Project Butter 黄油计划,并且在 Android 4.1 中正式开启了这个机制。黄油计划 对 Android Display 系统进行了重构,引入了三个核心元素,即 VSYNC、Triple Buffer 和 Choreographer,其主要目的就是提供一个稳定的帧率输出机制,让软件层和硬件层可以以共同的频率一起工作。👇
另外,Android 在 4.1 还对 Handler 机制进行了略微改造,使之支持 Asynchronous Message(异步消息) 和 Synchronization Barrier(同步屏障)。
Choreographer的职能
Choreographer的引入,主要是配合Vsync,给上层App的渲染提供一个稳定的绘制处理的时机,也就是Vsync到来的时候。Choreographer可以接收系统的VSync信号,统一管理应用的输入、动画和绘制等任务的执行时机。Android 的 UI 绘制任务将在它的统一指挥下,井然有序的完成。这就是引入 Choreographer的主要作用,业内一般通过它来监控应用的帧率。
Choreographer的踪迹
我们在使用systrace的时候,很容易发现Choreographer的踪迹👇:
通过trace我们发现:input事件的处理和绘制流程traversal都是由Choreographer来触发的,我们接下来从源码的角度来看一下它的工作流程
主菜:源码分析
Choreographer的初始化
和Choreographer关系最为密切的就是ViewRootImpl了,在ViewRootImpl的构造函数中是通过Choreographer.getInstance()
来获取Choreographer实例的
public static Choreographer getInstance() {
return sThreadInstance.get();
}
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
//当前线程必须要有Looper
throw new IllegalStateException("The current thread must have a looper!");
}
return new Choreographer(looper, VSYNC_SOURCE_APP);
}
};
可以看到,Choreographer是由ThreadLocal管理的,每个线程一个单利,而且这个线程必须要有Looper,因为Choreographer申请和接收VSync信号需要依赖native层的Looper。接下来,我们去看看它的构造方法:
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC //Android 4.1之后默认为true
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
//Nanos 纳秒 ,1秒 = 1000毫秒 = 1000000微秒 = 1000000000纳秒
//一帧的时间,出于计算精度的考虑,精确到纳秒
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()); //getRefreshRate()获取屏幕刷新率
//mCallbackQueue是一个容量为4的数组,每一个元素内部维护了一个callback链表,4种事件就是通过这4个链表来维护的。链表是按照执行时间的先后顺序排序
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
在初始化的过程中,除了一些变量的赋值操作以外,有两个关键步骤:
1.创建了一个FrameDisplayEventReceiver,这个东西是用来申请和接收Vsync信号的
2.初始化CallbackQueue数组,容量是4,对应4种callback类型(以前是3种,在Android 6.0的时候添加的第4种)每个CallbackQueue内部维护一个链表,按照时间先后排序。CallBack类型分别为:
//输入回调,最先调用
public static final int CALLBACK_INPUT = 0;
//动画回调,traversals之前调用
public static final int CALLBACK_ANIMATION = 1;
//traversals回调,处理布局和绘制。 在处理完所有其他异步消息之后执行
public static final int CALLBACK_TRAVERSAL = 2;
//提交回调,处理当前帧绘制后的操作,traversals之后执行。Android 6.0添加的
public static final int CALLBACK_COMMIT = 3;
CallbackQueue
Choreographer初始化的时候有一个重要的步骤是初始化CallbackQueue,我们来简单的说一下CallbackQueue,这里就不贴代码了,感兴趣的朋友可以自己去看一下
- 每个callback都会被封装成一个
CallbackRecord
,CallbackRecord以链表的方式储存。CallbackRecord内部记录了链表的next节点、callback的执行时间、callback的执行行为(runnable.run或者doFrame回调) - CallbackQueue内部持有着CallbackRecord链表的头元素
- CallbackQueue有三个方法:
- extractDueCallbacksLocked:取出满足时间条件的callBack,返回一个链表
- addCallbackLocked:添加一个callback,按照执行时间先后排序
- removeCallbacksLocked 删除callback
postCallback
postCallback
是Choreographer核心逻辑的入口,ViewRootImpl与Choreographer的交互主要就是通过postCallback
。接下来我们从postCallback
入手,看一看Choreographer核心逻辑的流程
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
if (action == null) {
throw new IllegalArgumentException("action must not be null");
}
if (callbackType < 0 || callbackType > CALLBACK_LAST) {
throw new IllegalArgumentException("callbackType is invalid");
}
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//callBack入队,队列按照dueTime的时间先后顺序排列
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//立即执行
scheduleFrameLocked(now);
} else {
//延时执行
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
...
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}
void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
//判断这个callBack是否被提前remove
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}
postCallback
最终会调用到postCallbackDelayedInternal
,在postCallbackDelayedInternal
中首先会将CallBack放入对应的队列,然后判断是否需要立即执行,如果需要立即执行就直接调用scheduleFrameLocked
,如果需要延时执行,就通过handler发送一个延时异步消息,最终也是执行scheduleFrameLocked
,所以我们接下来看一下scheduleFrameLocked
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) { //标记位,保证在一次VSync周期内不会重复的去申请下一个VSync信号,这个标记位会在doFrame时清除
mFrameScheduled = true;
if (USE_VSYNC) { //使用垂直同步,Android 4.1后默认开启
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}
// 如果在 UI 线程上运行,则立即调度 vsync,
// 否则尽快从 UI 线程发布消息调度 vsync。
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//不使用垂直同步,通过消息机制模拟周期信号
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
这里的逻辑比较简单,如果当前在主线程,就直接去申请下一个VSync信号,如果不在主线程,就通过Handler发送异步消息去申请VSync信号,如果没有开启VSync,就模拟VSync信号。另外,有一个mFrameScheduled
变量用来避免重复的申请VSync信号,因为这个流程是从postCallBack走进来的,在绘制流程中,会执行多次postCallBack,让各种各样的CallBack入队,不能每次postCallBack都申请一次VSync。
调用scheduleVsyncLocked
之后,会通过初始化时创建的FrameDisplayEventReceiver
去向native层申请下一次VSync信号,当下一个VSync信号来临时,就会调用FrameDisplayEventReceiver.onVsync()
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
...
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this); //因为FrameDisplayEventReceiver实现了Runnable,所以这里传入this,接下来就会被执行下面的run方法
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
在接收到Vsync后,将Receiver本身作为runnable传入异步消息中,并使用handler发送这个异步消息,最终执行的就是doFrame()方法了。需要注意的是,在这里仅仅是用handler发送消息到MessageQueue中,不一定是立刻执行,需要等待MessageQueue中排在前面的消息执行完毕。
我们通过一张流程图来小结一下👇:
流程看起来比较复杂,但是有很多可以简化的地方:
- 我们假设都是在主线程调用,省略掉handler的调度
- Android 4.1后,默认开启VSycn,所以省略掉非VSync的情况
我们最终可以得到一个精简版的流程图👇:
通过上图,相信大家已经非常清楚
postCallback
做了什么事了,那我们接下来继续往下看
doFrame
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
// 只有申请vsync信号的时候mFrameScheduled才会置为true,doFrame执行后会被置为false。
// 保证一次申请信号只执行一次绘制流程
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos; //预期的帧时间
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos; //抖动时间(当前时间和预期时间差)
//如果时间差超过了一帧的时间,修正帧时间,修正成距离最近的一帧的时间
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
if (frameTimeNanos < mLastFrameTimeNanos) {
// 帧时间似乎倒退了。 可能是由于先前跳过的帧。 等待下一个vsync。
scheduleVsyncLocked();
return;
}
//FrameInfo初始化,设置:预期的vsync时间,抖动调整后的vsync时间
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
//开始处理输入事件,FrameInfo记录
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
//开始处理动画,FrameInfo记录
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
//开始执行 ViewRootImpl#performTraversals(),FrameInfo记录
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
//处理commit回调
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
doFrame中首先是对帧时间进行了一些修正,初始化了FrameInfo,这个FrameInfo也是Android 6.0引入的,是用来记录doFrame每个步骤的时间的👇:
Pos | Name | Desc |
---|---|---|
0 | FLAGS | 绘制过程中的额外标志位,例如FLAG_WINDOW_LAYOUT_CHANGED |
1 | INTENDED_VSYNC | 预期的vsync时间,不受抖动调整 |
2 | VSYNC | 抖动调整的垂直同步时间,这是用作动画和绘图系统的输入 |
3 | OLDEST_INPUT_EVENT | 本次处理的输入事件队列中,最早的那个事件的时间 |
4 | NEWEST_INPUT_EVENT | 本次处理的输入事件队列中,最后的那个事件的时间 |
5 | HANDLE_INPUT_START | 输入事件开始处理的时间 |
6 | ANIMATION_START | 动画开始的时间 |
7 | PERFORM_TRAVERSALS_START | ViewRootImpl#performTraversals()的开始时间 |
8 | DRAW_START | View:draw()开始时间 |
我们通过adb命令adb shell dumpsys gfxinfo <应用包名> framestats
就可以看到相应的日志输出,这个命令会抓取过去2秒内的所有帧信息。
准备工作做完后,就开始处理callback链表,处理顺序是:input、animation、traversal、commit。我们来看一下doCallbacks的实现:
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now = System.nanoTime();
//取出所有满足时间的callBack,返回的是一个链表头
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
if (callbackType == Choreographer.CALLBACK_COMMIT) {
final long jitterNanos = now - frameTimeNanos;
Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
// 如果从收到VSync信号到处理Commit回调的时间间隔超过2帧,修正 mLastFrameTimeNanos
if (jitterNanos >= 2 * mFrameIntervalNanos) {
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
+ mFrameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
+ " ms which is more than twice the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Setting frame time to " + (lastFrameOffset * 0.000001f)
+ " ms in the past.");
mDebugPrintNextFrameTimeDelta = true;
}
frameTimeNanos = now - lastFrameOffset;
mLastFrameTimeNanos = frameTimeNanos;
}
}
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
//依次执行每个Callback
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
//Callback用完了,回收
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
核心逻辑很简单,从CallbackQueue中取出所有满足时间的callback,是一个链表,然后遍历链表依次调用callback的run方法,执行callback中的Runnable或者FrameCallback。我们还是画一个流程图👇
小结
到此,Choreographer在一个VSync信号周期内的工作就完成了,我们来小结一下:
- postCallback会
触发VSync信号的申请
,同时会将callback入队
,当VSync信号到来时,会通过Handler调度
执行doFrame - doFrame负责从各个队列中取出符合时间的callback去执行,执行顺序依次是input、animation、traversal、commit
- native层VSync的接收是通过Looper实现的,VSync信号到来后执行doFrame也是通过Handler.post,所以,当主线程有耗时操作时,将直接影响到VSync信号的接收和doFrame的调度,从而引发丢帧
- 系统会不停的产生VSync信号,但是应用不会每次都接到VSync信号,当应用需要接收的时候,才去申请下一个VSync信号
总流程图👇
总结
Choreographer的分析到这里就结束了,可以看出来,Choreographer掌控着UI刷新的节奏,这也是它为什么被叫做编舞师:ViewRootImpl就像舞蹈演员,Choreographer负责告诉它什么时候应该做什么动作。
在业内Choreographer常被用来做一些帧率监控的工作,例如Matrix的TraceCanary模块中的FrameTracer,就是基于Choreographer的原理实现的,感兴趣的朋友可以去看一下源码
ok,Choreographer我们已经品完了,希望大家有所收获!溜了溜了。。。