Android系統從4.1(API 16)開始加入Choreographer這個類來控制同步處理輸入(Input)、動畫(Animation)、繪制(Draw)三個UI操作。其實UI顯示的時候每一幀要完成的事情只有這三種。如下圖是官網的相關說明:
Choreographer接收顯示系統的時間脈沖(垂直同步信號-VSync信號),在下一個frame渲染時控制執行這些操作。
Choreographer中文翻譯過來是"舞蹈指揮",字面上的意思就是優雅地指揮以上三個UI操作一起跳一支舞。這個詞可以概括這個類的工作,如果android系統是一場芭蕾舞,他就是Android UI顯示這出精彩舞劇的編舞,指揮臺上的演員們相互合作,精彩演出。Google的工程師看來挺喜歡舞蹈的!
好了廢話不多說,下面讓我們來看看劇本是怎么設計的,Let's Read the fucking source code!
Choreographer的源碼位于android.view這個pakage中,是view層框架的一部分,Android studio里面搜一下就可以看到源碼了。
首先看看頭部的一些說明,大體了解一下這個類是干嘛的,有助于我們理解接下來的源碼。 和官網的文檔是一樣的,應該就是用這個生成的,和上面一部分相比介紹了Choreographer的使用接口。開發者可以使用Choreographer#postFrameCallback設置自己的callback與Choreographer交互,你設置的callCack會在下一個frame被渲染時觸發。Callback有4種類型,Input、Animation、Draw,還有一種是用來解決動畫啟動問題的,將在下文介紹。這四種操作都是這么觸發的。
如下圖:
收到VSync信號后,順序執行3個操作,然后等待下一個信號,再次順序執行3個操作。假設在第二個信號到來之前,所有的操作都執行完成了,即Draw操作完成了,那么第二個信號來到時,此時界面將會更新為第一frame的內容,因為Draw操作已經完成了。否則界面將不會更新,還是顯示上一個frame的內容,表示你丟幀了。丟幀是造成卡頓的原因。如下圖:
第二個信號到來時,Draw操作沒有按時完成,導致第三個時鐘周期內顯示的還是第一幀的內容。
注意文檔的最后一段話:
Each Looper thread has its own choreographer. Other threads can post callbacks to run on the choreographer but they will run on the Looper to which the choreographer belongs.*
每個線程都有自己的choreographer。
基本上的原理就是上面這樣,那么接下來我們通過源碼詳細地看一下細節是怎么實現的。
首先先看看構造函數。
構造函數
private Choreographer(Looper looper) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
mLastFrameTimeNanos = Long.MIN_VALUE;
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
這里做了幾個初始化操作,根據Looper對象生成,Looper和線程是一對一的關系,對應上面說明里的每個線程對應一個Choreographer。
1.初始化FrameHandler。接收處理消息。
2.初始化FrameDisplayEventReceiver。FrameDisplayEventReceiver用來接收垂直同步脈沖,就是VSync信號,VSync信號是一個時間脈沖,一般為60HZ,用來控制系統同步操作,怎么同ChoreoGrapher一起工作的,將在下文介紹。
3.初始化mLastFrameTimeNanos(標記上一個frame的渲染時間)以及mFrameIntervalNanos(幀率,fps,一般手機上為1s/60)。
4.初始化CallbackQueue,callback隊列,將在下一幀開始渲染時回調。
我們首先看看FrameHandler和FrameDisplayEventReceiver的結構。
FrameHandler
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}
看上面的代碼,就是一個簡單的Handler。處理3個類型的消息。
MSG_DO_FRAME:開始渲染下一幀的操作
MSG_DO_SCHEDULE_VSYNC:請求Vsync信號
MSG_DO_SCHEDULE_CALLBACK:請求執行callback
額,下面再細分一下,分別詳細看一下這三個步驟是怎么實現的。繼續看源碼吧。。。
FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
public FrameDisplayEventReceiver(Looper looper) {
super(looper);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
...
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
FrameDisplayEventReceiver繼承自DisplayEventReceiver接收底層的VSync信號開始處理UI過程。VSync信號由SurfaceFlinger實現并定時發送。FrameDisplayEventReceiver收到信號后,調用onVsync方法組織消息發送到主線程處理。這個消息主要內容就是run方法里面的doFrame了,這里mTimestampNanos是信號到來的時間參數。
FrameHandler和FrameDisplayEventReceiver是怎么工作的呢?ChoreoGrapher的總體流程圖如下圖:
流程圖
以上是總體的流程圖:
1.PostCallBack,發起添加回調,這個FrameCallBack將在下一幀被渲染時執行。
2.AddToCallBackQueue,將FrameCallBack添加到回調隊列里面,等待時機執行回調。每種類型的callback按照設置的執行時間(dueTime)順序排序分別保存在一個單鏈表中。
3.判斷FrameCallBack設定的執行時間是否在當前時間之后,若是,發送MSG_DO_SCHEDULE_CALLBACK消息到主線程,安排執行doScheduleCallback,安排執行CallBack。否則直接跳到第4步。
4.執行scheduleFrameLocked,安排執行下一幀。
5.判斷上一幀是否已經執行,若未執行,當前操作直接結束。若已經執行,根據情況執行以下6、7步。
6.若使用垂直同步信號進行同步,則執行7.否則,直接跳到9。
7.若當前線程是UI線程,則通過執行scheduleVsyncLocked請求垂直同步信號。否則,送MSG_DO_SCHEDULE_VSYNC消息到主線程,安排執行doScheduleVsync,在主線程調用scheduleVsyncLocked。
8.收到垂直同步信號,調用FrameDisplayEventReceiver.onVsync(),發送消息到主線程,請求執行doFrame。
9.執行doFrame,渲染下一幀。
主要的工作在doFrame中,接下來我們具體看看doFrame函數都干了些什么。
從名字看很容易理解doFrame函數就是開始進行下一幀的顯示工作。好了以下源代碼又來了,我們一行一行分析一下吧。
doFrame
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) { //判斷是否有callback需要執行,mFrameScheduled會在postCallBack的時候置為true,一次frame執行時置為false
return; // no work to do
}
\\\\打印跳frame時間
if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
mDebugPrintNextFrameTimeDelta = false;
Log.d(TAG, "Frame time delta: "
+ ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
}
//設置當前frame的Vsync信號到來時間
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();//實際開始執行當前frame的時間
//時間差
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
//時間差大于一個時鐘周期,認為跳frame
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//跳frame數大于默認值,打印警告信息,默認值為30
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
//計算實際開始當前frame與時鐘信號的偏差值
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
//打印偏差及跳幀信息
if (DEBUG_JANK) {
Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ "which is more than the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Skipping " + skippedFrames + " frames and setting frame "
+ "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
}
//修正偏差值,忽略偏差,為了后續更好地同步工作
frameTimeNanos = startNanos - lastFrameOffset;
}
//若時間回溯,則不進行任何工作,等待下一個時鐘信號的到來
//這里為什么會發生時間回溯我沒搞明白,大概是未知時鐘錯誤引起?注釋里說的maybe 好像不太對
if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG_JANK) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ "previously skipped frame. Waiting for next vsync.");
}
//請求下一次時鐘信號
scheduleVsyncLocked();
return;
}
//記錄當前frame信息
mFrameInfo.setVsync(intendedFrameTimeNanos,frameTimeNanos);
mFrameScheduled = false;
//記錄上一次frame開始時間,修正后的
mLastFrameTimeNanos = frameTimeNanos;
}
try {
//執行相關callBack
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
if (DEBUG_FRAMES) {
final long endNanos = System.nanoTime();
Log.d(TAG, "Frame " + frame + ": Finished, took "
+ (endNanos - startNanos) * 0.000001f + " ms, latency "
+ (startNanos - frameTimeNanos) * 0.000001f + " ms.");
}
}
大部分內容都在上面的注釋中說明了,大概是以下的流程:
總結起來其實主要是兩個操作:
1.設置當前frame的啟動時間。
判斷是否跳幀,若跳幀修正當前frame的啟動時間到最近的VSync信號時間。如果沒跳幀,當前frame啟動時間直接設置為當前VSync信號時間。修正完時間后,無論當前frame是否跳幀,使得當前frame的啟動時間與VSync信號還是在一個節奏上的,可能可能延后了一到幾個周期,但是節奏點還是吻合的。
如下圖所示是時間修正的一個例子,
由于第二個frame執行超時,第三個frame實際啟動時間比第三個VSync信號到來時間要晚,因為這時候延時比較小,沒有超過一個時鐘周期,系統還是將frameTimeNanos3傳給回調,回調拿到的時間和VSync信號同步。
再來看看下圖:
由于第二個frame執行時間超過2個時鐘周期,導致第三個frame延后執行時間大于一個時鐘周期,系統認為這時候影響較大,判定為跳幀了,將第三個frame的時間修正為frameTimeNanos4,比VSync真正到來的時間晚了一個時鐘周期。
時間修正,既保證了doFrame操作和VSync保持同步節奏,又保證實際啟動時間與記錄的時間點相差不會太大,便于同步及分析。
2.順序執行callBack隊列里面的callback.
然后接下來看看doCallbacks的執行過程:
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
// We use "now" to determine when callbacks become due because it's possible
// for earlier processing phases in a frame to post callbacks that should run
// in a following phase, such as an input event that causes an animation to start.
final long now = System.nanoTime();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
// Update the frame time if necessary when committing the frame.
// We only update the frame time if we are more than 2 frames late reaching
// the commit phase. This ensures that the frame time which is observed by the
// callbacks will always increase from one frame to the next and never repeat.
// We never want the next frame's starting frame time to end up being less than
// or equal to the previous frame's commit frame time. Keep in mind that the
// next frame has most likely already been scheduled by now so we play it
// safe by ensuring the commit time is always at least one frame behind.
if (callbackType == Choreographer.CALLBACK_COMMIT) {
final long jitterNanos = now - frameTimeNanos;
Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
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) {
if (DEBUG_FRAMES) {
Log.d(TAG, "RunCallback: type=" + callbackType
+ ", action=" + c.action + ", token=" + c.token
+ ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
}
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
callback的類型有以下4種,除了文章一開始提到的3中外,還有一個CALLBACK_COMMIT。
CALLBACK_INPUT:輸入
CALLBACK_ANIMATION:動畫
CALLBACK_TRAVERSAL:遍歷,執行measure、layout、draw
CALLBACK_COMMIT:遍歷完成的提交操作,用來修正動畫啟動時間
然后看上面的源碼,分析一下每個callback的執行過程:
1.callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS);得到執行時間在當前時間之前的所有CallBack,保存在單鏈表中。每種類型的callback按執行時間先后順序排序分別存在一個單鏈表里面。為了保證當前callback執行時新post進來的callback在下一個frame時才被執行,這個地方extractDueCallbacksLocked會將需要執行的callback和以后執行的callback斷開變成兩個鏈表,新post進來的callback會被放到后面一個鏈表中。當前frame只會執行前一個鏈表中的callback,保證了在執行callback時,如果callback中Post相同類型的callback,這些新加的callback將在下一個frame啟動后才會被執行。
2.接下來,看一大段注釋,如果類型是CALLBACK_COMMIT,并且當前frame渲染時間超過了兩個時鐘周期,則將當前提交時間修正為上一個垂直同步信號時間。為了保證下一個frame的提交時間和當前frame時間相差為一且不重復。
這個地方注釋挺難看懂,實際上這個地方CALLBACK_COMMIT是為了解決ValueAnimator的一個問題而引入的,主要是解決因為遍歷時間過長導致動畫時間啟動過長,時間縮短,導致跳幀,這里修正動畫第一個frame開始時間延后來改善,這時候才表示動畫真正啟動。為什么不直接設置當前時間而是回溯一個時鐘周期之前的時間呢?看注釋,這里如果設置為當前frame時間,因為動畫的第一個frame其實已經繪制完成,第二個frame這時候已經開始了,設置為當前時間會導致這兩個frame時間一樣,導致沖突。詳細情況請看官方針對這個問題的修改。Fix animation start jank due to expensive layout operations.
如下圖所示:
比如說在第二個frame開始執行時,開始渲染動畫的第一個畫面,第二個frame執行時間超過了兩個時鐘周期,Draw操作執行結束后,這時候完成了動畫第一幀的渲染,動畫實際上還沒開始,但是時間已經過了兩個時鐘周期,后面動畫實際執行時間將會縮短一個時鐘周期。這時候系統通過修正commit時間到frameTimeNanos的上一個VSync信號時間,即完成動畫第一幀渲染之前的VSync信號到來時間,修正了動畫啟動時間,保證動畫執行時間的正確性。
3.接下來就是調用c.run(frameTimeNanos);執行回調。
例如,你可以寫一個自定義的FPSFrameCallback繼承自Choreographer.FrameCallback,實現里面的doFrame方法。
public class FPSFrameCallback implements Choreographer.FrameCallback{
@Override
public void doFrame(long frameTimeNanos){
//do something
}
}
通過
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback());
把你的回調添加到Choreographer之中,那么在下一個frame被渲染的時候就會回調你的callback,執行你定義的doFrame操作,這時候你就可以獲取到這一幀的開始渲染時間并做一些自己想做的事情了。
開源組件Tiny Dancer就是根據這個原理獲取每一幀的渲染時間,繼而分析實現獲取設備的當前幀率的。有興趣的人可以查看。
Tiny Dancer
好了,關于Choreographer的分析到此結束。希望對你有幫助。