Android VSYNC (Choreographer)與UI刷新原理分析

從UI控件內(nèi)容更改到被重新繪制到屏幕上拍顷,這中間到底經(jīng)歷了什么谍失?另外结澄,連續(xù)兩次setTextView到底會觸發(fā)幾次UI重繪呢朴沿?為什么Android APP的幀率最高是60FPS呢,這就是本文要討論的內(nèi)容授艰。

以電影為例辨嗽,動畫至少要達到24FPS,才能保證畫面的流暢性淮腾,低于這個值糟需,肉眼會感覺到卡頓。在手機上谷朝,這個值被調(diào)整到60FPS洲押,增加絲滑度,這也是為什么有個(1000/60)16ms的指標圆凰,一般而言目前的Android系統(tǒng)最高FPS也就是60杈帐,它是通過了一個VSYNC來保證每16ms最多繪制一幀。簡而言之:UI必須至少等待16ms的間隔才會繪制下一幀专钉,所以連續(xù)兩次setTextView只會觸發(fā)一次重繪挑童。下面來具體看一下UI的重繪流程。

UI刷新流程示意

以Textview為例 跃须,當(dāng)我們通過setText改變TextView內(nèi)容后站叼,UI界面不會立刻改變,APP端會先向VSYNC服務(wù)請求菇民,等到下一次VSYNC信號觸發(fā)后尽楔,APP端的UI才真的開始刷新,基本流程如下

image.png

從我們的代碼端來看如下:setText最終調(diào)用invalidate申請重繪玉雾,最后會通過ViewParent遞歸到ViewRootImpl的invalidate翔试,請求VSYNC,在請求VSYNC的時候复旬,會添加一個同步柵欄垦缅,防止UI線程中同步消息執(zhí)行,這樣做為了加快VSYNC的響應(yīng)速度驹碍,如果不設(shè)置壁涎,VSYNC到來的時候凡恍,正在執(zhí)行一個同步消息,那么UI更新的Task就會被延遲執(zhí)行怔球,這是Android的Looper跟MessageQueue決定的嚼酝。

APP端觸發(fā)重繪,申請VSYNC流程示意

image.png

等到VSYNC到來后竟坛,會移除同步柵欄闽巩,并率先開始執(zhí)行當(dāng)前幀的處理,調(diào)用邏輯如下

VSYNC回來流程示意

image.png

doFrame執(zhí)行UI繪制的示意圖

image.png

UI刷新源碼跟蹤

同TextView類似担汤,View內(nèi)容改變一般都會調(diào)用invalidate觸發(fā)視圖重繪涎跨,這中間經(jīng)歷了什么呢?View會遞歸的調(diào)用父容器的invalidateChild崭歧,逐級回溯隅很,最終走到ViewRootImpl的invalidate,如下:

View.java

 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會調(diào)用scheduleTraversals準備重繪率碾,但是叔营,重繪一般不會立即執(zhí)行,而是往Choreographer的Choreographer.CALLBACK_TRAVERSAL隊列中添加了一個mTraversalRunnable所宰,同時申請VSYNC绒尊,這個mTraversalRunnable要一直等到申請的VSYNC到來后才會被執(zhí)行,如下:

ViewRootImpl.java

 // 將UI繪制的mTraversalRunnable加入到下次垂直同步信號到來的等待callback中去
 // mTraversalScheduled用來保證本次Traversals未執(zhí)行前歧匈,不會要求遍歷兩邊垒酬,浪費16ms內(nèi)砰嘁,不需要繪制兩次
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 防止同步柵欄件炉,同步柵欄的意思就是攔截同步消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // postCallback的時候,順便請求vnsc垂直同步信號scheduleVsyncLocked
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         <!--添加一個處理觸摸事件的回調(diào)矮湘,防止中間有Touch事件過來-->
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

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) {
        <!--申請VSYNC同步信號-->
            scheduleFrameLocked(now);
        } 
    }
}

scheduleTraversals利用mTraversalScheduled保證斟冕,在當(dāng)前的mTraversalRunnable未被執(zhí)行前,scheduleTraversals不會再被有效調(diào)用缅阳,也就是Choreographer.CALLBACK_TRAVERSAL理論上應(yīng)該只有一個mTraversalRunnable的Task磕蛇。mChoreographer.postCallback將mTraversalRunnable插入到CallBack之后,會接著調(diào)用scheduleFrameLocked請求Vsync同步信號

// mFrameScheduled保證16ms內(nèi)十办,只會申請一次垂直同步信號
// scheduleFrameLocked可以被調(diào)用多次秀撇,但是mFrameScheduled保證下一個vsync到來之前,不會有新的請求發(fā)出
// 多余的scheduleFrameLocked調(diào)用被無效化
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
        
            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                // 因為invalid已經(jīng)有了同步柵欄向族,所以必須mFrameScheduled呵燕,消息才能被UI線程執(zhí)行
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        }  
    }
}

scheduleFrameLocked跟上一個scheduleTraversals類似,也采用了利用mFrameScheduled來保證:在當(dāng)前申請的VSYNC到來之前件相,不會再去請求新的VSYNC再扭,因為16ms內(nèi)申請兩個VSYNC沒意義氧苍。再VSYNC到來之后,Choreographer利用Handler將FrameDisplayEventReceiver封裝成一個異步Message泛范,發(fā)送到UI線程的MessageQueue让虐,

  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的機制出了問題-->
                timestampNanos = now;
            }
            <!--如果上一個vsync同步信號沒執(zhí)行赡突,那就不應(yīng)該相應(yīng)下一個(可能是其他線程通過某種方式請求的)-->
              if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }
            <!--timestampNanos其實是本次vsync產(chǎn)生的時間,從服務(wù)端發(fā)過來-->
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            <!--由于已經(jīng)存在同步柵欄区赵,所以VSYNC到來的Message需要作為異步消息發(fā)送過去-->
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            <!--這里的mTimestampNanos其實就是本次Vynsc同步信號到來的時候麸俘,但是執(zhí)行這個消息的時候,可能延遲了-->
            doFrame(mTimestampNanos, mFrame);
        }
    }

之所以封裝成異步Message惧笛,是因為前面添加了一個同步柵欄从媚,同步消息不會被執(zhí)行。UI線程被喚起患整,取出該消息拜效,最終調(diào)用doFrame進行UI刷新重繪

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
    <!--做了很多東西,都是為了保證一次16ms有一次垂直同步信號各谚,有一次input 紧憾、刷新、重繪-->
        if (!mFrameScheduled) {
            return; // no work to do
        }
       long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;
        <!--檢查是否因為延遲執(zhí)行掉幀昌渤,每大于16ms赴穗,就多掉一幀-->
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            <!--跳幀,其實就是上一次請求刷新被延遲的時間膀息,但是這里skippedFrames為0不代表沒有掉幀-->
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
            <!--skippedFrames很大一定掉幀般眉,但是為 0,去并非沒掉幀-->
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                <!--開始doFrame的真正有效時間戳-->
            frameTimeNanos = startNanos - lastFrameOffset;
        }

        if (frameTimeNanos < mLastFrameTimeNanos) {
            <!--這種情況一般是生成vsync的機制出現(xiàn)了問題潜支,那就再申請一次-->
            scheduleVsyncLocked();
            return;
        }
          <!--intendedFrameTimeNanos是本來要繪制的時間戳甸赃,frameTimeNanos是真正的,可以在渲染工具中標識延遲VSYNC多少-->
        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        <!--移除mFrameScheduled判斷冗酿,說明處理開始了埠对,-->
        mFrameScheduled = false;
        <!--更新mLastFrameTimeNanos-->
        mLastFrameTimeNanos = frameTimeNanos;
    }

    try {
         <!--真正開始處理業(yè)務(wù)-->
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
        <!--處理打包的move事件-->
        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 {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

doFrame也采用了一個boolean遍歷mFrameScheduled保證每次VSYNC中,只執(zhí)行一次裁替,可以看到项玛,為了保證16ms只執(zhí)行一次重繪,加了好多次層保障弱判。doFrame里除了UI重繪襟沮,其實還處理了很多其他的事,比如檢測VSYNC被延遲多久執(zhí)行,掉了多少幀臣嚣,處理Touch事件(一般是MOVE)净刮,處理動畫,以及UI硅则,當(dāng)doFrame在處理Choreographer.CALLBACK_TRAVERSAL的回調(diào)時(mTraversalRunnable)淹父,才是真正的開始處理View重繪:

  final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

回到ViewRootImpl調(diào)用doTraversal進行View樹遍歷,

// 這里是真正執(zhí)行了怎虫,
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        <!--移除同步柵欄暑认,只有重繪才設(shè)置了柵欄,說明重繪的優(yōu)先級還是挺高的大审,所有的同步消息必須讓步-->
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        performTraversals();
    }
}

doTraversal會先將柵欄移除蘸际,然后處理performTraversals,進行測量徒扶、布局粮彤、繪制,提交當(dāng)前幀給SurfaceFlinger進行圖層合成顯示姜骡。以上多個boolean變量保證了每16ms最多執(zhí)行一次UI重繪导坟,這也是目前Android存在60FPS上限的原因。

注: VSYNC同步信號需要用戶主動去請求才會收到圈澈,并且是單次有效惫周。

UI局部重繪

某一個View重繪刷新,并不會導(dǎo)致所有View都進行一次measure康栈、layout递递、draw,只是這個待刷新View鏈路需要調(diào)整啥么,剩余的View可能不需要浪費精力再來一遍登舞,反應(yīng)再APP側(cè)就是:不需要再次調(diào)用所有ViewupdateDisplayListIfDirty構(gòu)建RenderNode渲染Op樹,如下

View.java

    public RenderNode updateDisplayListIfDirty() {
        final RenderNode renderNode = mRenderNode;
          ...
        if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
                || !renderNode.isValid()
                || (mRecreateDisplayList)) {
           <!--失效了饥臂,需要重繪-->
        } else {
        <!--依舊有效逊躁,無需重繪-->
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        }
        return renderNode;
    }

總結(jié)

  • android最高60FPS,是VSYNC及決定的隅熙,每16ms最多一幀
  • VSYNC要客戶端主動申請,才會有
  • 有VSYNC到來才會刷新
  • UI沒更改核芽,不會請求VSYNC也就不會刷新
  • UI局部重繪其實只是省去了再次構(gòu)建硬件加速用的DrawOp樹(復(fù)用上衣幀的)

作者:看書的小蝸牛

Android VSYNC (Choreographer)與UI刷新原理分析.md

僅供參考囚戚,歡迎指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市轧简,隨后出現(xiàn)的幾起案子驰坊,更是在濱河造成了極大的恐慌,老刑警劉巖哮独,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拳芙,死亡現(xiàn)場離奇詭異察藐,居然都是意外死亡,警方通過查閱死者的電腦和手機舟扎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門分飞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人睹限,你說我怎么就攤上這事譬猫。” “怎么了羡疗?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵染服,是天一觀的道長。 經(jīng)常有香客問我叨恨,道長柳刮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任痒钝,我火速辦了婚禮诚亚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘午乓。我一直安慰自己站宗,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布益愈。 她就那樣靜靜地躺著梢灭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蒸其。 梳的紋絲不亂的頭發(fā)上敏释,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音摸袁,去河邊找鬼钥顽。 笑死,一個胖子當(dāng)著我的面吹牛靠汁,可吹牛的內(nèi)容都是我干的蜂大。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蝶怔,長吁一口氣:“原來是場噩夢啊……” “哼奶浦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起踢星,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤澳叉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體成洗,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡五督,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓶殃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片充包。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖碌燕,靈堂內(nèi)的尸體忽然破棺而出误证,到底是詐尸還是另有隱情,我是刑警寧澤修壕,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布愈捅,位于F島的核電站,受9級特大地震影響慈鸠,放射性物質(zhì)發(fā)生泄漏蓝谨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一青团、第九天 我趴在偏房一處隱蔽的房頂上張望譬巫。 院中可真熱鬧,春花似錦督笆、人聲如沸芦昔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咕缎。三九已至,卻和暖如春料扰,著一層夾襖步出監(jiān)牢的瞬間凭豪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工晒杈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫂伞,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓拯钻,卻偏偏與公主長得像帖努,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子说庭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1然磷、概述 不論電腦,電視刊驴,手機,我們看到的畫面都是由一幀幀的畫面組成的。FPS是圖像領(lǐng)域中的定義捆憎,是指畫面每秒傳輸...
    高丕基閱讀 12,016評論 6 34
  • 更新:最近發(fā)現(xiàn)該篇文章閱讀的人數(shù)挺多舅柜,并且也發(fā)現(xiàn)了更好的關(guān)于Android顯示系統(tǒng)的文章,所以將關(guān)于Android...
    htkeepmoving閱讀 9,651評論 0 10
  • 系統(tǒng)級別的流暢度優(yōu)化 流暢度應(yīng)該是終端用戶感知最明顯的性能指標了躲惰,提升流暢度是提升用戶體驗性價比最高的方式之一致份,我...
    sunhapper閱讀 4,796評論 1 20
  • 這篇博文是參考別人的博客,結(jié)合源碼自己又走了一遍础拨,僅供自己記錄學(xué)習(xí)氮块。 我們要掌握android,那么關(guān)于andro...
    scarecrowtb閱讀 2,169評論 1 3
  • 注意事項: 布局優(yōu)化诡宗;盡量使用include滔蝉、merge、ViewStub標簽塔沃,盡量不存在冗余嵌套及過于復(fù)雜布局(...
    HarryXR閱讀 5,149評論 1 19