AndroidQ UI刷新機制詳解

本篇文章分析Android UI刷新機制,就是更新UI,做Android開發初期我們經常會聽說不能在子線程更新UI,以及Activity的onCreate方法中獲取不到View寬高的問題

我們先來說一下子線程不能更新UI的問題:

public class MainActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      button.setText("button");
                  }
              }).start();
            }
        });
    }
}

我們定義一個button,點擊時修改button的文字,發現果然報錯:很熟悉的錯誤,只能在創建視圖的原始線程修改視圖

03-24 18:17:17.974 25194 27234 E AndroidRuntime: FATAL EXCEPTION: Thread-2
03-24 18:17:17.974 25194 27234 E AndroidRuntime: Process: com.example.myapplication, PID: 25194
03-24 18:17:17.974 25194 27234 E AndroidRuntime: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
03-24 18:17:17.974 25194 27234 E AndroidRuntime: at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8464)
03-24 18:17:17.974 25194 27234 E AndroidRuntime: at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1468)

我們再把代碼改成這樣,不在button點擊時修改它的text,而在onCreate中修改:

public class MainActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        new Thread(new Runnable() {
            @Override
            public void run() {
                button.setText("button");
            }
        }).start();
    }
}

我們發現這樣就不報錯了,而且text修改成功,所以我們如果在onCreate中更新UI的話隨便在哪個線程都是OK的

爲什麼會這樣?我們看下上面那段拋出的異常,在ViewRootImpl的requestLayout中調用checkThread來檢查UI更新的線程的,那麼onCreate中能在子線程更新UI,說明此時無法使用checkThread來檢查線程,實際上此時ViewRootImpl並沒有被創建

ViewRootImpl的創建是在Activity的onResume過程中,在onResume時會通過WindowManagerGlobal的addView方法將Activity的頂層視圖DecoreView添加到WMS中,而在WindowManagerGlobal的addView方法中就會創建ViewRootImpl,並調用它的setView方法

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
          ...
            root = new ViewRootImpl(view.getContext(), display);
  			...
            root.setView(view, wparams, panelParentView);
         
            ....
    }

ViewRootImpl的setView方法中會調用requestLayout方法,此方法就會調用
checkThread檢查當前線程,並進行View的測量,佈局,繪製流程

@Override
    public void requestLayout() {
            ...
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

這也解決了爲什麼onCreate中無法獲取View的寬高的問題,因爲onResume時纔對Activity的佈局文件進行測量

好了,在checkThread檢查完UI更新的線程是否合法之後就會調用scheduleTraversals觸發UI的更新操作了,首先給一個結論,UI的更新不是同步的,而是將UI更新請求首先放入一個callback鏈表,等待底層的垂直同步脈衝信號(vsync),收到這個信號之後回調callback執行測量,佈局,繪製操作,這個信號在底層由硬件發出,有些也是通過軟件模擬的,標準情況下是每秒60次,即每秒刷新60幀,也就是我們常說的16.6ms需要完成一幀的繪製,不然就會卡頓

接下來進行代碼分析

scheduleTraversals

    @UnsupportedAppUsage
    void scheduleTraversals() {
       //防止重複調用
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            .....
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            //通知底層即將進行畫面渲染
           ......
    }

這個方法裏面我們關注兩個重點,1.設置同步屏障,2.post一個繪製請求

首先來看設置同步屏障,同步屏障其實就是向MessageQueue添加一個不帶target的消息,作用是屏蔽handler的同步消息,只取出異步消息執行,默認handle都是同步消息,
關於同步屏障我在AndroidQ Handler消息機制(java層)這篇文章中詳細分析過

接着看調用Choreographer的postCallback方法添加一個Callback,第一個參數是Callback的type,第二個參數是Callback執行的mTraversalRunnable,
我們後面再來看這個Runnable

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

來看看Choreographer的postCallback方法

 public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
    @TestApi
    public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
        if (action == null) {
            throw new IllegalArgumentException("action must not be null");
        }
        if (callbackType < 0 || callbackType > CALLBACK_LAST) {
            throw new IllegalArgumentException("callbackType is invalid");
        }

        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
         ...
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            //(1)步驟1
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
             //(2)步驟2
            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

一通調用,就是一個參數的傳遞,postCallbackDelayedInternal中我們分兩個步驟來分析,先看步驟1:
mCallbackQueues類型爲CallbackQueue[],裏面有四種類型的CallbackQueue,CallbackQueue是一個鏈表,就是一系列CALLBACK_XXXX類型請求的集合,按照執行的先後順序排列,頭部是即將執行的請求,我們只關心繪製類型請求,另外還有輸入類型,動畫類型等

/**
     * Callback type: Traversal callback.  Handles layout and draw.  Runs
     * after all other asynchronous messages have been handled.
     * @hide
     */
    public static final int CALLBACK_TRAVERSAL = 3;

我們傳遞過來的dueTime爲0,需要立即執行,在來看步驟2:
其實如下不管是哪個分支最終都是通過handler發送MSG_DO_SCHEDULE_CALLBACK消息來處理的,只是是否延遲執行而已

 if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }

我們來看scheduleFrameLocked方法

scheduleFrameLocked

private void scheduleFrameLocked(long now) {
        //保證執行一次
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            //是否使用VSYNC,現在手機一般都使用的
            if (USE_VSYNC) {
               ...
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
                //不使用VSYNC的情況
                ......
            }
        }
    }

上面代碼的兩個分支最終都是執行同樣的方法scheduleVsyncLocked,只不過需要判斷當前是否在Looper線程,我們理解成主線程就行了,如果不在就通過Handler消息到主線程執行,我們看Choreographer的代碼會發現所有的Handler消息都被設置成了異步消息setAsynchronous(true),這樣我們前面開啓的同步屏障就起作用了,目的是UI的更新必須以最高優先級進行

scheduleVsyncLocked

@UnsupportedAppUsage
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }

mDisplayEventReceiver類型爲FrameDisplayEventReceiver,它是上層用來接收Vsync信號的,調用它的scheduleVsync方法向native層註冊監聽Vsync,需要注意Vsync的接收是註冊一次才能接收一次

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

最終通過nativeScheduleVsync向native層註冊Vsync的監聽,關於Vsync的註冊我們下一篇文章來分析

這裏我們直接來看接收到Vsync之後的回調

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
			......
			@Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
            
            long now = System.nanoTime();
            
            .....
            
            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() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
			......
}

接收到Vsync的回調接着就通過handler到UI線程執行Runnable的回調doFrame

doFrame

@UnsupportedAppUsage
    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            //這裏有很多關於Vsync到來時間,frame時間計算,以及
            //跳frame相關的各種計算我就省略了
            .....
            

        try {
            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);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

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

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        ....
    }

最終此方法實際是通過doCallbacks方法執行我們最開始添加的繪製類型的回調的,我們關注傳遞的type爲CALLBACK_TRAVERSAL的回調

doCallbacks

void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            //步驟1
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }

            //不關注這個類型
            if (callbackType == Choreographer.CALLBACK_COMMIT) {
               .....
            }
        }
        try {
            //步驟2
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
               ...
                c.run(frameTimeNanos);
            }
        } finally {
           //收尾工作
            ....
        }
    }

爲了代碼簡潔,刪掉了不關注的部分,上面代碼我分爲兩個部分來分析,先看步驟1:
對type爲CALLBACK_TRAVERSAL的CallbackQueues執行extractDueCallbacksLocked方法,主要看看這個方法要幹嘛的

extractDueCallbacksLocked

 public CallbackRecord extractDueCallbacksLocked(long now) {
            CallbackRecord callbacks = mHead;//頭指針
            //如果鏈表爲空或者鏈表中頭指針指向的callback的執行時間都
            //大於當前時間則返回,因爲還沒到callback執行時間
            if (callbacks == null || callbacks.dueTime > now) {
                return null;
            }
            //last是頭指針
            CallbackRecord last = callbacks;
            //next是頭指針下一個Callback
            CallbackRecord next = last.next;
            //這種一看就是遍歷鏈表操作
            while (next != null) {
                //如果下一個Callback的執行時間大於當前時間
                if (next.dueTime > now) {
                   //則需要斷掉鏈表,因爲這裏只有頭部Callback
                   //滿足執行條件
                    last.next = null;
                    break;
                }
                //否則繼續往後查找
                last = next;
                next = next.next;
            }
            //鏈表遍歷完成之後,頭指針重新指向next,這裏的next可能爲空,
            //爲空代表鏈表所有Callback都需要執行,不爲空則指向被斷掉的鏈表
            mHead = next;
            return callbacks;
        }

總結一下extractDueCallbacksLocked的作用,此方法就是爲了得到執行時間小於當前時間的Callback鏈表集合,得到之後會將鏈表斷成兩份,前面一份爲需要馬上執行的Callback,後面一份爲還未到執行時間的Callback,這樣保證了下一次繪製請求的Callback能夠放在後面一個鏈表中,最早也在下一個Vsync到才執行

好了我們接着看步驟2,步驟2就比較簡單了,就是遍歷extractDueCallbacksLocked得到的鏈表,然後依次調用它們的run方法,我們看看CallbackRecord的run方法

 private static final class CallbackRecord {
        ......
        @UnsupportedAppUsage
        public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
                ((Runnable)action).run();
            }
        }
    }

這裏的action就是我們最開始scheduleTraversals方法中post進去的,就是這個:

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

調用doTraversal方法

doTraversal

void doTraversal() {
        .....
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            performTraversals();
           ....
        }
    }

這個方法先移除了我們開始添加的同步屏障,接着調用我們熟悉的performTraversals方法,開始了測量,佈局,繪製流程

到這裏UI刷新機制全部分析完了,我們做一個總結:

  1. 不管什麼形式的UI更新,最終都會調用到ViewRootImpl的scheduleTraversals方法,這個方法會開啓同步屏障,並且將UI的繪製請求post到一個callback數組中等待執行
  2. Choreographer中的DisplayEventReceiver會在UI線程中通過Handler異步消息向native層註冊一個Vsync信號監聽(nativeScheduleVsync)
  3. 在下一個Vsync到來時會回調FrameDisplayEventReceiver的onVsync方法,並且到UI線程中執行它的run方法,調用doFrame方法
  4. doFrame中通過doCallbacks獲取到執行時間小於當前時間的一條CallbackRecord鏈表,並遍歷鏈表執行最開始添加的Runnable的run方法
  5. 這個Runnable中會執行doTraversal,doTraversal中移除同步屏障,並執行performTraversals開始測量,佈局,繪製流程

下一篇文章分析Vsync的監聽與上層的接收

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