這個問題在其他人整理的面試寶典中也有提及,一般來說都是問View的刷新,基本上從ViewRootImpl的scheduleTraversals()方法開始講就可以了。之前看別人面試斗魚的面經(jīng),被問到了Android屏幕刷新機制、雙緩沖、三緩沖、黃油計劃,然后我面網(wǎng)易云的時候也確實被問到了這個題目。
屏幕刷新這一整套,你把我這篇文章里的內容講清楚了,肯定ok了。網(wǎng)易云還附加問了我CPU和GPU怎么交換繪制數(shù)據(jù)的,這個我個人認為完全是加分題了,我答不出來,感興趣的小伙伴可以去看一看,你要是能說清楚,肯定能讓面試官眼前一亮。
雙緩沖
在講雙緩沖這個概念之前,先來了解一些基礎知識。
顯示系統(tǒng)基礎
在一個典型的顯示系統(tǒng)中,一般包括CPU、GPU、Display三個部分, CPU負責計算幀數(shù)據(jù),把計算好的數(shù)據(jù)交給GPU, GPU會對圖形數(shù)據(jù)進行渲染,渲染好后放到buffer(圖像緩沖區(qū))里存起來,然后Display(屏幕或顯示器)負責把buffer里的數(shù) 據(jù)呈現(xiàn)到屏幕上。
- 畫面撕裂
屏幕刷新頻是固定的,比如每16.6ms從buffer取數(shù)據(jù)顯示完一幀,理想情況下幀率和刷新頻率保持一致,即每繪制完成一 幀,顯示器顯示一幀。但是CPU/GPU寫數(shù)據(jù)是不可控的,所以會出現(xiàn)buffer里有些數(shù)據(jù)根本沒顯示出來就被重寫了,即 buffer里的數(shù)據(jù)可能是來自不同的幀的。當屏幕刷新時,此時它并不知道buffer的狀態(tài),因此從buffer抓取的幀并不是完整的一幀畫面,即出現(xiàn)畫面撕裂。
簡單說就是Display在顯示的過程中,buffer內數(shù)據(jù)被CPU/GPU修改,導致畫面撕裂。
那咋解決畫面撕裂呢? 答案是使用雙緩沖。
雙緩沖
由于圖像繪制和屏幕讀取 使用的是同個buffer,所以屏幕刷新時可能讀取到的是不完整的一幀畫面。
雙緩沖,讓繪制和顯示器擁有各自的buffer:GPU 始終將完成的一幀圖像數(shù)據(jù)寫入到 Back Buffer,而顯示器使用 Frame Buffer,當屏幕刷新時,F(xiàn)rame Buffer 并不會發(fā)生變化,當Back buffer準備就緒后,它們才進行交換。
VSync
什么時候進行兩個buffer的交換呢?
假如是 Back buffer準備完成一幀數(shù)據(jù)以后就進行,那么如果此時屏幕還沒有完整顯示上一幀內容的話,肯定是會出問題的。 看來只能是等到屏幕處理完一幀數(shù)據(jù)后,才可以執(zhí)行這一操作了。
當掃描完一個屏幕后,設備需要重新回到第一行以進入下一次的循環(huán),此時有一段時間空隙,稱為VerticalBlanking Interval(VBI)。這個時間點就是我們進行緩沖區(qū)交換的最佳時間。因為此時屏幕沒有在刷新,也就避免了交換過程中出現(xiàn)畫面撕裂的狀況。
VSync(垂直同步)是VerticalSynchronization的簡寫,它利用VBI時期出現(xiàn)的vertical sync pulse(垂直同步脈沖)來保證雙緩沖在最佳時間點才進行交換。另外,交換是指各自的內存地址,可以認為該操作是瞬間完成。
所以說VSync這個概念并不是Google首創(chuàng)的,它在早年的PC機領域就已經(jīng)出現(xiàn)了。
Android屏幕刷新機制
先總體概括一下,Android屏幕刷新使用的是“雙緩存+VSync機制”,單純的雙緩沖模式容易造成jank(丟幀)現(xiàn)象,為了解決這個問題,Google在 Android4.1 提出了Project Butter(?油工程),引入了 drawing with VSync 的概念。
jank(丟幀)
以時間的順序來看下將會發(fā)生的過程:
- Display顯示第0幀數(shù)據(jù),此時CPU和GPU渲染第1幀畫面,且在Display顯示下一幀前完成
- 因為渲染及時,Display在第0幀顯示完成后,也就是第1個VSync后,緩存進行交換,然后正常顯示第1幀
- 接著第2幀開始處理,是直到第2個VSync快來前才開始處理的。
- 第2個VSync來時,由于第2幀數(shù)據(jù)還沒有準備就緒,緩存沒有交換,顯示的還是第1幀。這種情況被Android開發(fā)組命名為“Jank”,即發(fā)生了丟幀。
- 當?shù)?幀數(shù)據(jù)準備完成后,它并不會?上被顯示,而是要等待下一個VSync 進行緩存交換再顯示。
所以總的來說,就是屏幕平白無故地多顯示了一次第1幀。 原因是第2幀的CPU/GPU計算 沒能在VSync信號到來前完成。
這里注意一下一個細節(jié),jank(丟幀、掉幀),不是說這一幀丟棄了不顯示,而是這一幀延遲顯示了,因為緩存交換的時機只能等下一個VSync了。
黃油計劃 —— drawing with VSync
為了優(yōu)化顯示性能,Google在Android 4.1系統(tǒng)中對Android Display系統(tǒng)進行了重構,實現(xiàn)了Project Butter(?油工程): 系統(tǒng)在收到VSync pulse后,將?上開始下一幀的渲染。即一旦收到VSync通知(16ms觸發(fā)一次),CPU和GPU 才立刻開 始計算然后把數(shù)據(jù)寫入buffer。如下圖:
CPU/GPU根據(jù)VSYNC信號同步處理數(shù)據(jù),可以讓CPU/GPU有完整的16ms時間來處理數(shù)據(jù),減少了jank。 一句話總結,VSync同步使得CPU/GPU充分利用了16.6ms時間,減少jank。
問題又來了,如果界面比較復雜,CPU/GPU的處理時間較?,超過了16.6ms呢?如下圖:
- 在第二個時間段內,但卻因 GPU 還在處理 B 幀,緩存沒能交換,導致 A 幀被重復顯示。
- 而B完成后,又因為缺乏VSync pulse信號,它只能等待下一個signal的來臨。于是在這一過程中,有一大段時間是被浪費的。
- 當下一個VSync出現(xiàn)時,CPU/GPU?上執(zhí)行操作(A幀),且緩存交換,相應的顯示屏對應的就是B。這時看起來就是正常的。只不過由于執(zhí)行時間仍然超過16ms,導致下一次應該執(zhí)行的緩沖區(qū)交換又被推遲了——如此循環(huán)反復,便出現(xiàn)了越來越多的“Jank”。
為什么 CPU 不能在第二個 16ms 處理繪制工作呢? 因為只有兩個 buffer,Back buffer正在被GPU用來處理B幀的數(shù)據(jù), Frame buffer的內容用于Display的顯示,這樣兩個 buffer都被占用,CPU 則無法準備下一幀的數(shù)據(jù)。 那么,如果再提供一個buffer,CPU、GPU 和顯示設備都能使用各自的 buffer工作,互不影響。這就是三緩沖的來源了。
三緩沖
三緩存就是在雙緩沖機制基礎上增加了一個 Graphic Buffer 緩沖區(qū),這樣可以最大限度的利用空閑時間,帶來的壞處是多使用的一個 Graphic Buffer 所占用的內存。
- 第一個Jank,是不可避免的。但是在第二個 16ms 時間段,CPU/GPU 使用 第三個 Buffer 完成C幀的計算,雖然還是 會多顯示一次 A 幀,但后續(xù)顯示就比較順暢了,有效避免 Jank 的進一步加劇。
- 注意在第3段中,A幀的計算已完成,但是在第4個vsync來的時候才顯示,如果是雙緩沖,那在第三個vynsc就可以顯示了。
三緩沖有效利用了等待VSync的時間,減少了jank,但是帶來了延遲。是不是 Buffer 越多越好呢?這個是否定的, Buffer 正常還是兩個,當出現(xiàn) Jank 后三個足以。
Choreographer
上邊講的都是基礎的刷新知識,那么在 Android 系統(tǒng)中,真正來實現(xiàn)繪制的類叫Choreographer。
Choreographer負責對CPU/GPU繪制的指導 —— 收到VSync信號才開始繪制,保證繪制擁有完整 16.6ms,避免繪制的隨機性。
通常 應用層不會直接使用Choreographer,而是使用更高級的API,例如動畫和View繪制相關的 ValueAnimator.start()、View.invalidate()等。
(這邊補充說一個面試題,屬性動畫更新時會回調onDraw嗎?不會,因為它內部是通過AnimationHandler中的Choreographer機制來實現(xiàn)的更新,具體的邏輯,如果以后有時間的話可以寫篇文章來說一說。)
業(yè)界一般通過Choreographer來監(jiān)控應用的幀率。
(這個東西也是個面試題,會問你如何檢測應用的幀率?你可以提一下Choreographer里面的FrameCallback,然后結合一些第三方庫的實現(xiàn)具體說一下。)
View刷新的入口
Activity啟動,走完onResume方法后,會進行window的添加。window添加過程會調用ViewRootImpl的setView()方法, setView()方法會調用requestLayout()方法來請求繪制布局,requestLayout()方法內部又會走到scheduleTraversals()方法。最后會走到performTraversals()方法,接著到了我們熟知的測量、布局、繪制三大流程了。
當我們使用 ValueAnimator.start()、View.invalidate()時,最后也是走到ViewRootImpl的 scheduleTraversals()方法。(View.invalidate()內部會循環(huán)獲取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals(),可自行查看源碼)
即所有UI的變化都是走到ViewRootImpl的scheduleTraversals()方法。
這里注意一個點:scheduleTraversals()之后不是立即就執(zhí)行performTraversals()的,它們中間隔了一個Choreographer機制。簡單來說就是scheduleTraversals()中,Choreographer會去請求native的VSync信號,VSync信號來了之后才會去調用performTraversals()方法進行View繪制的三大流程。
//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//添加同步屏障,屏蔽同步消息,保證VSync到來立即執(zhí)行繪制
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//mTraversalRunnable是TraversalRunnable實例,最終走到run(),也即doTraversal();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ...
//開始三大繪制流程
performTraversals();
...
}
}
復制代碼
- postSyncBarrier 開啟同步屏障,保證VSync到來后立即執(zhí)行繪制
- mChoreographer.postCallback()方法,發(fā)送一個會在下一幀執(zhí)行的回調,即在下一個VSync到來時會執(zhí)行 TraversalRunnable–>doTraversal()—>performTraversals()–>繪制流程。
Choreographer
初始化
mChoreographer,是在ViewRootImpl的構造方法內使用 Choreographer.getInstance()創(chuàng)建。
Choreographer和Looper一樣是線程單例的,通過ThreadLocal機制來保證唯一性。因為Choreographer內部通過FrameHandler來發(fā)送消息,所以初始化的時候會先判斷當前線程有無Looper,沒有的話直接拋異常。
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) {
//當前線程要有l(wèi)ooper,Choreographer實例需要傳入
throw new IllegalStateException("The current thread must have a looper!");
}
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};
復制代碼
postCallback
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法,第一個參數(shù)是CALLBACK_TRAVERSAL,表示回調任務的類型,共有以下5種類型:
//輸入事件,首先執(zhí)行
public static final int CALLBACK_INPUT = 0;
//動畫,第二執(zhí)行
public static final int CALLBACK_ANIMATION = 1;
//插入更新的動畫,第三執(zhí)行
public static final int CALLBACK_INSETS_ANIMATION = 2;
//繪制,第四執(zhí)行
public static final int CALLBACK_TRAVERSAL = 3;
//提交,最后執(zhí)行,
public static final int CALLBACK_COMMIT = 4;
復制代碼
五種類型任務對應存入對應的CallbackQueue中,每當收到 VSYNC 信號時,Choreographer 將首先處理 INPUT 類型的任 務,然后是 ANIMATION 類型,最后才是 TRAVERSAL 類型。
postCallback()內部調用postCallbackDelayed(),接著又調用postCallbackDelayedInternal(),正常消息執(zhí)行scheduleFrameLocked,延遲運行的消息會發(fā)送一個MSG_DO_SCHEDULE_CALLBACK類型的meessage:
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
...
synchronized (mLock) {
...
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) { //立即執(zhí)行
scheduleFrameLocked(now);
} else {
//延遲運行,最終也會走到scheduleFrameLocked()
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
復制代碼
FrameHandler這個類是內部專門用來處理消息的,可以看到延遲的MSG_DO_SCHEDULE_CALLBACK類型消息最終也是走到scheduleFrameLocked:
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:
// 執(zhí)行doFrame,即繪制過程
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
//申請VSYNC信號,例如當前需要繪制任務時
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
//需要延遲的任務,最終還是執(zhí)行上述兩個事件
doScheduleCallback(msg.arg1);
break;
}
}
}
void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}
復制代碼
申請VSync信號
scheduleFrameLocked()方法里面就會去真正的申請 VSync 信號了。
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//當前執(zhí)行的線程,是否是mLooper所在線程
if (isRunningOnLooperThreadLocked()) {
//申請 VSYNC 信號
scheduleVsyncLocked();
} else {
// 若不在,就用mHandler發(fā)送消息到原線程,最后還是調用scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);//異步
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
// 如果未開啟VSYNC則直接doFrame方法(4.1后默認開啟)
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);//異步
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
復制代碼
VSync信號的注冊和監(jiān)聽是通過mDisplayEventReceiver實現(xiàn)的。mDisplayEventReceiver是在Choreographer的構造方法中創(chuàng)建的,是FrameDisplayEventReceiver的實例。 FrameDisplayEventReceiver是 DisplayEventReceiver 的子類,
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
復制代碼
public DisplayEventReceiver(Looper looper, int vsyncSource) {
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mMessageQueue = looper.getQueue();
// 注冊native的VSYNC信號監(jiān)聽者
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,vsyncSource);
mCloseGuard.open("dispose");
}
復制代碼
VSync信號回調
native的VSync信號到來時,會走到onVsync()回調:
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
...
//將本身作為runnable傳入msg, 發(fā)消息后 會走run(),即doFrame(),也是異步消息
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);
}
}
復制代碼
(這里補充一個面試題:頁面UI沒有刷新的時候onVsync()回調也會執(zhí)行嗎?不會,因為VSync是UI需要刷新的時候主動去申請的,而不是native層不停地往上面去推這個回調的,這邊要注意。)
doFrame
doFrame()方法中會通過doCallbacks()方法去執(zhí)行各種callbacks,主要內容就是取對應任務類型的隊列,遍歷隊列執(zhí)行所有任務,其中就包括了 ViewRootImpl 發(fā)起的繪制任務mTraversalRunnable了。mTraversalRunnable執(zhí)行doTraversal()方法,移除同步屏障,調用performTraversals()開始三大繪制流程。
到這里整個流程就閉環(huán)了。
本文在開源項目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄,里面包含了Android組件化最全開源項目(美團App、得到App、支付寶App、微信App、蘑菇街App、有贊APP...)等,資源持續(xù)更新中...