Android 屏幕刷新機(jī)制

基本概念

  • CPU:執(zhí)行應(yīng)用層的measure护侮、layout、draw等操作,繪制完成后將數(shù)據(jù)提交給GPU
  • GPU:進(jìn)一步處理數(shù)據(jù),并將數(shù)據(jù)緩存起來
  • 屏幕:由一個(gè)個(gè)像素點(diǎn)組成酸役,以固定的頻率(16.6ms贱呐,即1秒60幀)從緩沖區(qū)中取出數(shù)據(jù)來填充像素點(diǎn)

總結(jié)一句話就是:CPU 繪制后提交數(shù)據(jù)吼句、GPU 進(jìn)一步處理和緩存數(shù)據(jù)惕艳、最后屏幕從緩沖區(qū)中讀取數(shù)據(jù)并顯示:

雙緩沖機(jī)制

在屏幕刷新中远搪,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è)備性能較差的時(shí)候抡医,CPU并不能保證在16.6ms內(nèi)就完成繪制數(shù)據(jù)的計(jì)算忌傻,所以這里系統(tǒng)又做了一個(gè)處理水孩。

  • 當(dāng)你的應(yīng)用正在往Back Buffer中填充數(shù)據(jù)時(shí)俘种,系統(tǒng)會將Back Buffer鎖定宙刘。如果到了GPU交換兩個(gè)Buffer的時(shí)間點(diǎn)怖现,你的應(yīng)用還在往Back Buffer中填充數(shù)據(jù),GPU會發(fā)現(xiàn)Back Buffer被鎖定了吊输,它會放棄這次交換季蚂。

這樣做的后果就是手機(jī)屏幕仍然顯示原先的圖像扭屁,這就是我們常常說的丟幀料滥,所以為了避免丟幀的發(fā)生,我們就要盡量減少布局層級践宴,減少不必要的View的invalidate調(diào)用阻肩,減少大量對象的創(chuàng)建(GC也會占用CPU時(shí)間)等等烤惊。

Choreographer

Android在每一幀中實(shí)際上只是在完成三個(gè)操作,分別是輸入(Input)伦泥、動畫(Animation)不脯、繪制(Draw)防楷。

在Android4.1(API 16)之后冲簿,Android系統(tǒng)開始加入Choreographer這個(gè)類峦剔,這個(gè)類名翻譯過來是“舞蹈指導(dǎo)”吝沫,字面上的意思就是指揮以上三個(gè)UI操作一起完成一支舞蹈。

這個(gè)類就可以解決vsync和繪制不同步的問題,其實(shí)它的原理用一句話總結(jié)就是往Choreographer里發(fā)一個(gè)消息一屋,最快也要等到下一個(gè)vsync信號來的時(shí)候才會開始處理消息。

Activity中的布局首次繪制诽嘉,以及每次調(diào)用View 的 invalidate() 時(shí),都會調(diào)用到ViewRootImp#requestLayout()悦冀,所以我們接下來分析一下ViewRootImp#requestLayout()里面做了什么

ViewRootImp#requestLayout()
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            //檢查是否是主線程踏烙,不然會拋出異常
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

    // ViewRootImpl 構(gòu)造函數(shù)的時(shí)候初始化 mThread
    // 也就是 mThread 是ViewRootImpl創(chuàng)建的那個(gè)線程 
    // 通常ViewRootImpl是在主線程創(chuàng)建的 
    // 所以更新UI要在UI線程(主線程)操作 
    public ViewRootImpl(Context context, Display display) {
        ...
        mThread = Thread.currentThread();
        ...
    }

ViewRootImp#scheduleTraversals()

如果在同一幀中出現(xiàn)多次requestLayout()調(diào)用寒屯,其實(shí)最終也只會繪制一次处面,為什么呢?我們可以看到下面有個(gè)mTraversalScheduled標(biāo)志位砸紊,稍后我們可以看看這個(gè)標(biāo)志位是哪里被置為false的

void scheduleTraversals() {
    if (!mTraversalScheduled) {         
        mTraversalScheduled = true;
        //添加同步消息屏障沼溜,這個(gè)方法也比較關(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)用這個(gè)方法

 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)前的時(shí)間戳放進(jìn)mCallbackQueue 隊(duì)列里
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                //如果沒有設(shè)置消息延時(shí)亡驰,直接執(zhí)行
                scheduleFrameLocked(now);
            } else {
                //消息延時(shí)戒职,但是最終依然會調(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í)就是一個(gè)標(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)用到一個(gè)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)前的時(shí)間戳放進(jìn)隊(duì)列里,那消息什么時(shí)候被取出來執(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),這個(gè)CallbackQueue#extractDueCallbacksLocked()會被Choreographer#doCallbacks()調(diào)用搞坝,Choreographer#doCallbacks()又會被Choreographer#doFrame()調(diào)用峰弹,最終我們跟到了FrameDisplayEventReceiver類。

FrameDisplayEventReceiver

因?yàn)樯厦娴膎ative方法我們沒有跟進(jìn)去分析舀射,擔(dān)心給大家繞暈了,我們會用一個(gè)新的章節(jié)來分析native層做的事情,這里先直接給出結(jié)論:

nativeScheduleVsync()會向SurfaceFlinger注冊Vsync信號的監(jiān)聽署惯,VSync信號由SurfaceFlinger實(shí)現(xiàn)并定時(shí)發(fā)送轻猖,當(dāng)Vsync信號來的時(shí)候就會回調(diào)FrameDisplayEventReceiver#onVsync()域那,這個(gè)方法給發(fā)送一個(gè)帶時(shí)間戳Runnable消息咙边,這個(gè)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)前時(shí)間與時(shí)間戳的間隔败许,間隔越大表示這一幀處理的時(shí)間越久,如果間隔超過一個(gè)周期淑蔚,就會去計(jì)算跳過了多少幀市殷,并打印出一個(gè)日志,這個(gè)日志我想很多人可能都見過:

Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
        + "The application may be doing too much work on its main thread.");

最終doFrame()會從mCallbackQueue 中取出消息并按照時(shí)間戳順序調(diào)用mTraversalRunnable的run()函數(shù)刹衫,mTraversalRunnable就是最初被加入到Choreographer中的Runnable()

//ViewRootImp 
mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

TraversalRunnable

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;
        }
    }
}

同步消息屏障

還記不記得前面說有mHandler.getLooper().getQueue().postSyncBarrier()這個(gè)方法還沒有進(jìn)行分析醋寝,這個(gè)方法的作用是什么呢搞挣?

void scheduleTraversals() {
    if (!mTraversalScheduled) {         
        mTraversalScheduled = true;
        //☆
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //向Choreographer中發(fā)送消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        //...
    }
}

我們知道,Android是基于消息機(jī)制的音羞,每一個(gè)操作都是一個(gè)Message囱桨,如果在觸發(fā)繪制的時(shí)候,消息隊(duì)列中還有很多消息沒有被執(zhí)行嗅绰,那是不是意味著要等到消息隊(duì)列中的消息執(zhí)行完成后舍肠,繪制消息才能被執(zhí)行到,那么依然無法保證Vsync信號和繪制的同步办陷,所以依然可能出現(xiàn)丟幀的現(xiàn)象.

還記不記得我們之前在Choreographer#scheduleFrameLocked()和FrameDisplayEventReceiver#onVsync()中提到貌夕,我們會給與Message有關(guān)的繪制請求設(shè)置成異步消息(msg.setAsynchronous(true)),為什么要這么做呢民镜?

這時(shí)候MessageQueue#postSyncBarrier()就發(fā)揮它的作用了啡专,簡單來說,它的作用就是一個(gè)同步消息屏障制圈,能夠把我們的異步消息(也就是繪制消息)的優(yōu)先級提到最高们童。

MessageQueue#postSyncBarrier()

主線程的 Looper 會一直循環(huán)調(diào)用 MessageQueue 的 next() 來取出隊(duì)頭的 Message 執(zhí)行,當(dāng) Message 執(zhí)行完后再去取下一個(gè)鲸鹦。

當(dāng) next() 方法在取 Message 時(shí)發(fā)現(xiàn)隊(duì)頭是一個(gè)同步屏障的消息時(shí)慧库,就會去遍歷整個(gè)隊(duì)列,只尋找設(shè)置了異步標(biāo)志的消息馋嗜,如果有找到異步消息齐板,那么就取出這個(gè)異步消息來執(zhí)行,否則就讓 next() 方法陷入阻塞狀態(tài)葛菇。

如果 next() 方法陷入阻塞狀態(tài)甘磨,那么主線程此時(shí)就是處于空閑狀態(tài)的,也就是沒在干任何事眯停。

所以济舆,如果隊(duì)頭是一個(gè)同步屏障的消息的話,那么在它后面的所有同步消息就都被攔截住了莺债,直到這個(gè)同步屏障消息被移除滋觉,否則主線程就一直不會去處理同步屏障后面的同步消息

那這么同步屏障是什么時(shí)候被移除的呢?

其實(shí)我們就是在我們上面提到的ViewRootImp#doTraversal()方法中齐邦。

mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

總結(jié)

  • 界面上任何一個(gè) View 的刷新請求最終都會走到 ViewRootImpl 中的 scheduleTraversals() 里來安排一次遍歷繪制 View 樹的任務(wù)椎侠;
  • scheduleTraversals() 會先過濾掉同一幀內(nèi)的重復(fù)調(diào)用,在同一幀內(nèi)只需要安排一次遍歷繪制 View 樹的任務(wù)即可措拇,這個(gè)任務(wù)會在下一個(gè)屏幕刷新信號到來時(shí)調(diào)用 performTraversals() 遍歷 View 樹我纪,遍歷過程中會將所有需要刷新的 View 進(jìn)行重繪;
  • 接著 scheduleTraversals() 會往主線程的消息隊(duì)列中發(fā)送一個(gè)同步屏障,攔截這個(gè)時(shí)刻之后所有的同步消息的執(zhí)行宣羊,但不會攔截異步消息璧诵,以此來盡可能的保證當(dāng)接收到屏幕刷新信號時(shí)可以盡可能第一時(shí)間處理遍歷繪制 View 樹的工作;
  • 發(fā)完同步屏障后 scheduleTraversals() 才會開始安排一個(gè)遍歷繪制 View 樹的操作仇冯,作法是把 performTraversals() 封裝到 Runnable 里面之宿,然后調(diào)用 Choreographer 的 postCallback() 方法;
  • postCallback() 方法會先將這個(gè) Runnable 任務(wù)以當(dāng)前時(shí)間戳放進(jìn)一個(gè)待執(zhí)行的隊(duì)列里苛坚,然后如果當(dāng)前是在主線程就會直接調(diào)用一個(gè)native 層方法比被,如果不是在主線程,會發(fā)一個(gè)最高優(yōu)先級的 message 到主線程泼舱,讓主線程第一時(shí)間調(diào)用這個(gè) native 層的方法等缀;
  • native 層的這個(gè)方法是用來向底層注冊監(jiān)聽下一個(gè)屏幕刷新信號,當(dāng)下一個(gè)屏幕刷新信號發(fā)出時(shí)娇昙,底層就會回調(diào) Choreographer 的onVsync() 方法來通知上層 app尺迂;
  • onVsync() 方法被回調(diào)時(shí),會往主線程的消息隊(duì)列中發(fā)送一個(gè)執(zhí)行 doFrame() 方法的消息冒掌,這個(gè)消息是異步消息噪裕,所以不會被同步屏障攔截住股毫;
  • doFrame() 方法會去取出之前放進(jìn)待執(zhí)行隊(duì)列里的任務(wù)來執(zhí)行膳音,取出來的這個(gè)任務(wù)實(shí)際上是 ViewRootImpl 的 doTraversal() 操作;
  • 上述第4步到第8步涉及到的消息都手動設(shè)置成了異步消息铃诬,所以不會受到同步屏障的攔截祭陷;
  • doTraversal() 方法會先移除主線程的同步屏障,然后調(diào)用 performTraversals() 開始根據(jù)當(dāng)前狀態(tài)判斷是否需要執(zhí)行performMeasure() 測量趣席、perfromLayout() 布局兵志、performDraw() 繪制流程,在這幾個(gè)流程中都會去遍歷 View 樹來刷新需要更新的View吩坝;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毒姨,一起剝皮案震驚了整個(gè)濱河市哑蔫,隨后出現(xiàn)的幾起案子钉寝,更是在濱河造成了極大的恐慌,老刑警劉巖闸迷,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嵌纲,死亡現(xiàn)場離奇詭異,居然都是意外死亡腥沽,警方通過查閱死者的電腦和手機(jī)逮走,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來今阳,“玉大人师溅,你說我怎么就攤上這事茅信。” “怎么了墓臭?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵蘸鲸,是天一觀的道長。 經(jīng)常有香客問我窿锉,道長酌摇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任嗡载,我火速辦了婚禮窑多,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘洼滚。我一直安慰自己埂息,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布遥巴。 她就那樣靜靜地躺著耿芹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挪哄。 梳的紋絲不亂的頭發(fā)上吧秕,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機(jī)與錄音迹炼,去河邊找鬼砸彬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛斯入,可吹牛的內(nèi)容都是我干的砂碉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼刻两,長吁一口氣:“原來是場噩夢啊……” “哼增蹭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起磅摹,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤滋迈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后户誓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饼灿,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年帝美,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了碍彭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖庇忌,靈堂內(nèi)的尸體忽然破棺而出舞箍,到底是詐尸還是另有隱情,我是刑警寧澤皆疹,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布创译,位于F島的核電站,受9級特大地震影響墙基,放射性物質(zhì)發(fā)生泄漏软族。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一残制、第九天 我趴在偏房一處隱蔽的房頂上張望立砸。 院中可真熱鬧,春花似錦初茶、人聲如沸颗祝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽螺戳。三九已至,卻和暖如春折汞,著一層夾襖步出監(jiān)牢的瞬間倔幼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工爽待, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留损同,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓鸟款,卻偏偏與公主長得像膏燃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子何什,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評論 2 359

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