Android的VSYNC機(jī)制和UI刷新流程示例詳解
前言
屏幕刷新幀率不穩(wěn)定,掉幀嚴(yán)重,無(wú)法保證每秒60幀,導(dǎo)致屏幕畫面撕裂;
今天我們來(lái)講解下VSYNC機(jī)制和UI刷新流程
一、 Vsync信號(hào)詳解
1、屏幕刷新相關(guān)知識(shí)點(diǎn)
- 屏幕刷新頻率: 一秒內(nèi)屏幕刷新的次數(shù)(一秒內(nèi)顯示了多少幀的圖像),單位 Hz(赫茲),如常見(jiàn)的 60 Hz。刷新頻率取決于硬件的固定參數(shù)(不會(huì)變的);
- 逐行掃:顯示器并不是一次性將畫面顯示到屏幕上,而是從左到右邊,從上到下逐行掃描,順序顯示整屏的一個(gè)個(gè)像素點(diǎn),不過(guò)這一過(guò)程快到人眼無(wú)法察覺(jué)到變化。以 60 Hz 刷新率的屏幕為例,這一過(guò)程即 1000 / 60 ≈ 16ms;
- 幀率: 表示 GPU 在一秒內(nèi)繪制操作的幀數(shù),單位 fps。例如在電影界采用 24 幀的速度足夠使畫面運(yùn)行的非常流暢。而 Android 系統(tǒng)則采用更加流程的 60 fps,即每秒鐘GPU最多繪制 60 幀畫面。幀率是動(dòng)態(tài)變化的,例如當(dāng)畫面靜止時(shí),GPU 是沒(méi)有繪制操作的,屏幕刷新的還是buffer中的數(shù)據(jù),即GPU最后操作的幀數(shù)據(jù);
- 屏幕流暢度:即以每秒60幀(每幀16.6ms)的速度運(yùn)行,也就是60fps,并且沒(méi)有任何延遲或者掉幀;
- FPS:每秒的幀數(shù);
- 丟幀:在16.6ms完成工作卻因各種原因沒(méi)做完,占了后n個(gè)16.6ms的時(shí)間,相當(dāng)于丟了n幀;
2、VSYNC機(jī)制
VSync機(jī)制: Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào),觸發(fā)對(duì)UI進(jìn)行渲染,VSync是Vertical Synchronization(垂直同步)的縮寫,是一種在PC上很早就廣泛使用的技術(shù),可以簡(jiǎn)單的把它認(rèn)為是一種定時(shí)中斷。而在Android 4.1(JB)中已經(jīng)開始引入VSync機(jī)制;

VSync機(jī)制下的繪制過(guò)程;CPU/GPU接收vsync信號(hào),Vsync每16ms一次,那么在每次發(fā)出Vsync命令時(shí),CPU都會(huì)進(jìn)行刷新的操作。也就是在每個(gè)16ms的第一時(shí)間,CPU就會(huì)響應(yīng)Vsync的命令,來(lái)進(jìn)行數(shù)據(jù)刷新的動(dòng)作。CPU和GPU的刷新時(shí)間,和Display的FPS是一致的。因?yàn)橹挥械桨l(fā)出Vsync命令的時(shí)候,CPU和GPU才會(huì)進(jìn)行刷新或顯示的動(dòng)作。CPU/GPU接收vsync信號(hào)提前準(zhǔn)備下一幀要顯示的內(nèi)容,所以能夠及時(shí)準(zhǔn)備好每一幀的數(shù)據(jù),保證畫面的流暢;

可見(jiàn)vsync信號(hào)沒(méi)有提醒CPU/GPU工作的情況下,在第一個(gè)16ms之內(nèi),一切正常。然而在第二個(gè)16ms之內(nèi),幾乎是在時(shí)間段的最后CPU才計(jì)算出了數(shù)據(jù),交給了Graphics Driver,導(dǎo)致GPU也是在第二段的末尾時(shí)間才進(jìn)行了繪制,整個(gè)動(dòng)作延后到了第三段內(nèi)。從而影響了下一個(gè)畫面的繪制。這時(shí)會(huì)出現(xiàn)Jank(閃爍,可以理解為卡頓或者停頓)。這時(shí)候CPU和GPU可能被其他操作占用了,這就是卡頓出現(xiàn)的原因;
二、UI刷新原理流程
1、VSYNC流程示意
當(dāng)我們通過(guò)setText改變TextView內(nèi)容后,UI界面不會(huì)立刻改變,APP端會(huì)先向VSYNC服務(wù)請(qǐng)求,等到下一次VSYNC信號(hào)觸發(fā)后,APP端的UI才真的開始刷新,基本流程如下:

setText最終調(diào)用invalidate申請(qǐng)重繪,最后會(huì)通過(guò)ViewParent遞歸到ViewRootImpl的invalidate,請(qǐng)求VSYNC,在請(qǐng)求VSYNC的時(shí)候,會(huì)添加一個(gè)同步柵欄,防止UI線程中同步消息執(zhí)行,這樣做為了加快VSYNC的響應(yīng)速度,如果不設(shè)置,VSYNC到來(lái)的時(shí)候,正在執(zhí)行一個(gè)同步消息;
2、view的invalidate
View會(huì)遞歸的調(diào)用父容器的invalidateChild,逐級(jí)回溯,最終走到ViewRootImpl的invalidate
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
ViewRootImpl.java
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
ViewRootImpl會(huì)調(diào)用scheduleTraversals準(zhǔn)備重繪,但是,重繪一般不會(huì)立即執(zhí)行,而是往Choreographer的Choreographer.CALLBACK_TRAVERSAL隊(duì)列中添加了一個(gè)mTraversalRunnable,同時(shí)申請(qǐng)VSYNC,這個(gè)mTraversalRunnable要一直等到申請(qǐng)的VSYNC到來(lái)后才會(huì)被執(zhí)行;
3、scheduleTraversals
ViewRootImpl.java
// 將UI繪制的mTraversalRunnable加入到下次垂直同步信號(hào)到來(lái)的等待callback中去
// mTraversalScheduled用來(lái)保證本次Traversals未執(zhí)行前,不會(huì)要求遍歷兩邊,浪費(fèi)16ms內(nèi),不需要繪制兩次
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 防止同步柵欄,同步柵欄的意思就是攔截同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// postCallback的時(shí)候,順便請(qǐng)求vnsc垂直同步信號(hào)scheduleVsyncLocked
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
<!--添加一個(gè)處理觸摸事件的回調(diào),防止中間有Touch事件過(guò)來(lái)-->
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
4、申請(qǐng)VSYNC同步信號(hào)
Choreographer知識(shí)點(diǎn)在上個(gè)文章詳細(xì)介紹過(guò);
Choreographer.java
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
<!--申請(qǐng)VSYNC同步信號(hào)-->
scheduleFrameLocked(now);
}
}
}
5、scheduleFrameLocked
// mFrameScheduled保證16ms內(nèi),只會(huì)申請(qǐng)一次垂直同步信號(hào)
// scheduleFrameLocked可以被調(diào)用多次,但是mFrameScheduled保證下一個(gè)vsync到來(lái)之前,不會(huì)有新的請(qǐng)求發(fā)出
// 多余的scheduleFrameLocked調(diào)用被無(wú)效化
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
// 因?yàn)閕nvalid已經(jīng)有了同步柵欄,所以必須mFrameScheduled,消息才能被UI線程執(zhí)行
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
}
}
- 在當(dāng)前申請(qǐng)的VSYNC到來(lái)之前,不會(huì)再去請(qǐng)求新的VSYNC,因?yàn)?6ms內(nèi)申請(qǐng)兩個(gè)VSYNC沒(méi)意義;
- 再VSYNC到來(lái)之后,Choreographer利用Handler將FrameDisplayEventReceiver封裝成一個(gè)異步Message,發(fā)送到UI線程的MessageQueue;
6、FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper) {
super(looper);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
long now = System.nanoTime();
if (timestampNanos > now) {
<!--正常情況,timestampNanos不應(yīng)該大于now,一般是上傳vsync的機(jī)制出了問(wèn)題-->
timestampNanos = now;
}
<!--如果上一個(gè)vsync同步信號(hào)沒(méi)執(zhí)行,那就不應(yīng)該相應(yīng)下一個(gè)(可能是其他線程通過(guò)某種方式請(qǐng)求的)-->
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
<!--timestampNanos其實(shí)是本次vsync產(chǎn)生的時(shí)間,從服務(wù)端發(fā)過(guò)來(lái)-->
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
<!--由于已經(jīng)存在同步柵欄,所以VSYNC到來(lái)的Message需要作為異步消息發(fā)送過(guò)去-->
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
<!--這里的mTimestampNanos其實(shí)就是本次Vynsc同步信號(hào)到來(lái)的時(shí)候,但是執(zhí)行這個(gè)消息的時(shí)候,可能延遲了-->
doFrame(mTimestampNanos, mFrame);
}
}
- 之所以封裝成異步Message,是因?yàn)榍懊嫣砑恿艘粋€(gè)同步柵欄,同步消息不會(huì)被執(zhí)行;
- UI線程被喚起,取出該消息,最終調(diào)用doFrame進(jìn)行UI刷新重繪;
7、doFrame
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
<!--做了很多東西,都是為了保證一次16ms有一次垂直同步信號(hào),有一次input 、刷新、重繪-->
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
<!--檢查是否因?yàn)檠舆t執(zhí)行掉幀,每大于16ms,就多掉一幀-->
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
<!--跳幀,其實(shí)就是上一次請(qǐng)求刷新被延遲的時(shí)間,但是這里skippedFrames為0不代表沒(méi)有掉幀-->
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
<!--skippedFrames很大一定掉幀,但是為 0,去并非沒(méi)掉幀-->
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
<!--開始doFrame的真正有效時(shí)間戳-->
frameTimeNanos = startNanos - lastFrameOffset;
}
if (frameTimeNanos < mLastFrameTimeNanos) {
<!--這種情況一般是生成vsync的機(jī)制出現(xiàn)了問(wèn)題,那就再申請(qǐng)一次-->
scheduleVsyncLocked();
return;
}
<!--intendedFrameTimeNanos是本來(lái)要繪制的時(shí)間戳,frameTimeNanos是真正的,可以在渲染工具中標(biāo)識(shí)延遲VSYNC多少-->
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
<!--移除mFrameScheduled判斷,說(shuō)明處理開始了,-->
mFrameScheduled = false;
<!--更新mLastFrameTimeNanos-->
mLastFrameTimeNanos = frameTimeNanos;
}
try {
<!--真正開始處理業(yè)務(wù)-->
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
<!--處理打包的move事件-->
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
<!--處理動(dòng)畫-->
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
<!--處理重繪-->
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
<!--提交->
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
- doFrame也采用了一個(gè)boolean遍歷mFrameScheduled保證每次VSYNC中,只執(zhí)行一次,可以看到,為了保證16ms只執(zhí)行一次重繪,加了好多次層保障;
- doFrame里除了UI重繪,其實(shí)還處理了很多其他的事,比如檢測(cè)VSYNC被延遲多久執(zhí)行,掉了多少幀,處理Touch事件(一般是MOVE),處理動(dòng)畫,以及UI;
- 當(dāng)doFrame在處理Choreographer.CALLBACK_TRAVERSAL的回調(diào)時(shí)(mTraversalRunnable),才是真正的開始處理View重繪;
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
回到ViewRootImpl調(diào)用doTraversal進(jìn)行View樹遍歷;
8、doTraversal
// 這里是真正執(zhí)行了,
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
<!--移除同步柵欄,只有重繪才設(shè)置了柵欄,說(shuō)明重繪的優(yōu)先級(jí)還是挺高的,所有的同步消息必須讓步-->
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
}
}
- doTraversal會(huì)先將柵欄移除,然后處理performTraversals,進(jìn)行測(cè)量、布局、繪制,提交當(dāng)前幀給SurfaceFlinger進(jìn)行圖層合成顯示;
- 以上多個(gè)boolean變量保證了每16ms最多執(zhí)行一次UI重繪;
9、UI局部重繪
View重繪刷新,并不會(huì)導(dǎo)致所有View都進(jìn)行一次measure、layout、draw,只是這個(gè)待刷新View鏈路需要調(diào)整,剩余的View可能不需要浪費(fèi)精力再來(lái)一遍;
View.java
public RenderNode updateDisplayListIfDirty() {
final RenderNode renderNode = mRenderNode;
...
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
|| !renderNode.isValid()
|| (mRecreateDisplayList)) {
<!--失效了,需要重繪-->
} else {
<!--依舊有效,無(wú)需重繪-->
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
return renderNode;
}
繪制總結(jié)
- android最高60FPS,是VSYNC及決定的,每16ms最多一幀;
- VSYNC要客戶端主動(dòng)申請(qǐng),才會(huì)有;
- 有VSYNC到來(lái)才會(huì)刷新;
- UI沒(méi)更改,不會(huì)請(qǐng)求VSYNC也就不會(huì)刷新;
以上就是Android的VSYNC機(jī)制和UI刷新流程示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Android VSYNC機(jī)制UI刷新的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android App中用Handler實(shí)現(xiàn)ViewPager頁(yè)面的自動(dòng)切換
這篇文章主要介紹了Android App中用Handler實(shí)現(xiàn)ViewPager頁(yè)面的自動(dòng)切換的方法,類似于相冊(cè)自動(dòng)播放,主要是切換后要提示當(dāng)前頁(yè)面所在的位置,需要的朋友可以參考下2016-05-05
Android開發(fā)之登錄驗(yàn)證實(shí)例教程
這篇文章主要介紹了Android開發(fā)之登錄驗(yàn)證實(shí)現(xiàn)方法,包括發(fā)送數(shù)據(jù)、服務(wù)器端驗(yàn)證、配置文件等,需要的朋友可以參考下2014-08-08
Android入門之實(shí)現(xiàn)自定義可復(fù)用的BaseAdapter
這篇文章主要為大家詳細(xì)介紹了Android如何構(gòu)建一個(gè)可復(fù)用的自定義BaseAdapter,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Android有一定的幫助,需要的可以參考一下2022-11-11
Android WebView 詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android WebView 詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-04-04
Android超詳細(xì)講解組件LinearLayout的使用
LinearLayout又稱作線性布局,是一種非常常用的布局。正如它的名字所描述的一樣,這個(gè)布局會(huì)將它所包含的控件在線性方向上依次排列。既然是線性排列,肯定就不僅只有一個(gè)方向,這里一般只有兩個(gè)方向:水平方向和垂直方向2022-03-03
android使用intent傳遞參數(shù)實(shí)現(xiàn)乘法計(jì)算
這篇文章主要為大家詳細(xì)介紹了android使用intent傳遞參數(shù)實(shí)現(xiàn)乘法計(jì)算,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
Android仿微信菜單(Menu)(使用C#和Java分別實(shí)現(xiàn))
這篇文章主要介紹了Android仿微信菜單(Menu)(使用C#和Java分別實(shí)現(xiàn)),本文分別給出C#和Java版的運(yùn)行效果及實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-06-06
Android實(shí)現(xiàn)圖片點(diǎn)擊放大
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)圖片點(diǎn)擊放大,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10

