Choreographer 是 Android 4.1 google的黃油計劃新增的機制馍佑,用于配合系統(tǒng)的 VSYNC 中斷信號混巧。其主要用途是接收系統(tǒng)的 VSYNC 信號姜挺,統(tǒng)一管理應(yīng)用的輸入盐类、動畫和繪制等任務(wù)的執(zhí)行時機晰绎。也就是(CALLBACK_INPUT,CALLBACK_ANIMATION,CALLBACK_TRAVERSAL,CALLBACK_COMMIT)寓落,而我們主要的用途就是來查看app的幀率情況,下面來具體分析下這個神秘的類
當(dāng)我們進行invalidate或者requestLayout時荞下,總會執(zhí)行viewRootImp的scheduleTraversals方法
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
而這個mTraversalRunnable就是我們所要執(zhí)行的任務(wù)了伶选,那究竟是何時執(zhí)行呢?
首先會mChoreographer.postCallback會間接調(diào)用postCallbackDelayedInternal方法
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;
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);
}
}
}
我們可以看到正常情況下會執(zhí)行scheduleFrameLocked方法
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}
// If running on the Looper thread, then schedule the vsync immediately,
// otherwise post a message to schedule the vsync from the UI thread
// as soon as possible.
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);
}
}
}
由于在4.1上是使用VSYNC信號的尖昏,所以就自然會調(diào)用scheduleVsyncLocked方法,會間接調(diào)用scheduleVsync方法
**
* Schedules a single vertical sync pulse to be delivered when the next
* display frame begins.
*/
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
nativeScheduleVsync(mReceiverPtr);
}
}
注意:這里的注釋說的很清楚了仰税,當(dāng)下一幀來臨時準(zhǔn)備一個要分發(fā)的垂直同步信號,啥意思呢抽诉?簡單來說就是當(dāng)調(diào)用了nativeScheduleVsync方法時陨簇,當(dāng)屏幕需要刷新的時候,也就是每隔16.6ms會通過native的looper分發(fā)到j(luò)ava層迹淌,從而調(diào)用java的方法河绽,那是哪個方法呢己单?
// Called from native code.
@SuppressWarnings("unused")
private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
onVsync(timestampNanos, builtInDisplayId, frame);
}
很明顯是此方法
舉個例子,比如屏幕顯示的是第一幀耙饰,你在第一幀調(diào)用invalidate纹笼,其實并不是立即刷新的,而是在一幀會去注冊一個Vsync(前提是這一幀cpu空閑情況下)苟跪,當(dāng)下一幀來臨時也就是第二幀的時候會調(diào)用dispatchVsync此方法允乐,當(dāng)然這是一種比較簡單的情況,復(fù)雜的等會說
那么來看一下調(diào)用的onVsync方法
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
// Ignore vsync from secondary display.
// This can be problematic because the call to scheduleVsync() is a one-shot.
// We need to ensure that we will still receive the vsync from the primary
// display which is the one we really care about. Ideally we should schedule
// vsync for a particular display.
// At this time Surface Flinger won't send us vsyncs for secondary displays
// but that could change in the future so let's log a message to help us remember
// that we need to fix this.
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
Log.d(TAG, "Received vsync from secondary display, but we don't support "
+ "this case yet. Choreographer needs a way to explicitly request "
+ "vsync for a specific display to ensure it doesn't lose track "
+ "of its scheduled vsync.");
scheduleVsync();
return;
}
// Post the vsync event to the Handler.
// The idea is to prevent incoming vsync events from completely starving
// the message queue. If there are no messages in the queue with timestamps
// earlier than the frame time, then the vsync event will be processed immediately.
// Otherwise, messages that predate the vsync event will be handled first.
long now = System.nanoTime();
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
timestampNanos = now;
}
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
注意看下timestampNanos的參數(shù)(簡單來說就是從調(diào)用native方法以后到回調(diào)到這個方法所經(jīng)過的的時間)
接著看會發(fā)送一條異步消息削咆,簡單來說就是此消息在消息隊列中不用排隊牍疏,可以最先被取出來,很明顯拨齐,會調(diào)用下面的run方法進行處理
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
這里調(diào)用的doframe方法
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
mDebugPrintNextFrameTimeDelta = false;
Log.d(TAG, "Frame time delta: "
+ ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
}
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;
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;
}
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;
}
if (mFPSDivisor > 1) {
long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
scheduleVsyncLocked();
return;
}
}
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
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 {
AnimationUtils.unlockAnimationClock();
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.");
}
}
有幾個重點的地方要說下鳞陨,我們開發(fā)時偶爾在log上會看到Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread."這句話,很多人以為出現(xiàn)這句話是因為ui線程太耗時了瞻惋,其實仔細(xì)想想就知道是錯的厦滤,因為在回掉這方法的時候根本沒有執(zhí)行到
測量,繪制等歼狼,它的兩個時間對比的是回掉到此幀的時間frameTimeNanos和當(dāng)前的時間掏导,如果大于16.66ms就會打印這句話,那就是說執(zhí)行native方法到onFrame回掉超過16.66ms羽峰,而onFrame回掉是通過異步消息趟咆,可以忽略不計,那唯一可能出現(xiàn)的情況就是通過handler后執(zhí)行dispatchVsync方法梅屉,與執(zhí)行native方法的耗時值纱,也就是說此時會有多個message,而執(zhí)行dispatchVsync方法的message是排在比較后面的坯汤,這也解釋了這句log:he application may be doing too much work on its main thread.
所以這句話并不能判斷是ui卡頓了虐唠,只能說明有很多message,要減少不必要的message才是優(yōu)化的根本惰聂。
而frameTimeNanos = startNanos - lastFrameOffset;簡單來說就是給vsnc設(shè)置幀數(shù)的偏移量
那又是啥意思呢疆偿?
比如我在第一幀發(fā)起了重繪制,按理來說第二幀就會收到Vsync的信號值搓幌,但是由于message阻塞了超過了16.66杆故,所以收到Vsync的信號自然延續(xù)要了第三幀。
在了解這句話以后鼻种,接下來就是回調(diào)繪制反番,或者input事件了,可以看到代碼會間接調(diào)用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);
}
}
從而執(zhí)行 c.run(frameTimeNanos);方法進行回調(diào)
這里放張圖叉钥,大家可以理解一下
簡單說下
一開始注冊了vsync信號罢缸,所以在下一幀調(diào)用了dispatchVsync方法,由于沒有message阻塞投队,所以接收到了此幀的信號枫疆,進行了繪制,在繪制完成后又注冊了信號敷鸦,可以看到一幀內(nèi)注冊同一信號是無效的息楔,但是回掉會執(zhí)行,到了下一幀扒披,由于message的超時不到16.66ms值依,所以也就是執(zhí)行dispatchVsync與執(zhí)行native方法的間隔時間,所以還是此幀還是有信號的碟案,而由于此幀耗時超過了一幀愿险,所以沒有注冊Vsync,當(dāng)然也不會執(zhí)行dispatchVsync方法价说,到了最后可以看到由于message超過了16.66即使在第三幀注冊了Vsync信號辆亏,但是dispatchVsync執(zhí)行的事件已經(jīng)到了第5幀
卡頓優(yōu)化
在簡單分析完了Choreographer機制以后,來具體說下卡頓優(yōu)化的兩種方案的原理
1鳖目、 利用UI線程的Looper打印的日志匹配扮叨;
2、 使用Choreographer.FrameCallback
第一種是blockcanary的原理领迈,就是利用looper.loop分發(fā)事件的時間間隔作為卡頓的依據(jù)
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);
boolean slowDeliveryDetected = false;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
slowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
slowDeliveryDetected = true;
}
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
也就是對logging進行深入的研究彻磁,一般超過了1000ms就認(rèn)為卡頓了,但我們有沒有想過狸捅,我們通常說的卡頓不是說超過了16.66ms么兵迅,為何這里要超過500ms,甚至1000ms才算是卡頓?
我們要知道薪贫,android系統(tǒng)所有的執(zhí)行都是基于looper機制的恍箭,也就是所有的消息執(zhí)行的時間超過1000ms就認(rèn)定卡頓了,舉個例子瞧省,我們可能在主線程操作數(shù)據(jù)庫扯夭,可能在主線程解析json,可能在主線程寫文件鞍匾,可能在主線程做一些例如高斯模糊的耗時操作
這種情況下我們利用blockcanary是可以檢測出來的交洗,但是如果是卡頓呢?當(dāng)然我們也可以把時間設(shè)定為50ms橡淑,但是檢測出來的太多了构拳,所以就需要第二個機制了Choreographer.FrameCallback,通常這樣寫
public class BlockDetectByChoreographer {
public static void start() {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long lastFrameTimeNanos = 0;
long currentFrameTimeNanos = 0;
@Override
public void doFrame(long frameTimeNanos) {
if(lastFrameTimeNanos == 0){
lastFrameTimeNanos == frameTimeNanos;
}
currentFrameTimeNanos = frameTimeNanos;
long diffMs = TimeUnit.MILLISECONDS.convert(currentFrameTimeNanos-lastFrameTimeNanos, TimeUnit.NANOSECONDS);
if (diffMs > 16.6f) {
long droppedCount = (int)diffMs / 16.6;
}
if (LogMonitor.getInstance().isMonitor()) {
LogMonitor.getInstance().removeMonitor();
}
LogMonitor.getInstance().startMonitor();
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
我們可以看到調(diào)用了postFrameCallback方法,會間接調(diào)用postFrameCallbackDelayed
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
可以看到這里是CALLBACK_ANIMATION的callback,也就說只要監(jiān)聽了此方法他就會不斷的調(diào)用doFrame置森,在doframe里調(diào)用postFrameCallback斗埂,從而來達(dá)到完美的監(jiān)聽ui卡頓的效果
也就是onMeasure,onLayout,onDraw的耗時時間