如需轉(zhuǎn)載請?jiān)u論或簡信啄骇,并注明出處痴鳄,未經(jīng)允許不得轉(zhuǎn)載
目錄
前言
現(xiàn)在Android的應(yīng)用界面越來越復(fù)雜,很多時候頁面中還有各種動畫缸夹,所以頁面卡頓痪寻、掉幀等問題就隨之而來,所以就想研究一下屏幕刷新的原理虽惭,以便于更快的定位和解決問題
基本概念
Android的屏幕刷新中涉及到最重要的三個概念(為便于理解橡类,這里先做簡單介紹)
CPU:執(zhí)行應(yīng)用層的measure、layout芽唇、draw等操作顾画,繪制完成后將數(shù)據(jù)提交給GPU
GPU:進(jìn)一步處理數(shù)據(jù),并將數(shù)據(jù)緩存起來
屏幕:由一個個像素點(diǎn)組成匆笤,以固定的頻率(16.6ms研侣,即1秒60幀)從緩沖區(qū)中取出數(shù)據(jù)來填充像素點(diǎn)
總結(jié)一句話就是:CPU 繪制后提交數(shù)據(jù)、GPU 進(jìn)一步處理和緩存數(shù)據(jù)炮捧、最后屏幕從緩沖區(qū)中讀取數(shù)據(jù)并顯示
我們開發(fā)過程中主要關(guān)心CPU繪制部分庶诡,對GPU和屏幕基本不用關(guān)心。所以咆课,看到這里末誓,有的人可能就會想說扯俱,我對view的繪制流程(measure、layout基显、draw)已經(jīng)非常熟悉蘸吓,至于GPU和屏幕,和我也沒有太大關(guān)系吧撩幽。其實(shí)這里面還有更多的細(xì)節(jié)值得我們?nèi)ヌ剿骺饧蹋私夂驼莆樟诉@些細(xì)節(jié),有助于我們解決一些實(shí)際開發(fā)過程中的問題窜醉,我們不妨一步步往下看
雙緩沖機(jī)制
看完上面的流程圖宪萄,我們很容易想到一個問題,屏幕是以16.6ms的固定頻率進(jìn)行刷新的榨惰,但是我們應(yīng)用層觸發(fā)繪制的時機(jī)是完全隨機(jī)的(比如我們隨時都可以觸摸屏幕觸發(fā)繪制)拜英,如果在GPU向緩沖區(qū)寫入數(shù)據(jù)的同時,屏幕也在向緩沖區(qū)讀取數(shù)據(jù)琅催,會發(fā)生什么情況呢居凶?有可能屏幕上就會出現(xiàn)一部分是前一幀的畫面,一部分是另一幀的畫面藤抡,這顯然是無法接受的侠碧,那怎么解決這個問題呢?
這個其實(shí)和我們平時使用代碼管理工具Git的一些思路有相似之處缠黍,首先我們有一個master分支弄兜,對應(yīng)線上版本的代碼,當(dāng)有新的需求來的時候瓷式,我們往往不會在master分支上直接進(jìn)行開發(fā)替饿,都會拉出一個新的分支,比如develop分支贸典,在develop分支上開發(fā)新需求视卢,等開發(fā)完成測試通過后才會合并到master分支
所以,在屏幕刷新中廊驼,Android系統(tǒng)引入了雙緩沖機(jī)制腾夯。GPU只向Back Buffer中寫入繪制數(shù)據(jù),且GPU會定期交換Back Buffer和Frame Buffer蔬充,也就是讓Back Buffer 變成Frame Buffer交給屏幕進(jìn)行繪制蝶俱,讓原先的Frame Buffer變成Back Buffer進(jìn)行數(shù)據(jù)寫入。交換的頻率也是60次/秒饥漫,這就與屏幕的刷新頻率保持了同步
雖然我們引入了雙緩沖機(jī)制榨呆,但是我們知道,當(dāng)布局比較復(fù)雜庸队,或設(shè)備性能較差的時候积蜻,CPU并不能保證在16.6ms內(nèi)就完成繪制數(shù)據(jù)的計(jì)算闯割,所以這里系統(tǒng)又做了一個處理
當(dāng)你的應(yīng)用正在往Back Buffer中填充數(shù)據(jù)時,系統(tǒng)會將Back Buffer鎖定竿拆。如果到了GPU交換兩個Buffer的時間點(diǎn)宙拉,你的應(yīng)用還在往Back Buffer中填充數(shù)據(jù),GPU會發(fā)現(xiàn)Back Buffer被鎖定了丙笋,它會放棄這次交換
這樣做的后果就是手機(jī)屏幕仍然顯示原先的圖像谢澈,這就是我們常常說的丟幀,所以為了避免丟幀的發(fā)生御板,我們就要盡量減少布局層級锥忿,減少不必要的View的invalidate調(diào)用,減少大量對象的創(chuàng)建(GC也會占用CPU時間)等等怠肋。對這方面有興趣的可以看我的性能優(yōu)化專題下的文章
Choreographer
我們看下面這張圖敬鬓,這里已經(jīng)是基于雙緩沖機(jī)制,且應(yīng)用層的優(yōu)化已經(jīng)做得非常好笙各,繪制時間均少于16.6ms钉答,但依然出現(xiàn)了丟幀,為什么呢杈抢?
原因是第2幀雖然繪制時間少于16.6ms希痴,但是繪制開始的時間距離vsync信號(就是一個發(fā)起屏幕刷新的信號,Vertical Synchronization的縮寫)發(fā)出的時間比較短暫春感,導(dǎo)致當(dāng)vsync信號來的時候,第2幀還沒有繪制完成虏缸,所以Back Buffer依然是鎖定的狀態(tài)鲫懒,也就出現(xiàn)了丟幀
如果我們可以保證每次繪制開始的時間和vsync信號發(fā)起的時間一致(如下圖所示),是不是就可以解決這個問題呢刽辙?
Android在每一幀中實(shí)際上只是在完成三個操作窥岩,分別是輸入(Input)、動畫(Animation)宰缤、繪制(Draw)颂翼。在Android4.1(API 16)之后,Android系統(tǒng)開始加入Choreographer
這個類慨灭,這個類名翻譯過來是“舞蹈指導(dǎo)”朦乏,字面上的意思就是指揮以上三個UI操作一起完成一支舞蹈。這個類就可以解決vsync和繪制不同步的問題氧骤,其實(shí)它的原理用一句話總結(jié)就是往Choreographer里發(fā)一個消息呻疹,最快也要等到下一個vsync信號來的時候才會開始處理消息
下面我們通過源碼分析來看看Choreographer
的實(shí)現(xiàn)原理
Activity中的布局首次繪制,以及每次調(diào)用View 的 invalidate()
時筹陵,都會調(diào)用到ViewRootImp#requestLayout()
刽锤,對于這塊不是很清楚的具體可以看最全的View繪制流程(上)— Window镊尺、DecorView、ViewRootImp的關(guān)系并思,所以我們接下來分析一下ViewRootImp#requestLayout()
里面做了什么
ViewRootImp#requestLayout()
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//檢查是否是主線程庐氮,不然會拋出異常
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
ViewRootImp#scheduleTraversals()
如果在同一幀中出現(xiàn)多次requestLayout()
調(diào)用,其實(shí)最終也只會繪制一次宋彼,為什么呢弄砍?我們可以看到下面有個mTraversalScheduled
標(biāo)志位,稍后我們可以看看這個標(biāo)志位是哪里被置為false的
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//添加同步消息屏障宙暇,這個方法也比較關(guān)鍵输枯,這里先不關(guān)心,我們說完Choreographer再分析
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//向Choreographer中發(fā)送消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//...
}
}
Choreographer#postCallbackDelayedInternal()
mChoreographer.postCallback()
接著會調(diào)用這個方法
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//將消息以當(dāng)前的時間戳放進(jìn)mCallbackQueue 隊(duì)列里
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//如果沒有設(shè)置消息延時占贫,直接執(zhí)行
scheduleFrameLocked(now);
} else {
//消息延時桃熄,但是最終依然會調(diào)用scheduleFrameLocked
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
Choreographer#scheduleFrameLocked()
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
//如果當(dāng)前線程是Choreographer的工作線程,我理解就是主線程
scheduleVsyncLocked();
} else {
//否則發(fā)一條消息到主線程
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
//設(shè)置消息為異步消息型奥,其實(shí)就是一個標(biāo)志位瞳收,具體作用我們后面會講
msg.setAsynchronous(true);
//插到消息隊(duì)列頭部,可以理解為設(shè)置最高優(yōu)先級
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);
}
}
}
接下來最終會調(diào)用到一個native方法
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
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);
}
}
@FastNative
private static native void nativeScheduleVsync(long receiverPtr);
native方法我們在Android Studio中不能直接查看厢汹,這里我們換一種思路螟深。前面Choreographer#postCallbackDelayedInternal()
方法中,我們看到了將消息以當(dāng)前的時間戳放進(jìn)隊(duì)列里烫葬,那消息什么時候被取出來執(zhí)行呢界弧?
CallbackQueue
private final class CallbackQueue {
private CallbackRecord mHead;
public boolean hasDueCallbacksLocked(long now) {
return mHead != null && mHead.dueTime <= now;
}
//這就是取出消息的方法
public CallbackRecord extractDueCallbacksLocked(long now) {
CallbackRecord callbacks = mHead;
if (callbacks == null || callbacks.dueTime > now) {
return null;
}
CallbackRecord last = callbacks;
CallbackRecord next = last.next;
while (next != null) {
if (next.dueTime > now) {
last.next = null;
break;
}
last = next;
next = next.next;
}
mHead = next;
return callbacks;
}
//添加消息
public void addCallbackLocked(long dueTime, Object action, Object token) {...}
//刪除消息
public void removeCallbacksLocked(Object action, Object token) {...}
}
跟蹤代碼發(fā)現(xiàn),這個CallbackQueue#extractDueCallbacksLocked()
會被Choreographer#doCallbacks()
調(diào)用搭综,Choreographer#doCallbacks()
又會被Choreographer#doFrame()
調(diào)用垢箕,最終我們跟到了FrameDisplayEventReceiver
類
FrameDisplayEventReceiver
因?yàn)樯厦娴膎ative方法我們沒有跟進(jìn)去分析,擔(dān)心給大家繞暈了兑巾,我們會用一個新的章節(jié)來分析native層做的事情条获,這里先直接給出結(jié)論
nativeScheduleVsync()
會向SurfaceFlinger
注冊Vsync信號的監(jiān)聽,VSync信號由SurfaceFlinger
實(shí)現(xiàn)并定時發(fā)送蒋歌,當(dāng)Vsync信號來的時候就會回調(diào)FrameDisplayEventReceiver#onVsync()
帅掘,這個方法給發(fā)送一個帶時間戳Runnable
消息,這個Runnable
消息的run()
實(shí)現(xiàn)就是FrameDisplayEventReceiver# run()
堂油, 接著就會執(zhí)行doFrame()
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);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
scheduleVsync();
return;
}
long now = System.nanoTime();
if (timestampNanos > now) {
timestampNanos = now;
}
if (mHavePendingVsync) {
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
//設(shè)置異步消息
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
doFrame()
會計(jì)算當(dāng)前時間與時間戳的間隔修档,間隔越大表示這一幀處理的時間越久,如果間隔超過一個周期府框,就會去計(jì)算跳過了多少幀萍悴,并打印出一個日志,這個日志我想很多人可能都見過
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
最終doFrame()
會從mCallbackQueue
中取出消息并按照時間戳順序調(diào)用mTraversalRunnable
的run()
函數(shù),mTraversalRunnable
就是最初被加入到Choreographer
中的Runnable()
//ViewRootImp
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
TraversalRunnable
doTraversal()
中就會開始我們View的繪制流程癣诱,View的繪制流程不是本文的重點(diǎn)计维,感興趣的可以看最全的View繪制流程(下)— Measure、Layout撕予、Draw
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
ViewRootImp#doTraversal()
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步消息屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
到此為止鲫惶,從觸發(fā)繪制到屏幕真正開始繪制的過程就基本講完了,但是這里還有最后一個細(xì)節(jié)沒有進(jìn)行分析
同步消息屏障
還記不記得前面說有mHandler.getLooper().getQueue().postSyncBarrier()
這個方法還沒有進(jìn)行分析实抡,這個方法的作用是什么呢欠母?
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//☆
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//向Choreographer中發(fā)送消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//...
}
}
我們知道,Android是基于消息機(jī)制的吆寨,每一個操作都是一個Message
赏淌,如果在觸發(fā)繪制的時候,消息隊(duì)列中還有很多消息沒有被執(zhí)行啄清,那是不是意味著要等到消息隊(duì)列中的消息執(zhí)行完成后六水,繪制消息才能被執(zhí)行到,那么依然無法保證Vsync信號和繪制的同步辣卒,所以依然可能出現(xiàn)丟幀的現(xiàn)象
還記不記得我們之前在Choreographer#scheduleFrameLocked()
和FrameDisplayEventReceiver#onVsync()
中提到掷贾,我們會給與Message
有關(guān)的繪制請求設(shè)置成異步消息(msg.setAsynchronous(true)
),為什么要這么做呢荣茫?這時候MessageQueue#postSyncBarrier()
就發(fā)揮它的作用了想帅,簡單來說,它的作用就是一個同步消息屏障啡莉,能夠把我們的異步消息(也就是繪制消息)的優(yōu)先級提到最高
MessageQueue#postSyncBarrier()
主線程的 Looper
會一直循環(huán)調(diào)用 MessageQueue
的 next()
來取出隊(duì)頭的 Message
執(zhí)行港准,當(dāng) Message
執(zhí)行完后再去取下一個。當(dāng) next()
方法在取 Message
時發(fā)現(xiàn)隊(duì)頭是一個同步屏障的消息時咧欣,就會去遍歷整個隊(duì)列浅缸,只尋找設(shè)置了異步標(biāo)志的消息,如果有找到異步消息该押,那么就取出這個異步消息來執(zhí)行,否則就讓 next()
方法陷入阻塞狀態(tài)阵谚。如果 next()
方法陷入阻塞狀態(tài)蚕礼,那么主線程此時就是處于空閑狀態(tài)的,也就是沒在干任何事梢什。所以奠蹬,如果隊(duì)頭是一個同步屏障的消息的話,那么在它后面的所有同步消息就都被攔截住了嗡午,直到這個同步屏障消息被移除囤躁,否則主線程就一直不會去處理同步屏障后面的同步消息
那這么同步屏障是什么時候被移除的呢?
其實(shí)我們就是在我們上面提到的ViewRootImp#doTraversal()
方法中
總結(jié)
本文講了屏幕刷新的基本原理,以及雙緩沖機(jī)制狸演、Choreographer的作用言蛇、同步消息屏障,不同的地方出了問題都可能引起丟幀宵距,所以了解這些細(xì)節(jié)有助于我們更好的排查項(xiàng)目開發(fā)過程中的問題腊尚,最后,來梳理一下屏幕刷新的流程圖