丟幀和卡頓
卡頓,是字面意思上來講间景,就是畫面不流暢佃声,即頁面刷新不連貫。Android系統(tǒng)默認的頁面刷新頻率是60幀倘要,每秒刷新60次圾亏,即屏幕上的畫面16.6ms刷新一次,這個頻率是由手機設備的屏幕硬件來控制的。如果16.6ms沒有完成一次刷新碗誉,就造成了丟幀召嘶。大部分的App偶爾間歇性地出現(xiàn)幾次丟幀,不會造成明顯的卡頓哮缺,只有連續(xù)或者短時間多次出現(xiàn)丟幀弄跌,就會讓用戶感覺到明顯的卡頓現(xiàn)象。
UI渲染機制
手機屏幕由一個個的像素點組成尝苇,如1920x1080分辨率的屏幕就表示在屏幕上橫向每行有1080個像素點铛只,縱向每列有1920個像素點。也可以想象成二維數(shù)組糠溜。數(shù)組中的每個數(shù)值淳玩,代表對應的像素點顯示的顏色。屏幕畫面每隔16.6ms刷新一次非竿,也就是更新每個像素點要顯示的顏色蜕着。而這個16.6ms的刷新頻率,也就是60FPS,是由手機屏幕硬件來控制的承匣。
同時可能會有多個Surface习蓬,它們可能來自不同的應用,也可能是同一個應用里面類似SurfaceView和TextureView措嵌,它們也都有自己單獨的Surface躲叼。屏幕刷新時,SufaceFlinger從每一個Surface的Front Buffer中企巢,拿到要顯示的內(nèi)容枫慷。然后將所有Surface要顯示的內(nèi)容,統(tǒng)一交給Hardware Composer浪规,它會根據(jù)位置或听,Z-Order順序等信息合成最終要顯示顯示在屏幕上的內(nèi)容,而這個內(nèi)容就會交給系統(tǒng)的幀緩沖區(qū)Frame Buffer來顯示笋婿。也就是說誉裆,系統(tǒng)根據(jù)Frame Buffer中的內(nèi)容來確定每一個像素點在每一幀要顯示的顏色。
至此缸濒,我們了解了Frame Buffer的作用足丢,并由此引出了Surface,SurfaceFlinger庇配,Graphic Buffer等圖形組件的概念斩跌。這里引用一篇文章中的比喻來讓大家對整個圖形繪制系統(tǒng)的整體架構有個大概的了解,然后再進一步深入了解捞慌。
如果把應用程序的頁面渲染過程當做是一次繪畫過程耀鸦,那么繪畫過程中Android的各個圖形組件的作用是:
- 畫筆:Skia或者OpenGL。我們可以用Skia畫筆來繪制2D圖形啸澡,也可以用OpenGL畫筆來繪制2D/3D圖形袖订。前者使用CPU繪制萝快,或者使用GPU繪制。
- 畫布:Surface著角。所有的內(nèi)容元素都在Surface這張畫紙上進行繪制和渲染揪漩。在Android中首昔,Window是View的容器哮内,每一個Window都會關聯(lián)一個Surface。而WindowManager負責管理這些Window啡邑,并把他們的數(shù)據(jù)傳遞給SurfaceFlinger产徊。
- 畫板:Graphic Buffer昂勒。Graphic Buffer緩沖用于應用程序的頁面繪制,在Android4.1之前使用的是雙緩沖機制舟铜。Android4.1之后戈盈,使用的是三緩沖機制。
顯示:SurfaceFlinger谆刨。它將WindowManager提供的所有Surface塘娶,通過硬件合成器Hardware Composer合成并輸出到顯示屏。
CPU和GPU
整個UI渲染機制主要依賴三個硬件:CPU痊夭、GPU和屏幕刁岸。上面已經(jīng)介紹過了。下圖則展示了在UI渲染過程中她我,CPU和GPU分別負責的工作虹曙。UI組件繪制到屏幕之前,都需要經(jīng)過柵格化(Rasterization)操作番舆,而柵格化操作又是一個相對耗時的操作酝碳。相比于CPU,GPU更擅長處理圖形運算恨狈,可以加快柵格化過程疏哗。這也是通常所說的硬件加速繪制的原理,將CPU不擅長的圖形計算轉(zhuǎn)換為GPU的專有指令來處理拴事。
硬件加速繪制
在軟件繪制過程中娩嚼,由于CPU對圖形計算的處理不是那么高效蘑险,這個過程完全沒有利用到GPU在圖形處理方面的優(yōu)勢。所以在Android 3.0之后岳悟,Android開始支持硬件加速繪制佃迄。到Android 4.0時,系統(tǒng)會默認開啟硬件加速贵少。Peoject Butter
Google 在 2012 年的 I/O 大會上宣布了Project Butter黃油計劃争占,在Android4.1上燃逻,對Android Display系統(tǒng)進行了重構,引入了三個核心要素:VSYNC臂痕、Tripe Buffer和Choreographer伯襟。
VSYNC
VSYNC信號,可以理解為一種定時中斷握童,是一種在PC上已經(jīng)很早就廣泛使用的技術姆怪。由系統(tǒng)底層控制VSYNC信號的發(fā)送頻率。由于大部分屏幕硬件的刷新頻率都是60FPS,所以VSYNC信號的發(fā)送頻率也是60FPS稽揭,即系統(tǒng)底層16.6ms發(fā)出一個VSYNC信號俺附。每次收到VSYNC信號,CPU會立即開始計算需要繪制的數(shù)據(jù)(可以理解為執(zhí)行View的measure溪掀、layout事镣、draw的過程),這也是應用的頁面下一幀繪制的開始揪胃,然后由GPU對數(shù)據(jù)進行柵格化并填充到Offscreen Buffer蛮浑。收到VSYNC信號的同時,SurfaceFlinger開始收集每一個Surface的Front Buffer中的內(nèi)容只嚣,交給Hardware Composer進行合成并輸出到Frame Buffer沮稚,最終完成屏幕刷新。也就是說系統(tǒng)每次發(fā)出VSYNC信號時册舞,CPU開始進行下一幀繪制的準備蕴掏,最終由GPU完成下一幀顯示內(nèi)容的繪制處理。而SurfaceFlinger則完成的是當前這一幀內(nèi)容的顯示操作调鲸。這就是雙緩沖機制的原理盛杰。Tripe Buffer
Tripe Buffer,即三重緩沖機制藐石,三個Graphic Buffer即供。如果你理解了雙緩沖機制的原理,就可以想象一下這樣一個問題于微。當前頁面正在顯示Front Buffer的內(nèi)容逗嫡,GPU正在往Offscreen Buffer填充下一幀的顯示內(nèi)容,而在GPU進行這一操作時株依,會將Offscreen Buffer鎖定驱证,如果CPU和GPU的繪制處理過程耗時太長,超過了一個VSYNC信號周期恋腕,就會導致本該進行Swap Buffer操作時抹锄,因為Offscreen Buffer被鎖定,無法正常進行Swap Buffer荠藤,從而導致Front Buffer里的內(nèi)容也不能更新伙单,還是保留上一幀的內(nèi)容。從而出現(xiàn)丟幀現(xiàn)象哈肖。ChoreoGrapher
ChoreoGrapher,主要作用是接受VSYNC信號器罐。系統(tǒng)發(fā)出VSYNC信號的頻率是60FPS梢为,那是不是意味著,不管App的頁面是否需要刷新轰坊,都會接收VSYNC信號铸董,然后開始由CPU準備下一幀繪制需要的數(shù)據(jù)呢?其實不然肴沫,如果App的頁面不需要刷新粟害,App就不會接收到VSYNC信號。只有當頁面需要刷新時颤芬,才會由ChoreoGrapher來執(zhí)行一個類似注冊監(jiān)聽VSYNC信號的操作悲幅,然后當系統(tǒng)下一次發(fā)出VSYNC信號時,ChoreoGrapher就會接收到VSYNC信號站蝠,來執(zhí)行頁面繪制的相關工作汰具。可能這樣的說法有點抽象菱魔,接下來通過介紹ChoreoGrapher的相關源碼來解釋說明它的作用留荔。
了解View的繪制機制的同學都知道,當頁面有視圖變化澜倦,需要刷新時聚蝶,都會執(zhí)行到ViewRootImpl的requestLayoiut方法,很多介紹View繪制原理的文章肥隆,都以requestLayout方法作為頁面繪制過程真正的開始點既荚。它內(nèi)部又會調(diào)用scheduleTraversals方法。
//ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
//mLayoutRequested用來輔助判斷是否需要執(zhí)行View的measure和layout過程
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//向主線程的Messagequeue中添加同步屏障栋艳,目的是讓頁面繪制的相關任務能盡快執(zhí)行
//頁面繪制的相關任務是以異步消息的方式發(fā)到主線程,在添加同步屏障之后句各,異步消息的任務將優(yōu)先執(zhí)行
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
在scheduleTraversals方法中吸占,我們看到了mChoreographer。此處調(diào)用了它的postCallback方法凿宾。第一個參數(shù)表示回調(diào)類型矾屯,第二個參數(shù)表示一個待執(zhí)行的任務。
//Choreographer.java
@TestApi
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
@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");
}
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;
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);
}
}
}
如上述代碼所示初厚,經(jīng)過層層調(diào)用件蚕,完成實際工作的是postCallbackDelayedInternal方法孙技,主要完成兩件事:
第一,將postCallback方法傳進來的待執(zhí)行任務排作,封裝成一個Callback牵啦,保存在mCallbackQueues隊列中,mCallbackQueues是CallbackQueue類型的數(shù)組妄痪。CallbackQueue是Choreographer的內(nèi)部類哈雏,其實質(zhì)是一個單向鏈表,其中的每一個Callback按dueTime的先后排序衫生。
//Choreographer#CallbackQueue
@UnsupportedAppUsage
public void addCallbackLocked(long dueTime, Object action, Object token) {
CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
CallbackRecord entry = mHead;
if (entry == null) {
mHead = callback;
return;
}
if (dueTime < entry.dueTime) {
callback.next = entry;
mHead = callback;
return;
}
while (entry.next != null) {
if (dueTime < entry.next.dueTime) {
callback.next = entry.next;
break;
}
entry = entry.next;
}
entry.next = callback;
}
第二件事就是向主線程中添加一個MSG_DO_SCHEDULE_VSYNC類型的異步消息裳瘪,根據(jù)duetime的時序判斷是直接添加還是在指定的dutime時間添加,而添加MSG_DO_SCHEDULE_VSYNC消息的工作就是在scheduleFrameLocked方法中完成
//Choreographer.java
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
// mFrameScheduled保證16ms內(nèi)罪针,只會申請一次垂直同步信號
// scheduleFrameLocked可以被調(diào)用多次彭羹,但是mFrameScheduled保證下一個vsync到來之前,不會有新的請求發(fā)出
// 多余的scheduleFrameLocked調(diào)用被無效化
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);
}
}
}
首先泪酱,需要注意的是mFrameScheduled派殷,它是當前Choreographer是否正在監(jiān)聽VSYNC的標識,同時也能防止重復監(jiān)聽西篓,Choreographer每次接收到VSYNC信號時愈腾,將mFrameScheduled置為false,當需要監(jiān)聽VSYNC信號時岂津,再將mFrameScheduled置為true虱黄。這就意味著Choreographer每次接收到VSYNC信號,處理完后續(xù)邏輯吮成,之后監(jiān)聽下一個VSYNC信號時橱乱,需要重新注冊。
根據(jù)代碼中的注釋粱甫,方法開頭的USE_VSYNC用來區(qū)分是否啟用了vysnc機制泳叠,默認為true。我們主要關注的也是USE_VSYNC為true的情況茶宵。同樣根據(jù)代碼中的注釋危纫,如果當前線程是主線程就直接執(zhí)行scheduleVsyncLocked方法,否則就通過異步消息的方式乌庶,讓主線程執(zhí)行scheduleVsyncLocked方法
ChoreoGrapher.java
@UnsupportedAppUsage
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
mDisplayEventReceiver是抽象類DisplayEventReceiver的對象种蝶。它是來定義SYNC信號的接收器。它的主要功能有兩個:“注冊”VSYNC信號的監(jiān)聽和接收VSYNC信號瞒大。它的scheduleVsync方法內(nèi)部會調(diào)用nativeScheduleVsync方法螃征,這個native方法是最終實現(xiàn)VSYNC信號監(jiān)聽的"注冊"。這里不再對nativeScheduleVsync進一步分析透敌,所以這里說的注冊盯滚,并不一定和Android的BroadcasrReceiver機制的注冊一個概念踢械。有興趣的同學可以繼續(xù)深入到native方法中進一步了解。
@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 {
nativeScheduleVsync(mReceiverPtr);
}
}
ChoreoGrapher的內(nèi)部類FrameDisplayEventReceiver魄藕,繼承自DisplayEventReceiver内列,重寫了onVsync方法。而從DisplayEventReceiver源碼的注釋可以得知泼疑,onVsync方法就是接收到VSYNC信號的回調(diào)方法德绿。由于FrameDisplayEventReceiver實現(xiàn)了Runnable接口,可以將其當做是一個可執(zhí)行任務退渗。而FrameDisplayEventReceiver的onVsync方法移稳,就做了一件事,就是向主線程發(fā)送異步消息会油,在主線程中執(zhí)行它的run方法个粱。
//Choreographer#FrameDisplayEventReceiver
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);
}
// TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
// the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
// for the internal display implicitly.
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
// 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);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
如上述代碼所示,在主線程中通過執(zhí)行FrameDisplayEventReceiver的run方法翻翩,也就是執(zhí)行Choreographer的doFrame方法都许,其中需要重點關注的相關代碼如下
//Choreographer.java
@UnsupportedAppUsage
void doFrame(long frameTimeNanos, int frame) {
...
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);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
...
}
這個Choreographer.CALLBACK_TRAVERSAL很眼熟,在ViewRootImpl的schduleTraversals方法中調(diào)用Choreographer的postCallback方法時傳入的第一個參數(shù)也是Choreographer.CALLBACK_TRAVERSAL嫂冻。
//Choreographer.java
void doCallbacks(int callbackType, long frameTimeNanos) {
...
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
...
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);
}
....
}
在doCallbacks方法中胶征,根據(jù)callbackType也就是Choreographer.CALLBACK_TRAVERSAL。取出對應的CallbackQueue桨仿。在schduleTraversals調(diào)用postCallback方法時傳入的第二個參數(shù)mTraversalRunnable睛低,就保存在上述代碼的某一個CallbackRecord中,查看CallbackRecord的run方法服傍,其實就是調(diào)用了封裝在其中的action的run方法钱雷。從以上分析,這里的action吹零,就是在mTraversalRunnable罩抗。分析到這一步,就可以回到ViewRootImpl的代碼中了灿椅。
//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
mTraversalRunnable的run方法套蒂,調(diào)用了doTraversal方法。到此茫蛹,熟悉View的繪制過程的同學泣懊,應該就很清楚接下來的流程了。doTraversal內(nèi)部會調(diào)用performTraversals方法麻惶。在performTraversals方法中會,執(zhí)行當前頁面窗口對應的DecorView的整個視圖層級的繪制流程信夫。
//ViewRootImpl.java
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
總結(jié)
本文主要介紹了
- 卡頓和丟幀的概念
- UI渲染機制的原理窃蹋,其中涉及到的重要組件是Canvas卡啰、Surface、Graphic Buffer警没、和SurfaceFlinger匈辱。
- 軟件繪制和硬件加速繪制的概念和區(qū)別。CPU和GPU在繪制渲染過程中杀迹,各自完成的工作亡脸。以及Skia庫和OpenGL ES庫的簡單介紹。
- Project Butter的三要素树酪,VSYNC信號和Triple Buffer的原理浅碾,著重從源碼角度分析了Choreographer的工作機制。
本文參考
Android開發(fā)高手課 UI 優(yōu)化(上):UI 渲染的幾個關鍵概念
Android 屏幕刷新機制