Android 怎麼就不卡了呢之Choreographer

Drummor發表於2019-04-13

題外話

時光如斯 人世無常 逼哥忽然就被 全網封殺

難過 還沒有去聽過現場 總是感覺現在很忙 來日方長 我這種短視動物怎麼屑於把意外這種小概率東西納入考慮範圍呢 現在和將來這兩個選項悲觀點完全能翻譯成希望與永別

封殺原因未知 如果是因為XD 這個自媒體猖獗的時代 是個好時代 它把這個世界本有的模樣赤裸裸矗在你跟前 允許你懷疑思考進步 也是個壞時代 公信力成了偽命題 把人類最後那點遮羞布撕個粉碎 打擊相信人性是正直勇敢美麗的勇氣 如果是因為ZF 嗯 愛我中華

前言

針對Android UI不流暢的問題,Google提出了Project Butter對Android的顯示系統進行了重構。 這次重構的三個關鍵點

  • VSynch 垂直同步
  • Triple Buffer 三重快取
  • Choreographer 編舞者

這篇文章我們主要聊一聊Choregrapher,後續的我們寫關於其他。

choreographer

介面的顯示大體會經過CPU的計算-> GPU合成柵格化->顯示裝置顯示。我們知道Android裝置的重新整理頻率一般都是60HZ也就是一秒60次,如果一次繪製在約16毫喵內完成時沒有問題的。但是一旦出現不協調的地方就會出問題如下圖

  • 第一個週期,cpu計算、GPU操作、顯示裝置顯示沒有問題。
  • 第二個週期,顯示上一個週期繪製準備好的影象;CPU計算是在週期將要結束的時候才開始計算
  • 第三個週期,由於進入第二個週期的時候CPU動手計算晚點了,致使進入第三個週期的時候本應該顯示的影象沒有準備好,導致整個第三個週期內還是顯示上一個週期的影象,這樣看上去會卡,掉幀!google工程師們管整個叫Jank延誤軍機。

image

這事不小啊,怎麼解決呢?垂直同步 簡單的說,就是讓CPU計算別沒有計劃沒有規律而是在每個週期開始的時候開始計算,這樣就有條不紊的有序進行了(如下圖)。這個在android4.1及以後的版本中加入了Choreographer這個類,讓我們扒開看看他是怎麼實現的。 (Choreographer 這個類的位置android.view.Choreographer)

image

一、入口

1.1 postCallbackDelayedInternal

ViewRoot的 doTravle()方法中有這樣一行程式碼

mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
複製程式碼

它的意思就是對整個View樹發起測量佈局繪製操作。關於ViewRootImpl的更多內容這裡就不多介紹了。

以下方法

  • public void postCallback(...) ;
  • public void postCallbackDelayed(...);
  • public void postFrameCallback(FrameCallback callback);
  • public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis)

最終都會呼叫 postCallbackDelayedInternal();,那麼我們就看看這個方法的功能。

   private void postCallbackDelayedInternal(int callbackType,
                                             Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();//獲取當前時間
            final long dueTime = now + delayMillis;//到期時間
            //將執行動作放在mCallback陣列中
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
            //如果已經到期就註冊請求垂直同步訊號
            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
            //如果還沒有到期,使用handler在傳送一個延時訊息。這個延時訊息會在到期的時候執行。
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }
複製程式碼

1.2 FrameHandler

上一小節,如果已經到期就直接執行scheduleFrameLocked()方法,如果沒有執行就使用mHandler(FrameHandler型別)傳送一個what值為MSG_DO_SCHEDULE_CALLBACK的Message。到期後怎麼執行的呢。這要看FrameHandler怎麼處理的。

 private final class FrameHandler extends Handler {
        public FrameHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DO_FRAME:
                    doFrame(System.nanoTime(), 0);
                    break;
                case MSG_DO_SCHEDULE_VSYNC:
                    doScheduleVsync();
                    break;
                case MSG_DO_SCHEDULE_CALLBACK:
                //postCallbackDelayedInternal()方法中當未到期的時候傳送過來的
                    doScheduleCallback(msg.arg1);
                    break;
            }
        }
    }
複製程式碼

以上程式碼我們可以看出這個,FramHandler拿到 whate屬性值為MSG_DO_SCHEDULE_CALLBACK的時候會去執行 doScheduleCallback(msg.arg1);方法,跟進去看下

1.3 Choreography#doScheduleCallback

void doScheduleCallback(int callbackType) {
        synchronized (mLock) {
            if (!mFrameScheduled) {
                final long now = SystemClock.uptimeMillis();
                if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
                    scheduleFrameLocked(now);
                }
            }
        }
    }
複製程式碼

這個方法中先是做了一些判斷,mFrameSceduled為false 並且hasDueCallbacksLocked()這個方法的返回值為true,看方法名就能猜出這個callback是否到期了,下面我們再分析這個。最終如果滿足條件的情況下它會呼叫 scheduleFrameLocked()這個方法,咦這個方法眼熟不?對,沒錯,postCallbackDelayedInternal()方法中如果到期了的話就直接執行的那個方法。是時候看這個方法裡面搞的什麼事情了。

1.4 scheduleFrameLocked()


    private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;//設定標記位,表示已經安排請求下一幀渲染了。
            if (USE_VSYNC) {

                // 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 {
                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);
            }
        }
    }
複製程式碼
  • 這個方法的目的很明確就是安排,安排垂直同步而且立刻馬上儘快。安排垂直同步的條件是USE_VSYNC為true,也就是裝置支援垂直同步
  • 如果不是垂直同步就通過handler傳送一個延時一個週期的訊息安排垂直同步,這個Message的what值為 MSG_DO_FRAME,參照1.2的程式碼塊對what為MSG_DO_FRAME的訊息會去執行doFrame()方法。
  • 一個細節,當這個值mFrameScheduled為true的時候就直接返回不安排請求下一幀渲染了,如果為false,執行scheduleFrameLocked()方法繼續執行,並且將其設定為ture;在什麼時候設定為false的呢?詳細細節看附錄二

安排垂直同步的具體實現是FrameDisplayEventReceiver類他是DisplayEventReceiver的用於接收垂直訊號

    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) {
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);//Message設定為非同步
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }
        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
    }
複製程式碼

接收到垂直同步訊號後回撥onVsync方法,這個方法使用handler傳送帶callback(Runnable型別,自身已繼承)的message,最後run()中也是呼叫doFrame();(關於這個handler的這個操作詳細資訊邏輯,參照下面本文附錄一 handler 分發message

這個message設定為了非同步 (msg.setAsynchronous(true);)這意味這他有優先執行的權利,他是怎麼被優先執行的呢?參照附錄三 message的非同步模式

綜上,新增callback流程

image

二、執行

doFrame

    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }
        
            //當前時間
            startNanos = System.nanoTime();
            //當前時間和垂直同步時間
            final long jitterNanos = startNanos - frameTimeNanos;
            //垂直同步時間和當前時間的差值如果大於一個週期就修正一下
            if (jitterNanos >= mFrameIntervalNanos) {
            //取插值和始終週期的餘數
               final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
               //當前時間減去上一步得到的餘數當作最新的始終訊號時間
               frameTimeNanos = startNanos - lastFrameOffset;
            }
            //垂直同步時間上一次時間還小,就安排下次垂直,直接返回
            if (frameTimeNanos < mLastFrameTimeNanos) {
                scheduleVsyncLocked();
                return;
            }
            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        }

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

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

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

        if (DEBUG_FRAMES) {
            final long endNanos = System.nanoTime();
            Log.d(TAG, "Frame " + frame + ": Finished, took "
                    + (endNanos - startNanos) * 0.000001f + " ms, latency "
                    + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
        }
    }
複製程式碼
  1. 第一步修正判斷
  • 當前時間 startNanos = System.nanoTime();

  • 求當前時間和垂直同步時間的差值 :jitterNanos = startNanos - frameTimeNanos;

  • 垂直同步時間和當前時間的差值如果大於一個週期(jitterNanos >= mFrameIntervalNanos)就修正一下

    • 取插值和始終週期的餘數:lastFrameOffset = jitterNanos % mFrameIntervalNanos;
    • 當前時間減去上一步得到的餘數當作最新的始終訊號時間:frameTimeNanos = startNanos - lastFrameOffset;
  • 垂直同步時間上一次時間還小,就安排下次渲染: frameTimeNanos < mLastFrameTimeNanos,直接返回

  1. 第二步‍執行callback callback的執行順序是:
  • CALLBACK_INPUT輸入時間優先順序最高
  • CALLBACK_ANIMATION 動畫的次之
  • CALLBACK_TRAVERSAL UI繪製佈局的再次之
  • CALLBACK_COMMIT動畫修正相關最後。

2.2 doCallbacks();

  • CallbackQueue[] mCallbackQueues在取特定型別(輸入,動畫,佈局,Commit)的單向連結串列
  • 然後取出已到期的Callback/Runable執行

取出需要被執行的Actions

Action包裝在CallbackRecord中,是一個單向列表,按照時間的大小順序排列的。 取出待執行的Actions是通過CallBackQueue的extractDueCallbacksLocked()方法,可以把CallBackQueue看做是CallBack的管理類,其中還包括新增Action addCallbackLocked(),移除Action removeCallbacksLocked(),是否有帶起的Anction hasDueCallbacksLocked()方法。

 private final class CallbackQueue {
        //連結串列頭
        private CallbackRecord mHead;
        //是否存在已經到期的Action
        public boolean hasDueCallbacksLocked(long now) {
            return mHead != null && mHead.dueTime <= now;
        }
        //獲取已經到期的Action
        public CallbackRecord extractDueCallbacksLocked(long now) {
            ...
            return callbacks;
        }
        
        //新增Action
        public void addCallbackLocked(long dueTime, Object action, Object token) {
         ...
        }
        //移除Action
        public void removeCallbacksLocked(Object action, Object token) {
          ...
        }
    }
複製程式碼

執行Action

   for (CallbackRecord c = callbacks; c != null; c = c.next) {
                c.run(frameTimeNanos);
    }
    
複製程式碼

從callback中遍歷出CallBcakRecord,挨個執行。

三、小結

  • Choreographer對外提供了postCallback等方法,最終他們內部都是通過呼叫postCallbackDelayedInternal()實現這個方法主要會做兩件事情
    • 儲存Action
    • 請求垂直同步,垂直同步
  • 垂直同步回撥立馬執行Action(CallBack/Runnable)。
  • Action一個動作內容的型別可能是
    • CALLBACK_INPUT輸入時間優先順序最高
    • CALLBACK_ANIMATION 動畫的次之
    • CALLBACK_TRAVERSAL UI繪製佈局的再次之
    • CALLBACK_COMMIT動畫修正相關最後。
  • 複習Hanlder機制,我認為他是Android系統跑起來的大引擎終點關注下,handler對message的分發執行,以及“非同步模式”。

附二、關於handler執行Message

下面是handler分發邏輯,Looper在MessageQueue得到要執行的message之後就會交給message的target(Handler型別)屬性處理msg.target.dispatchMessage(msg);;

    public void dispatchMessage(Message msg) {
    //當msg的callback不為空的時候直接執行msg的callback它是一個Runnable物件
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
    //然後再交給mCallBack,它是handler的一個屬性, 
    //建立Handler的時候可以選擇傳入一個CallBack物件
    //當callBack中handleMessage返回true的時候表示:True if no further handling is desired(不需要進一步處理)
  
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
    //當mCallback處理返回為false的時候才去執行Handler自身的handleMessage()方法
            handleMessage(msg);
        }
    }
複製程式碼

關鍵邏輯在已註釋,小結一下 handler的執行分發Message邏輯

  1. 如果message的callback(runnable)屬性不為空,呼叫這個runable的run()方法執行
 private static void handleCallback(Message message) {
        message.callback.run();
    }

複製程式碼

當我們使用handler.post(Runnable r)方法時候就是將r設定給message的callback

  1. 上述條件不滿足的情況下,如果handler自身的mCallback不為空的時候就會,將message交給mCallback處理,handlerMessage()結束。這個屬性是在handler建立的時候傳入的。mCallback是CallBack型別,他是handler的一個內部介面。
    public interface Callback {
         boolean handleMessage(Message msg);
    }
複製程式碼

3.當messaga 的callBak為空,且handler的mCallBack為空的時候就交給自己的handlerMessage()方法執行了。我們在自定義handler的時候可以重寫這個方法對message進行相應的操作。

附二 、mFrameScheduled屬性作用

  • 執行callcack的時候會判斷mFrameScheduled屬性如果為false表示沒有安排渲染下一幀就直接返回,不執行。
  void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }
            ...
            ...
            mFrameScheduled = false;
           ...
        }
複製程式碼
  • 在scheduleFrameLocked()方法中,將mFrameScheduled值設定為ture表示安排了請求渲染下一幀。如果這時mFrameScheduled為true表示已經安排了下一幀那麼就返回,不添亂!

附三、Handler機制的非同步模式

作用

“非同步模式”的作用就是優先,asynchronous 的message在非同步模式下有優先執行的權。

用法

MessageQueue使用postSyncBarrier()方法新增屏障,removeSyncBarrier()方法移除屏障這個兩個方法是成對使用的。

實現原理

  • messageQueued的postSyncBarrier方法向messagequeue的頭部新增一個target屬性為null的message
  • messageQueue的next()當碰到target為null的message的時候就只會在message連結串列中取出去“非同步message”,而忽略普通的message,交給Looper做進一步分發處理。
 Message next() {
   ...
   for (;;) {
       if (nextPollTimeoutMillis != 0) {
           Binder.flushPendingCommands();
       }
       nativePollOnce(ptr, nextPollTimeoutMillis);
       synchronized (this) {
           // Try to retrieve the next message.  Return if found.
           final long now = SystemClock.uptimeMillis();
           Message prevMsg = null;
           Message msg = mMessages;
           if (msg != null && msg.target == null) {
               // Stalled by a barrier.  Find the next asynchronous messagin the queue.
               do {
                   prevMsg = msg;
                   msg = msg.next;
               } while (msg != null && !msg.isAsynchronous());
           }                       ...
           return msg;
         }
  ...
複製程式碼

完,水平有限,各位不吝批評指正。

相關文章