面試官:說說Android的UI刷新機制?

本文主要解決以下幾個問題:

  • 我們都知道Android的刷新頻率是60幀/秒,這是不是意味着每隔16ms就會調用一次onDraw方法?
  • 如果界面不需要重繪,那麼16ms到後還會刷新屏幕嗎?
  • 我們調用invalidate()之後會馬上進行屏幕刷新嗎?
  • 我們說丟幀是因爲主線程做了耗時操作,爲什麼主線程做了耗時操作就會引起丟幀?
  • 如果在屏幕快要刷新的時候纔去OnDraw()繪製,會丟幀嗎?

好了,帶着以上問題,我們進入源碼來找尋答案。

一、屏幕繪製流程

屏幕繪製機制的基本原理可以概括如下:

整個屏幕繪製的基本流程是:

  • 應用向系統服務申請buffer
  • 系統服務返回buffer
  • 應用繪製後提交buffer給系統服務

如果放到Android中來,那麼就是:

在Android中,一塊Surface對應一塊內存,當內存申請成功後,App端纔有繪圖的地方。由於Android的view繪製不是今天的重點,所以這裏點到爲止~

二、屏幕刷新分析

屏幕刷新的時機是當Vsync信號到來的時候,具體如圖:

在Android端,是誰在控制Vsync的產生?又是誰來通知我們應用進行刷新的呢?在Android中,Vysnc信號的產生是由底層HWComposer負責的,而通知應用進行刷新,是Java層的Choreographer,Android整個屏幕刷新的核心就在於這個Choreographer。下面我們結合代碼一起來看一下。每次當我們要進行ui重繪的時候,都會調用requestLayout(),所以,我們從這個方法入手:

2.1 requestLayout()

----》類名:ViewRootImpl

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            //重點
            scheduleTraversals();
        }
    }

2.2 scheduleTraversals()

----》類名:ViewRootImpl

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
           ......
        }
    }

可以看到,在這裏並沒有立即進行重繪,而是做了兩件事情:

  • 往消息隊列裏面插入一條SyncBarrier(同步屏障)
  • 通過Cherographer post了一個callback

接下來,我們簡單說一下這個SyncBarrier(同步屏障)。異步屏障的作用在於:

  • 阻止同步消息的執行
  • 優先執行異步消息

爲什麼要設計這個SyncBarrier呢?主要原因在於,在Android中,有些消息是十分緊急的,需要馬上執行,如果說消息隊列裏面普通消息太多的話,那等到執行它的時候可能早就過了時機了。

到這裏,可能有人會跟我一樣,覺得爲什麼不乾脆在Message裏搞個優先級,按照優先級來進行排序呢?弄個PriorityQueue不就完了嗎?

我自己的理解是,在Android中,消息隊列的設計是一個單鏈表,整個鏈表的排序是根據時間進行排序的,如果此時再加入一個優先級的排序規則,一方面會複雜會排序規則,另一方面,也會使得消息不可控。因爲優先級是可以用戶自己在外面填的,那樣不就亂套了嗎?如果用戶每次總填最高的優先級,這樣就會導致系統消息很久纔會消費,整個系統運作就會出問題,最後影響用戶體驗,所以,我自己覺得Android的同步屏障這個設計還是挺巧妙的~

好了,總結一下,執行scheduleTraversals() 後,會插入一個屏障,保證異步消息的優先執行。

插入一個小小的思考題:如果說我們在一個方法裏連續調用了requestLayout()多次,那麼請問:系統會插入多條屏障或者post多個Callback嗎?答案是不會,爲什麼呢?看到mTraversalScheduled這個變量了嗎?它就是答案~

2.3 Choreographer.postCallback()

先來簡單說一下ChoreographerChoreographer中文翻譯叫編舞者,它的主要作用是進行系統協調的。(大家可以上網google下實際工作中的編舞者,這個類名真的起的很貼切了~) Choreographer這個類是應用怎麼初始化的呢?是通過getInstance()方法:

    public static Choreographer getInstance() {
        return sThreadInstance.get();
    }
    
        // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };

這裏貼出來是爲了提醒大家,Choreographer不是單例,而是每個線程都有單獨的一份。

好了,回到我們的代碼:

 ----》類名:Choreographer
 //1
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
  //2
     public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
       ....
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
    //3
      private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
                ...
                mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
                if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                ...
              }
            }

Choreographerpostcallback會放入CallbackQueue裏面,這個CallbackQueue是一個單鏈表。

首先會根據callbackType得到一條CallbackQueue單鏈表,之後會根據時間順序,將這個callback插入到單鏈表中;

2.4 scheduleFrameLocked()

 ----》類名:Choreographer
  private void scheduleFrameLocked(long now) {
       ...
       // 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 {
               ...
            }
        }
    }

scheduleFrameLocked的作用是:

如果當前線程就是Cherographer的工作線程的話,那麼就直接執行scheduleVysnLocked

否則,就發送一個異步消息到消息隊列裏面去 ,這個異步消息是不受同步屏障影響的,而且這個消息還要插入到消息隊列的頭部,可見這個消息是非常緊急的

跟蹤源代碼,我們發現,其實MSG_DO_SCHEDULE_VSYNC這條消息,最終執行的也是scheduleFrameLocked這個方法,所以我們直接跟蹤scheduleVsyncLocked()這個方法。

2.5 scheduleVsyncLocked()

 ----》類名:Choreographer
 
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }
    
 ----》類名:DisplayEventReceiver
 
        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 {
        //mReceiverPtr是Native層一個類的指針地址
        //這裏這個類指的是底層NativeDisplayEventReceiver這個類
        //nativeScheduleVsync底層會調用到requestNextVsync()去請求下一個Vsync,
        //具體不跟蹤了,native層代碼更長,還涉及到各種描述符監聽以及跨進程數據傳輸
            nativeScheduleVsync(mReceiverPtr);
        }
    }

這裏我們可以看到一個新的類:DisplayEventReceiver,這個類的作用是註冊Vsync信號的監聽,當下個Vsync信號到來的時候就會通知到這個DisplayEventReceiver了。

在哪裏通知呢?源碼裏註釋寫的非常清楚了:

 ----》類名:DisplayEventReceiver
 
    // Called from native code.  <---註釋還是很良心的
    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }

當下一個Vysnc信號到來的時候,會最終調用onVsync方法:

    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
    }

點進去一看,是個空實現,回到類定義,原來是個抽象類,它的實現類是:FrameDisplayEventReceiver,定義在Cherographer裏面:

 ----》類名:Choreographer
 
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
            ....
            }

2.6 FrameDisplayEventReceiver.onVysnc()

 ----》類名:Choreographer
 
 private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
             ....
            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() {
            ....
            doFrame(mTimestampNanos, mFrame);
        }
    }

onVsync方法往Cherographer所在線程的消息隊列中發送的一個消息,這個消息是就是它自己(它實現了Runnable),所以最終會調用到doFrame()方法。

2.7 doFrame(mTimestampNanos, mFrame)

doFrame()的處理分爲兩個階段:

   void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
           //1、階段一
            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();
            final long jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) {
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
                ...
            }
            ...
        }

frameTimeNanos是當前的時間戳,將當前的時間和開始時間相減,得到這一幀處理花費了多長,如果大於mFrameIntervalNano,說明處理耗時了,之後就打印出我們日常見到的The application may be doing too much work on its main thread。

階段二:

 void doFrame(long frameTimeNanos, int frame) {
 ...
try {
//階段2
            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);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        }
        ...
        }

doFrame()的第二個階段做的是處理各種callback,從CallbackQueue裏面取出到執行時間的callback進行處理,那這個callback是怎麼樣呢?

這裏要回憶一下之前的postCallback()操作:

這個Callback其實就一個mTraversalRunnable,它是一個Runnable,最終會調用到run()方法,實現界面的真正刷新:

 ----》類名:ViewRootImpl

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    void doTraversal() {
        if (mTraversalScheduled) {
          ...
            performTraversals();
         ...
        }
    }
    
    private void performTraversals() {
      ...
      //開始真正的界面繪製
       performDraw();
      ...
    }

三、總結

經過漫長的代碼跟蹤,整個界面刷新流程算是跟蹤完了,下面我們來總結一下:

四、問題解答

Q: 我們都知道Android的刷新頻率是60幀/秒,這是不是意味着每隔16ms就會調用一次onDraw方法?

A: 這裏60幀/秒是屏幕刷新頻率,但是是否會調用onDraw()方法要看應用是否調用requestLayout()進行註冊監聽。

Q: 如果界面不需要重繪,那麼還16ms到後還會刷新屏幕嗎?

A: 如果不需要重繪,那麼應用就不會受到Vsync信號,但是還是會進行刷新,只不過繪製的數據不變而已;

Q: 我們調用invalidate()之後會馬上進行屏幕刷新嗎?

A: 不會,到等到下一個Vsync信號到來

Q: 我們說丟幀是因爲主線程做了耗時操作,爲什麼主線程做了耗時操作就會引起丟幀

A: 原因是,如果在主線程做了耗時操作,就會影響下一幀的繪製,導致界面無法在這個Vsync時間進行刷新,導致丟幀了。

Q: 如果在屏幕快要刷新的時候纔去OnDraw()繪製,會丟幀嗎?

這個沒有太大關係,因爲Vsync信號是週期的,我們什麼時候發起onDraw()不會影響界面刷新;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章