60Hz刷新頻率由來
- 12fps:由於人類眼睛的特殊生理結構,如果所看畫面之幀率高於每秒約10~12幀的時候,就會認爲是連貫的
- 24fps:有聲電影的拍攝及播放幀率均爲美秒24幀,對一般人而言已經算可接受
- 30fps:早期的高動態電子遊戲,幀率少於美秒30幀的話就會顯得不連貫,這是因爲沒有動態模糊使流暢度降低
- 60fps:在與手機交互的過程中,如觸摸和反饋 60幀以下是能感覺出來的,60幀以上不能察覺變化
- 當幀率低於60fps時感覺畫面有卡頓遲滯現象
Android系統每隔16ms發出VSYNC信號(1000ms/60=16.66ms),觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,爲了能夠實現60fps,這意味着計算渲染的大多數操作都必須在16ms內完成。
屏幕刷新機制大致流程介紹
首先應用程序向系統服務申請一塊buffer(緩存),系統服務返回buffer,應用拿到buffer之後就可以進行繪製,繪製完之後將buffer提交給系統服務,系統服務將buffer寫到屏幕的一塊緩存區,屏幕會以一定的幀率刷新,每次刷新的時候,就會從緩存區將圖像數據讀取顯示出來。如果緩存區沒有新的數據,就一直用舊的數據,這樣屏幕看起來就沒有變
屏幕的圖像緩存不止一個,假如只有一個緩存,如果屏幕這邊正在讀緩存,而系統服務又在寫緩存,這有可能導致屏幕顯示不正常,如一半顯示第一幀圖像的畫面,另一半顯示第二幀圖像的畫面。如何避免這種問題的發生呢?可以弄多個緩存,屏幕從一塊緩存讀取數據顯示,系統服務向另一塊緩存寫入數據。如果要顯示下一幀圖像,將兩個緩存換一下即可,即屏幕從緩存2讀取顯示,系統服務向緩存1寫入數據。
vsync(垂直同步機制)是固定頻率的脈衝信號,屏幕根據這個信號週期性的刷新,屏幕每次收到這個信號,就從屏幕緩存區讀取一幀的圖像數據進行顯示,而繪製是由應用端(任何時候都有可能)發起的,如果屏幕收到vsync信號,但是這一幀的還沒有繪製完,就會顯示上一幀的數據,這並不是因爲繪製這一幀的時間過長(超過了信號發送週期),只是信號快來的時候纔開始繪製,如果頻繁的出現的這種情況,用戶就會感知屏幕的卡頓,即使繪製時間優化的再好也無濟於事,因爲這是底層刷新機制的缺陷。
當然系統提供瞭解決方案,如果繪製和vsync信號同步就好了,每次收到vsync信號時,一方面屏幕獲取圖像數據刷新界面,另一方面應用開始繪製準備下一幀圖像數據。如果優化的好,每一幀圖像繪製控制在16ms以內,就可以非常流暢了。那麼問題來了,應用層view的重繪一般調用requestLayout觸發,這個函數隨時都能調用,如何控制只在vsync信號來時觸發重繪呢?有一個關鍵類Choreography(舞蹈指導,編舞),它最大的作用就是你往裏面發送一個消息,這個消息最快也要等到下一個vsync信號來的時候觸發。比如說繪製可能隨時發起,封裝一個Runnable丟給Choreography,下一個vsync信號來的時候,開始處理消息,然後真正的開始界面的重繪了。相當於UI繪製的節奏完全由Choreography來控制。
Choreography原理分析
就從比較熟系的requestLayout方法開始吧,進行UI操作時,通過checkThread方法進行線程檢查,UI操作是否在UI線程,然後調用scheduleTraversals方法:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
在scheduleTraversals方法裏面,主要做了兩件事,一件事是向線程的消息隊列中加入了syncBarrier,另外就是往mChoreographer的mCallbackQueue數組插入了一個callback(需要執行的相關操作,這裏主要是UI繪製),syncBarrier是一個屏障,將它插入到消息隊列後,這個屏障後面的普通消息就不能處理了,等到屏障撤除之後才能處理。但是這個屏障對異步消息是沒有影響的。主要是有些類型的消息非常緊急,需要馬上處理。如果普通消息太多,容易耽誤事(影響緊急消息的執行),所以插入了一個屏障,優先處理異步消息。請求同步Vsync信號,就是一個異步消息,就是請求系統服務SurfaceFlinger在下一次Vsync信號過來時,立即通知我們,我們就可以立即執行mChoreographer的callback數組裏面對應callback的相關操作,即UI繪製了,這裏先簡單提一下,後面會具體分析。
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {// 註釋1
// 註釋2
mTraversalScheduled = true;
// 註釋3
// 插入同步屏障syncBarrier到消息隊列,擋住普通的同步消息,優先執行異步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 註釋4
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
...
}
}
Choreography和ViewRootImpl一起創建的,是通過ThreadLocal存儲的,即在不同的線程調用getInstance得到的是不同的Choreography對象。
public static Choreographer getInstance() {
// ThreadLocal<Choreography>
return sThreadInstance.get();
}
Choreographer要執行的操作就是mTraversalRunnable即TraversalRunnable的run方法,即doTraversal
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
順着代碼調用邏輯進入doTraversal方法,在scheduleTraversals方法中mTraversalScheduled賦值爲true,在doTraversal註釋1處賦值爲false,即使多次調用requestLayout方法,也不會多次執行,因爲只有下一次vsync信號過來的時候,執行doTraveersal方法時纔會置爲false,即在一個vsync信號週期內,只會觸發一次界面重繪。
void doTraversal() {
if (mTraversalScheduled) {
// 註釋1
mTraversalScheduled = false;
// 註釋2
// 移除同步消息屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
// 開始繪製流程
performTraversals();
...
}
}
}
到目前爲止,我們已經知道Choreography控制UI繪製和Vsync同步的基本原理了,但是同步屏障擋住同步消息,優先執行異步消息,關於異步消息的相關邏輯貌似目前還沒有涉及到,接着看下去,進入Choreography的postCallback方法:
@TestApi
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
順着調用邏輯進入postCallbackDelayed方法
@TestApi
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");
}
// 註釋1
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
我們需要關注的是註釋1處的postCallbackDelayedInternal方法:
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
...
synchronized (mLock) {
// 註釋1
// 獲取當前時間
final long now = SystemClock.uptimeMillis();
// 註釋2
// 超時時間或者理解爲觸發時間
final long dueTime = now + delayMillis;
// 註釋3
// 將執行動作放在mCallbackQueue數組中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {// 註釋4//如果已經到觸發時間就註冊請求垂直同步信號
scheduleFrameLocked(now);
} else {
// 註釋5
// 如果還沒有到觸發時間,使用handler在發送一個延時的異步消息。
// 這個延時消息會在到觸發時間的時候執行
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
註釋3處,Choreography裏面有一個mCallbackQueue數組,裏面的每一個元素都是一個callback的單鏈表,添加callback一方面要根據callback的類型callbackType插到對應的單鏈表,另一方面要根據callback執行的時間順序排序,越是馬上要執行的callback,越是插入到鏈表的前面,然後等待被調用。如果還沒有到觸發時間就走註釋5處邏輯,發送延時異步消息請求同步vsync信號,如果到了觸發時間,進入註釋4處的scheduleFrameLocked方法:
private void scheduleFrameLocked(long now) {
...
// 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.
// 如果在Choreography的UI線程中,就直接調用立即安排垂直同步,否則就發送一個消息到UI線程
// 儘快安排請求一個垂直同步
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
// 異步消息
msg.setAsynchronous(true);
// 插入到消息隊列的頭部,可見消息之緊急因爲要告訴SurfaceFlinger,
// vsync信號過來時第一時間通知
mHandler.sendMessageAtFrontOfQueue(msg);
}
...
}
這個方法的作用很明確,就是請求同步Vsync信號,就是說vsync信號過來的時候,讓系統服務SurfaceFlinger第一時間通知我們,我們去執行繪製的相關操作。如果不在Choreography的UI線程,就發送異步消息讓UI線程請求同步Vsync信號,再看註釋5處之後的邏輯
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:
//postCallbackDelayedInternal()方法中當未到期的時候發送過來的
doScheduleCallback(msg.arg1);
break;
}
}
}
以上代碼我們可以看出這個,FramHandler拿到 whate屬性值爲MSG_DO_SCHEDULE_CALLBACK的時候會去執行 doScheduleCallback(msg.arg1)方法
void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}
這個方法中先是做了一些判斷,mFrameSceduled爲false 並且hasDueCallbacksLocked()這個方法的返回值爲true,看方法名就能猜出這個callback是否到了觸發時間,下面我們再分析這個。最終如果滿足條件的情況下它會調用 scheduleFrameLocked()這個方法,也就是說註釋4處到了觸發時間,註釋5處還沒有到觸發時間,就發送一個延遲異步消息,到了觸發時間,最終都是調用scheduleVsyncLocked方法,該後續調用邏輯如下
@UnsupportedAppUsage
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
@UnsupportedAppUsage
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 {
// native方法
// 請求同步vsync信號
nativeScheduleVsync(mReceiverPtr);
}
}
前面分析了請求同步vsync信號的過程,當下一個vsync信號發生的時候,SurfaceFlinger就會通知我們,就會回調該類的onVsync函數,參數timestampNanos就是vsync的時間戳,該函數裏面會發送一個消息到Choreography的工作線程裏面去了,這裏並不是要切換工作線程,因爲onVsync本身就在Choreography的工作線程。這個消息帶了時間戳的,表示消息觸發的時間,有了這個時間戳,就可以按照時間戳的順序來處理消息。到時間了,就會去執行run方法,即執行doFrame方法
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
...
@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);
}
}
進入doFrame方法:
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
//當前時間
startNanos = System.nanoTime();
//當前時間和垂直同步時間
final long jitterNanos = startNanos - frameTimeNanos;
//垂直同步時間和當前時間的差值如果大於一個週期就修正一下
if (jitterNanos >= mFrameIntervalNanos) {
//取插值和始終週期的餘數
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
//當前時間減去上一步得到的餘數當作最新的始終信號時間
frameTimeNanos = startNanos - lastFrameOffset;
}
//垂直同步時間上一次時間還小,就安排下次垂直同步,直接返回
if (frameTimeNanos < mLastFrameTimeNanos) {
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.");
}
}
doFrame分爲兩個階段,先看第一個階段,參數frameTimeNanos表示這一幀的時間戳先計算當前時間和這個時間戳的間隔有多大,間隔越大,表示要延時了,如果延時超過一個週期(mFrameIntervalNanos), 就要計算到底延遲了幾個週期,如果延遲週期數(丟幀數,跳過的幀數)達到一個常量SKIPPED_FRAME_WARNING_LIMIT ,就會打印日誌"應用在主線程做了太多的事情(耗時操作)"導致繪製延遲,丟幀。第二階段就是處理callback了,callback有四種類型,每種類型對應一個單鏈表callbackQueue,給vsync事件分別分發到四種callback,然後執行對應的doCallbacks函數,單鏈表裏面的callback是有時間戳的,只有到了時間的的callback纔會回調,extractDueCallbacksLocked從callbackQueue裏面取出到了時間的callback,然後在循環裏面執行他們的run函數,requestLayout裏面的scheduleTraversals函數傳的callback是什麼?就是準備繪製,mTraversalRunnable的run方法其實調用的是performTraversals,真正的開始執行UI繪製流程。
總結
大致調用流程圖如下:
應用程序調用requestLayout發起重繪,通過Choreographer發送異步消息,請求同步vsync信號,即下一次vsync信號過來時,系統服務SurfaceFlinger在第一時間通知我們,觸發UI繪製。雖然可以手動多次調用,但是在一個vsync週期內,requestLayout只會執行一次。
常見的問題
1.丟幀一般是什麼原因引起的?
答:主線程有耗時操作,耽誤了view的繪製
2.Android刷新頻率60幀/秒,每隔16ms調onDraw繪製一次?
答: 60幀/秒也是vsync信號的頻率,但不一定每次vsync信號都會去繪製,先要應用端主動發起重繪,纔會向SurfaceFlinger請求接收vsync信號,這樣當vsync信號來的時候,纔會真正去繪製。
3.onDraw執行完之後屏幕會馬上刷新麼?
答: 不會馬上刷新,會等到下一次vsync信號時纔會刷新。
4.如果界面沒有重繪,還會每隔16ms刷新屏幕麼?
答:界面沒有重繪,應用就不會收的vsync信號,屏幕還是會刷新,畫面數據用的是舊的,看起來沒什麼變化而已
5.如果屏幕快要刷新的時候纔去onDraw繪製會丟幀麼?
答: 重繪不會立即執行,而是等到下一次vsync信號來時纔開始, 所以什麼時候發起重繪影響不大