Android Systrace 基礎知識(8) - Vsync-App :基於 Choreographer 的渲染機制詳解

本文是 Systrace 系列文章的第八篇,主要是對 Systrace 中的 Choreographer 進行簡單介紹

本系列的目的是通過 Systrace 這個工具,從另外一個角度來看待 Android 系統整體的運行,同時也從另外一個角度來對 Framework 進行學習。也許你看了很多講 Framework 的文章,但是總是記不住代碼,或者不清楚其運行的流程,也許從 Systrace 這個圖形化的角度,你可以理解的更深入一些。

本文介紹了 App 開發者不經常接觸到但是在 Android Framework 渲染鏈路中非常重要的一個類 Choreographer。包括 Choreographer 的引入背景、Choreographer 的簡介、部分源碼解析、Choreographer 與 MessageQueue、Choreographer 和 APM,以及手機廠商基於 Choreographer 的一些優化思路

Choreographer 的引入,主要是配合 Vsync ,給上層 App 的渲染提供一個穩定的 Message 處理的時機,也就是 Vsync 到來的時候 ,系統通過對 Vsync 信號週期的調整,來控制每一幀繪製操作的時機. 目前大部分手機都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系統爲了配合屏幕的刷新頻率,將 Vsync 的週期也設置爲 16.6 ms,每個 16.6 ms , Vsync 信號喚醒 Choreographer 來做 App 的繪製操作 ,這就是引入 Choreographer 的主要作用. 瞭解 Choreographer 還可以幫助 App 開發者知道程序每一幀運行的基本原理,也可以加深對 Message、Handler、Looper、MessageQueue、Measure、Layout、Draw 的理解

系列文章目錄

  1. Systrace 簡介
  2. Systrace 基礎知識 - Systrace 預備知識
  3. Systrace 基礎知識 - Why 60 fps ?
  4. Systrace 基礎知識 - SystemServer 解讀
  5. Systrace 基礎知識 - SurfaceFlinger 解讀
  6. Systrace 基礎知識 - Input 解讀
  7. Systrace 基礎知識 - Vsync 解讀
  8. Systrace 基礎知識 - Vsync-App :基於 Choreographer 的渲染機制詳解
  9. Systrace 基礎知識 - MainThread 和 RenderThread 解讀
  10. Systrace 基礎知識 - Triple Buffer 解讀
  11. Systrace 基礎知識 - CPU Info 解讀

主線程運行機制的本質

在講 Choreographer 之前,我們先理一下 Android 主線程運行的本質,其實就是 Message 的處理過程,我們的各種操作,包括每一幀的渲染操作 ,都是通過 Message 的形式發給主線程的 MessageQueue ,MessageQueue 處理完消息繼續等下一個消息,如下圖所示

主線程運行時的 MethodTrace 圖示

主線程運行時的 MethodTrace
主線程運行時的 MethodTrace

主線程運行時的 Systrace 圖示

主線程運行時的 Systrace
主線程運行時的 Systrace

演進

最開始 Android Framework 沒有 Vsync, 引入 Vsync 之前的 Android 版本,渲染一幀相關的 Message ,中間是沒有間隔的,上一幀繪製完,下一幀的 Message 緊接着就開始被處理。這樣的問題就是,幀率不穩定,可能高也可能低,不穩定,如下圖

沒有引入 Vsync 時主線程運行的 MethodTrace 圖示

MethodTrace_NoVsync
MethodTrace_NoVsync

沒有引入 Vsync 時主線程運行的 Systrace 圖示

Systrace_NoVsync
Systrace_NoVsync

可以看到這時候的瓶頸是在 dequeueBuffer, 因爲屏幕是有刷新週期的, FB 消耗 Front Buffer 的速度是一定的, 所以 SF 消耗 App Buffer 的速度也是一定的, 所以 App 會卡在 dequeueBuffer 這裏,這就會導致 App Buffer 獲取不穩定, 很容易就會出現卡頓掉幀的情況.

對於用戶來說,穩定的幀率纔是好的體驗,比如你玩王者榮耀,相比 fps 在 60 和 40 之間頻繁變化,用戶感覺更好的是穩定在 50 fps 的情況.

所以 Android 的演進中,引入了 Vsync + TripleBuffer + Choreographer 的機制,其主要目的就是提供一個穩定的幀率輸出機制,讓軟件層和硬件層可以以共同的頻率一起工作。

引入 Choreographer

Choreographer 的引入,主要是配合 Vsync ,給上層 App 的渲染提供一個穩定的 Message 處理的時機,也就是 Vsync 到來的時候 ,系統通過對 Vsync 信號週期的調整,來控制每一幀繪製操作的時機. 至於爲什麼 Vsync 週期選擇是 16.6ms (60 fps) ,是因爲目前大部分手機的屏幕都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系統爲了配合屏幕的刷新頻率,將 Vsync 的週期也設置爲 16.6 ms,每隔 16.6 ms ,Vsync 信號到來喚醒 Choreographer 來做 App 的繪製操作 ,如果每個 Vsync 週期應用都能渲染完成,那麼應用的 fps 就是 60 ,給用戶的感覺就是非常流暢,這就是引入 Choreographer 的主要作用

應用一幀顯示流程
應用一幀顯示流程

當然目前使用 90Hz 或者 120Hz 刷新率屏幕的手機越來越多,系統的 Vsync 週期從 16.6ms 到了 11.1ms 再到 8ms ,上圖中的操作要在更短的時間內完成,對性能的要求也越來越高,具體可以看新的流暢體驗,90Hz 漫談 這篇文章

Choreographer 簡介

Choreographer 扮演 Android 渲染鏈路中承上啓下的角色

  1. 承上:負責接收和處理 App 的各種更新消息和回調,等到 Vsync 到來的時候統一處理。比如集中處理 Input(主要是 Input 事件的處理) 、Animation(動畫相關)、Traversal(包括 measure、layout、draw 等操作) ,判斷卡頓掉幀情況,記錄 CallBack 耗時等
  2. 啓下:負責請求和接收 Vsync 信號。接收 Vsync 事件回調(通過 FrameDisplayEventReceiver.onVsync );請求 Vsync(FrameDisplayEventReceiver.scheduleVsync) .

從上面可以看出來, Choreographer 擔任的是一個工具人的角色,他之所以重要,是因爲通過 Choreographer + SurfaceFlinger + Vsync + TripleBuffer 這一套從上到下的機制,保證了 Android App 可以以一個穩定的幀率運行(目前大部分是 60fps),減少幀率波動帶來的不適感.

瞭解 Choreographer 還可以幫助 App 開發者知道程序每一幀運行的基本原理,也可以加深對 Message、Handler、Looper、MessageQueue、Measure、Layout、Draw 的理解 , 很多 APM 工具也用到了 Choreographer( 利用 FrameCallback + FrameInfo ) + MessageQueue ( 利用 IdleHandler ) + Looper ( 設置自定義 MessageLogging) 這些組合拳,深入瞭解了這些之後,再去做優化,腦子裏的思路會更清晰。

另外雖然畫圖是一個比較好的解釋流程的好路子,但是我個人不是很喜歡畫圖,因爲平時 Systrace 和 MethodTrace 用的比較多,Systrace 是按從左到右展示整個系統的運行情況的一個工具(包括 cpu、SurfaceFlinger、SystemServer、App 等關鍵進程),使用 SystraceMethodTrace 也可以很方便地展示關鍵流程。當你對系統代碼比較熟悉的時候,看 Systrace 就可以和手機運行的實際情況對應起來。所以下面的文章除了一些網圖之外,其他的我會多以 Systrace 來展示.

從 Systrace 的角度來看 Choreogrepher 的工作流程

下圖以滑動桌面爲例子,我們先看一下從左到右滑動桌面的一個完整的預覽圖(App 進程),可以看到 Systrace 中從左到右,每一個綠色的幀都表示一幀,表示最終我們可以手機上看到的畫面

  1. 圖中每一個灰色的條和白色的條寬度是一個 Vsync 的時間,也就是 16.6ms
  2. 每一幀處理的流程:接收到 Vsync 信號回調-> UI Thread --> RenderThread --> SurfaceFlinger(圖中未顯示)
  3. UI Thread 和 RenderThread 就可以完成 App 一幀的渲染,渲染完的 Buffer 拋給 SurfaceFlinger 去合成,然後我們就可以在屏幕上看到這一幀了
  4. 可以看到桌面滑動的每一幀耗時都很短(Ui Thread 耗時 + RenderThread 耗時),但是由於 Vsync 的存在,每一幀都會等到 Vsync 纔會去做處理
滑動桌面的 Systrace
滑動桌面的 Systrace

有了上面這個整體的概念,我們將 UI Thread 的每一幀放大來看,看看 Choreogrepher 的位置以及 Choreogrepher 是怎麼組織每一幀的

滑動桌面的詳細一幀的 Systrace
滑動桌面的詳細一幀的 Systrace

Choreographer 的工作流程

  1. Choreographer 初始化
  2. 初始化 FrameHandler ,綁定 Looper
  3. 初始化 FrameDisplayEventReceiver ,與 SurfaceFlinger 建立通信用於接收和請求 Vsync
  4. 初始化 CallBackQueues
  5. SurfaceFlinger 的 appEventThread 喚醒發送 Vsync ,Choreographer 回調 FrameDisplayEventReceiver.onVsync , 進入 SurfaceFlinger 的主處理函數 doFrame
  6. Choreographer.doFrame 計算掉幀邏輯
  7. Choreographer.doFrame 處理 Choreographer 的第一個 callback : input
  8. Choreographer.doFrame 處理 Choreographer 的第二個 callback : animation
  9. Choreographer.doFrame 處理 Choreographer 的第三個 callback : insets animation
  10. Choreographer.doFrame 處理 Choreographer 的第四個 callback : traversal
  11. traversal-draw 中 UIThread 與 RenderThread 同步數據
  12. Choreographer.doFrame 處理 Choreographer 的第五個 callback : commit ?
  13. RenderThread 處理繪製數據,真正進行渲染
  14. 將渲染好的 Buffer swap 給 SurfaceFlinger 進行合成

第一步初始化完成後,後續就會在步驟 2-9 之間循環

同時也附上這一幀所對應的 MethodTrace(這裏預覽一下即可,下面會有詳細的大圖)

MethodTrace
MethodTrace

下面我們就從源碼的角度,來看一下具體的實現

源碼解析

下面從源碼的角度來簡單看一下,源碼只摘抄了部分重要的邏輯,其他的邏輯則被剔除,另外 Native 部分與 SurfaceFlinger 交互的部分也沒有列入,不是本文的重點,有興趣的可以自己去跟一下。

Choreographer 的初始化

Choreographer 的單例初始化

// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
        new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        // 獲取當前線程的 Looper
        Looper looper = Looper.myLooper();
        ......
        // 構造 Choreographer 對象
        Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
        if (looper == Looper.getMainLooper()) {
            mMainInstance = choreographer;
        }
        return choreographer;
    }
};

Choreographer 的構造函數

private Choreographer(Looper looper, int vsyncSource) {
    mLooper = looper;
    // 1. 初始化 FrameHandler
    mHandler = new FrameHandler(looper);
    // 2. 初始化 DisplayEventReceiver
    mDisplayEventReceiver = USE_VSYNC
            ? new FrameDisplayEventReceiver(looper, vsyncSource)
            : null;
    mLastFrameTimeNanos = Long.MIN_VALUE;
    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
    //3. 初始化 CallbacksQueues
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
    ......
}

FrameHandler

private final class FrameHandler extends Handler {
    ......
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_DO_FRAME://開始渲染下一幀的操作
                doFrame(System.nanoTime(), 0);
                break;
            case MSG_DO_SCHEDULE_VSYNC://請求 Vsync
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK://處理 Callback
                doScheduleCallback(msg.arg1);
                break;
        }
    }
}

Choreographer 初始化鏈

在 Activity 啓動過程,執行完 onResume 後,會調用 Activity.makeVisible(),然後再調用到 addView(), 層層調用會進入如下方法

ActivityThread.handleResumeActivity(IBinder, boolean, boolean, String) (android.app) 
-->WindowManagerImpl.addView(View, LayoutParams) (android.view) 
  -->WindowManagerGlobal.addView(View, LayoutParams, Display, Window) (android.view) 
    -->ViewRootImpl.ViewRootImpl(Context, Display) (android.view) 
    public ViewRootImpl(Context context, Display display) {
        ......
        mChoreographer = Choreographer.getInstance();
        ......
    }

FrameDisplayEventReceiver 簡介

Vsync 的註冊、申請、接收都是通過 FrameDisplayEventReceiver 這個類,所以可以先簡單介紹一下。 FrameDisplayEventReceiver 繼承 DisplayEventReceiver , 有三個比較重要的方法

  1. onVsync -- Vsync 信號回調
  2. run -- 執行 doFrame
  3. scheduleVsync -- 請求 Vsync 信號
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
    ......
    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, 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() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }

    public void scheduleVsync() {
        ......
        nativeScheduleVsync(mReceiverPtr);
        ......
    }
}

Choreographer 中 Vsync 的註冊

從下面的函數調用棧可以看到,Choreographer 的內部類 FrameDisplayEventReceiver.onVsync 負責接收 Vsync 回調,通知 UIThread 進行數據處理。

那麼 FrameDisplayEventReceiver 是通過什麼方式在 Vsync 信號到來的時候回調 onVsync 呢?答案是 FrameDisplayEventReceiver 的初始化的時候,最終通過監聽文件句柄的形式,其對應的初始化流程如下

android/view/Choreographer.java

private Choreographer(Looper looper, int vsyncSource) {
    mLooper = looper;
    mDisplayEventReceiver = USE_VSYNC
            ? new FrameDisplayEventReceiver(looper, vsyncSource)
            : null;
    ......
}

android/view/Choreographer.java

public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
    super(looper, vsyncSource);
}

android/view/DisplayEventReceiver.java

public DisplayEventReceiver(Looper looper, int vsyncSource) {
    ......
    mMessageQueue = looper.getQueue();
    mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
            vsyncSource);
}

nativeInit 後續的代碼可以自己跟一下,可以對照這篇文章和源碼,由於篇幅比較多,這裏就不細寫了(https://www.jianshu.com/p/304f56f5d486) , 後續梳理好這一塊的邏輯後,會在另外的文章更新。

簡單來說,FrameDisplayEventReceiver 的初始化過程中,通過 BitTube(本質是一個 socket pair),來傳遞和請求 Vsync 事件,當 SurfaceFlinger 收到 Vsync 事件之後,通過 appEventThread 將這個事件通過 BitTube 傳給 DisplayEventDispatcher ,DisplayEventDispatcher 通過 BitTube 的接收端監聽到 Vsync 事件之後,回調 Choreographer.FrameDisplayEventReceiver.onVsync ,觸發開始一幀的繪製,如下圖

FrameDisplayEventReceiver
FrameDisplayEventReceiver

Choreographer 處理一幀的邏輯

Choreographer 處理繪製的邏輯核心在 Choreographer.doFrame 函數中,從下圖可以看到,FrameDisplayEventReceiver.onVsync post 了自己,其 run 方法直接調用了 doFrame 開始一幀的邏輯處理

android/view/Choreographer.java

public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
    ......
    mTimestampNanos = timestampNanos;
    mFrame = frame;
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
public void run() {
    mHavePendingVsync = false;
    doFrame(mTimestampNanos, mFrame);
}

doFrame 函數主要做下面幾件事

  1. 計算掉幀邏輯
  2. 記錄幀繪製信息
  3. 執行 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_INSETS_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT

計算掉幀邏輯

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        ......
        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.");
            }
        }
        ......
    }
    ......
}

Choreographer.doFrame 的掉幀檢測比較簡單,從下圖可以看到,Vsync 信號到來的時候會標記一個 start_time ,執行 doFrame 的時候標記一個 end_time ,這兩個時間差就是 Vsync 處理時延,也就是掉幀

掉幀檢測邏輯
掉幀檢測邏輯

我們以 Systrace 的掉幀的實際情況來看掉幀的計算邏輯

Systrace 中的掉幀邏輯
Systrace 中的掉幀邏輯

這裏需要注意的是,這種方法計算的掉幀,是前一幀的掉幀情況,而不是這一幀的掉幀情況,這個計算方法是有缺陷的,會導致有的掉幀沒有被計算到

記錄幀繪製信息

Choreographer 中 FrameInfo 來負責記錄幀的繪製信息,doFrame 執行的時候,會把每一個關鍵節點的繪製時間記錄下來,我們使用 dumpsys gfxinfo 就可以看到。當然 Choreographer 只是記錄了一部分,剩餘的部分在 hwui 那邊來記錄。

從 FrameInfo 這些標誌就可以看出記錄的內容,後面我們看 dumpsys gfxinfo 的時候數據就是按照這個來排列的

// Various flags set to provide extra metadata about the current frame
private static final int FLAGS = 0;

// Is this the first-draw following a window layout?
public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1;

// A renderer associated with just a Surface, not with a ViewRootImpl instance.
public static final long FLAG_SURFACE_CANVAS = 1 << 2;

@LongDef(flag = true, value = {
        FLAG_WINDOW_LAYOUT_CHANGED, FLAG_SURFACE_CANVAS })
@Retention(RetentionPolicy.SOURCE)
public @interface FrameInfoFlags {}

// The intended vsync time, unadjusted by jitter
private static final int INTENDED_VSYNC = 1;

// Jitter-adjusted vsync time, this is what was used as input into the
// animation & drawing system
private static final int VSYNC = 2;

// The time of the oldest input event
private static final int OLDEST_INPUT_EVENT = 3;

// The time of the newest input event
private static final int NEWEST_INPUT_EVENT = 4;

// When input event handling started
private static final int HANDLE_INPUT_START = 5;

// When animation evaluations started
private static final int ANIMATION_START = 6;

// When ViewRootImpl#performTraversals() started
private static final int PERFORM_TRAVERSALS_START = 7;

// When View:draw() started
private static final int DRAW_START = 8;

doFrame 函數記錄從 Vsync time 到 markPerformTraversalsStart 的時間

void doFrame(long frameTimeNanos, int frame) {
    ......
    mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
    // 處理 CALLBACK_INPUT Callbacks
    mFrameInfo.markInputHandlingStart();
    // 處理 CALLBACK_ANIMATION Callbacks
    mFrameInfo.markAnimationsStart();
    // 處理 CALLBACK_INSETS_ANIMATION Callbacks
    // 處理 CALLBACK_TRAVERSAL Callbacks
    mFrameInfo.markPerformTraversalsStart();
    // 處理 CALLBACK_COMMIT Callbacks
    ......
}

執行 Callbacks

void doFrame(long frameTimeNanos, int frame) {
    ......
    // 處理 CALLBACK_INPUT Callbacks
    doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
    // 處理 CALLBACK_ANIMATION Callbacks
    doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
    // 處理 CALLBACK_INSETS_ANIMATION Callbacks
    doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
    // 處理 CALLBACK_TRAVERSAL Callbacks
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
    // 處理 CALLBACK_COMMIT Callbacks
    doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    ......
}

Input 回調調用棧

input callback 一般是執行 ViewRootImpl.ConsumeBatchedInputRunnable

android/view/ViewRootImpl.java

final class ConsumeBatchedInputRunnable implements Runnable {
    @Override
    public void run() {
        doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
    }
}
void doConsumeBatchedInput(long frameTimeNanos) {
    if (mConsumeBatchedInputScheduled) {
        mConsumeBatchedInputScheduled = false;
        if (mInputEventReceiver != null) {
            if (mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos)
                    && frameTimeNanos != -1) {
                scheduleConsumeBatchedInput();
            }
        }
        doProcessInputEvents();
    }
}

Input 時間經過處理,最終會傳給 DecorView 的 dispatchTouchEvent,這就到了我們熟悉的 Input 事件分發

input 事件分發邏輯
input 事件分發邏輯

Animation 回調調用棧

一般我們接觸的多的是調用 View.postOnAnimation 的時候,會使用到 CALLBACK_ANIMATION

public void postOnAnimation(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        attachInfo.mViewRootImpl.mChoreographer.postCallback(
                Choreographer.CALLBACK_ANIMATION, action, null);
    } else {
        // Postpone the runnable until we know
        // on which thread it needs to run.
        getRunQueue().post(action);
    }
}

那麼一般是什麼時候回調用到 View.postOnAnimation 呢,我截取了一張圖,大家可以自己去看一下,接觸最多的應該是 startScroll,Fling 這種操作

CALLBACK_ANIMATION
CALLBACK_ANIMATION

其調用棧根據其 post 的內容,下面是桌面滑動鬆手之後的 fling 動畫。

fling
fling

另外我們的 Choreographer 的 FrameCallback 也是用的 CALLBACK_ANIMATION

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    if (callback == null) {
        throw new IllegalArgumentException("callback must not be null");
    }

    postCallbackDelayedInternal(CALLBACK_ANIMATION,
            callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

Traversal 調用棧

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //爲了提高優先級,先 postSyncBarrier
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        // 真正開始執行 measure、layout、draw
        doTraversal();
    }
}
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        // 這裏把 SyncBarrier remove
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        // 真正開始
        performTraversals();
    }
}
private void performTraversals() {
      // measure 操作
      if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged || updatedConfiguration) {
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
      // layout 操作
      if (didLayout) {
          performLayout(lp, mWidth, mHeight);
      }
      // draw 操作
      if (!cancelDraw && !newSurface) {
          performDraw();
      }
}

doTraversal 的 TraceView 示例

doTraversal
doTraversal

下一幀的 Vsync 請求

由於動畫、滑動、Fling 這些操作的存在,我們需要一個連續的、穩定的幀率輸出機制。這就涉及到了 Vsync 的請求邏輯,在連續的操作,比如動畫、滑動、Fling 這些情況下,每一幀的 doFrame 的時候,都會根據情況觸發下一個 Vsync 的申請,這樣我們就可以獲得連續的 Vsync 信號。

看下面的 scheduleTraversals 調用棧(scheduleTraversals 中會觸發 Vsync 請求)

scheduleTraversals
scheduleTraversals

我們比較熟悉的 invalidate 和 requestLayout 都會觸發 Vsync 信號請求

我們下面以 Animation 爲例,看看 Animation 是如何驅動下一個 Vsync ,來持續更新畫面的

ObjectAnimator 動畫驅動邏輯

android/animation/ObjectAnimator.java

public void start() {
    super.start();
}

android/animation/ValueAnimator.java

private void start(boolean playBackwards) {
    ......
    addAnimationCallback(0); // 動畫 start 的時候添加 Animation Callback
    ......
}
private void addAnimationCallback(long delay) {
    ......
    getAnimationHandler().addAnimationFrameCallback(this, delay);
}

android/animation/AnimationHandler.java

public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
    if (mAnimationCallbacks.size() == 0) {
        // post FrameCallback
        getProvider().postFrameCallback(mFrameCallback);
    }
    ......
}

// 這裏的 mFrameCallback 回調 doFrame,裏面 post了自己
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        doAnimationFrame(getProvider().getFrameTime());
        if (mAnimationCallbacks.size() > 0) {
            // post 自己
            getProvider().postFrameCallback(this);
        }
    }
};

調用 postFrameCallback 會走到 mChoreographer.postFrameCallback ,這裏就會觸發 Choreographer 的 Vsync 請求邏輯

android/animation/AnimationHandler.java

public void postFrameCallback(Choreographer.FrameCallback callback) {
    mChoreographer.postFrameCallback(callback);
}

android/view/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 ->scheduleVsyncLocked-> mDisplayEventReceiver.scheduleVsync ->nativeScheduleVsync
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

通過上面的 Animation.start 設置,利用了 Choreographer.FrameCallback 接口,每一幀都去請求下一個 Vsync 動畫過程中一幀的 TraceView 示例

Animation
Animation

源碼小結

  1. Choreographer 是線程單例的,而且必須要和一個 Looper 綁定,因爲其內部有一個 Handler 需要和 Looper 綁定,一般是 App 主線程的 Looper 綁定
  2. DisplayEventReceiver 是一個 abstract class,其 JNI 的代碼部分會創建一個 IDisplayEventConnection 的 Vsync 監聽者對象。這樣,來自 AppEventThread 的 VSYNC 中斷信號就可以傳遞給 Choreographer 對象了。當 Vsync 信號到來時,DisplayEventReceiver 的 onVsync 函數將被調用。
  3. DisplayEventReceiver 還有一個 scheduleVsync 函數。當應用需要繪製 UI 時,將首先申請一次 Vsync 中斷,然後再在中斷處理的 onVsync 函數去進行繪製。
  4. Choreographer 定義了一個 FrameCallback interface,每當 Vsync 到來時,其 doFrame 函數將被調用。這個接口對 Android Animation 的實現起了很大的幫助作用。以前都是自己控制時間,現在終於有了固定的時間中斷。
  5. Choreographer 的主要功能是,當收到 Vsync 信號時,去調用使用者通過 postCallback 設置的回調函數。目前一共定義了五種類型的回調,它們分別是:
  6. CALLBACK_INPUT : 處理輸入事件處理有關
  7. CALLBACK_ANIMATION : 處理 Animation 的處理有關
  8. CALLBACK_INSETS_ANIMATION : 處理 Insets Animation 的相關回調
  9. CALLBACK_TRAVERSAL : 處理和 UI 等控件繪製有關
  10. CALLBACK_COMMIT : 處理 Commit 相關回調
  11. ListView 的 Item 初始化(obtain\setup) 會在 input 裏面也會在 animation 裏面,這取決於
  12. CALLBACK_INPUTCALLBACK_ANIMATION 會修改 view 的屬性,所以要先與 CALLBACK_TRAVERSAL 執行

APM 與 Choreographer

由於 Choreographer 的位置,許多性能監控的手段都是利用 Choreographer 來做的,除了自帶的掉幀計算,Choreographer 提供的 FrameCallback 和 FrameInfo 都給 App 暴露了接口,讓 App 開發者可以通過這些方法監控自身 App 的性能,其中常用的方法如下:

  1. 利用 FrameCallback 的 doFrame 回調
  2. 利用 FrameInfo 進行監控
  3. 使用 :adb shell dumpsys gfxinfo framestats
  4. 示例 :adb shell dumpsys gfxinfo com.meizu.flyme.launcher framestats
  5. 利用 SurfaceFlinger 進行監控
  6. 使用 :adb shell dumpsys SurfaceFlinger --latency
  7. 示例 :adb shell dumpsys SurfaceFlinger --latency com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher#0
  8. 利用 SurfaceFlinger PageFlip 機制進行監控
  9. 使用 : adb service call SurfaceFlinger 1013
  10. 備註:需要系統權限
  11. Choreographer 自身的掉幀計算邏輯
  12. BlockCanary 基於 Looper 的性能監控

利用 FrameCallback 的 doFrame 回調

FrameCallback 接口

public interface FrameCallback {
    public void doFrame(long frameTimeNanos);
}

接口使用

Choreographer.getInstance().postFrameCallback(youOwnFrameCallback );

接口處理

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    ......
    postCallbackDelayedInternal(CALLBACK_ANIMATION,
            callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

TinyDancer 就是使用了這個方法來計算 FPS (https://github.com/friendlyrobotnyc/TinyDancer)

利用 FrameInfo 進行監控

adb shell dumpsys gfxinfo framestats

Window: StatusBar
Stats since: 17990256398ns
Total frames rendered: 1562
Janky frames: 361 (23.11%)
50th percentile: 6ms
90th percentile: 23ms
95th percentile: 36ms
99th percentile: 101ms
Number Missed Vsync: 33
Number High input latency: 683
Number Slow UI thread: 273
Number Slow bitmap uploads: 8
Number Slow issue draw commands: 18
Number Frame deadline missed: 287
HISTOGRAM: 5ms=670 6ms=128 7ms=84 8ms=63 9ms=38 10ms=23 11ms=21 12ms=20 13ms=25 14ms=39 15ms=65 16ms=36 17ms=51 18ms=37 19ms=41 20ms=20 21ms=19 22ms=18 23ms=15 24ms=14 25ms=8 26ms=4 27ms=6 28ms=3 29ms=4 30ms=2 31ms=2 32ms=6 34ms=12 36ms=10 38ms=9 40ms=3 42ms=4 44ms=5 46ms=8 48ms=6 53ms=6 57ms=4 61ms=1 65ms=0 69ms=2 73ms=2 77ms=3 81ms=4 85ms=1 89ms=2 93ms=0 97ms=2 101ms=1 105ms=1 109ms=1 113ms=1 117ms=1 121ms=2 125ms=1 129ms=0 133ms=1 150ms=2 200ms=3 250ms=0 300ms=1 350ms=1 400ms=0 450ms=0 500ms=0 550ms=0 600ms=0 650ms=0

---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,DequeueBufferDuration,QueueBufferDuration,
0,10158314881426,10158314881426,9223372036854775807,0,10158315693363,10158315760759,10158315769821,10158316032165,10158316627842,10158316838988,10158318055915,10158320387269,10158321770654,428000,773000,
0,10158332036261,10158332036261,9223372036854775807,0,10158332799196,10158332868519,10158332877269,10158333137738,10158333780654,10158333993206,10158335078467,10158337689561,10158339307061,474000,885000,
0,10158348665353,10158348665353,9223372036854775807,0,10158349710238,10158349773102,10158349780863,10158350405863,10158351135967,10158351360446,10158352300863,10158354305654,10158355814509,471000,836000,
0,10158365296729,10158365296729,9223372036854775807,0,10158365782373,10158365821019,10158365825238,10158365975290,10158366547946,10158366687217,10158367240706,10158368429248,10158369291852,269000,476000,

利用 SurfaceFlinger 進行監控

命令解釋:

  1. 數據的單位是納秒,時間是以開機時間爲起始點
  2. 每一次的命令都會得到 128 行的幀相關的數據

數據:

  1. 第一行數據,表示刷新的時間間隔 refresh_period
  2. 第 1 列:這一部分的數據表示應用程序繪製圖像的時間點
  3. 第 2 列:在 SF(軟件)將幀提交給 H/W(硬件)繪製之前的垂直同步時間,也就是每幀繪製完提交到硬件的時間戳,該列就是垂直同步的時間戳
  4. 第 3 列:在 SF 將幀提交給 H/W 的時間點,算是 H/W 接受完 SF 發來數據的時間點,繪製完成的時間點。

掉幀 jank 計算

每一行都可以通過下面的公式得到一個值,該值是一個標準,我們稱爲 jankflag,如果當前行的 jankflag 與上一行的 jankflag 發生改變,那麼就叫掉幀

ceil((C - A) / refresh-period)

利用 SurfaceFlinger PageFlip 機制進行監控

Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
                data.writeInterfaceToken("android.ui.ISurfaceComposer");
mFlinger.transact(1013, data, reply, 0);
final int pageFlipCount = reply.readInt();

final long now = System.nanoTime();
final int frames = pageFlipCount - mLastPageFlipCount;
final long duration = now - mLastUpdateTime;
mFps = (float) (frames * 1e9 / duration);
mLastPageFlipCount = pageFlipCount;
mLastUpdateTime = now;
reply.recycle();
data.recycle();

Choreographer 自身的掉幀計算邏輯

SKIPPED_FRAME_WARNING_LIMIT 默認爲 30 , 由 debug.choreographer.skipwarning 這個屬性控制

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

BlockCanary

Blockcanary 計算做性能監控使用的是 Looper 的消息機制,通過對 MessageQueue 中每一個 Message 的前後進行記錄,打到監控性能的目的

android/os/Looper.java

public static void loop() {
    ...
    for (;;) {
        ...
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        msg.target.dispatchMessage(msg);
        if (logging != null) {
            logging.println("<<<<<< Finished to " + msg.target + " " + msg.callback);
        }

    }
}

MessageQueue 與 Choreographer

所謂的異步消息其實就是這樣的,我們可以通過 enqueueBarrier 往消息隊列中插入一個 Barrier,那麼隊列中執行時間在這個 Barrier 以後的同步消息都會被這個 Barrier 攔截住無法執行,直到我們調用 removeBarrier 移除了這個 Barrier,而異步消息則沒有影響,消息默認就是同步消息,除非我們調用了 Message 的 setAsynchronous,這個方法是隱藏的。只有在初始化 Handler 時通過參數指定往這個 Handler 發送的消息都是異步的,這樣在 Handler 的 enqueueMessage 中就會調用 Message 的 setAsynchronous 設置消息是異步的,從上面 Handler.enqueueMessage 的代碼中可以看到。

所謂異步消息,其實只有一個作用,就是在設置 Barrier 時仍可以不受 Barrier 的影響被正常處理,如果沒有設置 Barrier,異步消息就與同步消息沒有區別,可以通過 removeSyncBarrier 移除 Barrier

SyncBarrier 在 Choreographer 中使用的一個示例

scheduleTraversals 的時候 postSyncBarrier

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //爲了提高優先級,先 postSyncBarrier
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}

doTraversal 的時候 removeSyncBarrier

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        // 這裏把 SyncBarrier remove
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        // 真正開始
        performTraversals();
    }
}

Choreographer post Message 的時候,會把這些消息設爲 Asynchronous ,這樣 Choreographer 中的這些 Message 的優先級就會比較高,

Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);

廠商優化

系統廠商由於可以直接修改源碼,也利用這方面的便利,做一些功能和優化,不過由於保密的問題,代碼就不直接放上來了,我可以大概說一下思路,感興趣的可以私下討論

移動事件優化

Choreographer 本身是沒有 input 消息的, 不過修改源碼之後,input 消息可以直接給到 Choreographer 這裏, 有了這些 Input 消息,Choreographer 就可以做一些事情,比如說提前響應,不去等 Vsync

後臺動畫優化

當一個 Android App 退到後臺之後,只要他沒有被殺死,那麼他做什麼事情大家都不要奇怪,因爲這就是 Android。有的 App 退到後臺之後還在持續調用 Choreographer 中的 Animation Callback,而這個 Callback 的執行完全是無意義的,而且用戶還不知道,但是對 cpu 的佔用是比較高的。

所以在 Choreographer 中會針對這種情況做優化,禁止不符合條件的 App 在後臺繼續無用的操作

Background Animation
Background Animation

幀繪製優化

和移動事件優化一樣,由於有了 Input 事件的信息,在某些場景下我們可以通知 SurfaceFlinger 不用取等待 Vsync 直接做合成操作

應用啓動優化

我們前面說,主線程的所有操作都是給予 Message 的 ,如果某個操作,非重要的 Message 被排列到了隊列後面,那麼對這個操作產生影響;而通過重新排列 MessageQueue,在應用啓動的時候,把啓動相關的重要的啓動 Message 放到隊列前面,來起到加快啓動速度的作用

高幀率優化

90 或者 120 fps 的手機上 , Vsync 間隔從 16.6ms 變成了 11.1ms 或者 8ms,這帶來了巨大的性能和功耗挑戰,如何在一幀內完成渲染的必要操作,是手機廠商必須要思考和優化的地方:

  1. 超級 App 的性能表現以及優化
  2. 遊戲高幀率合作
  3. 120fps , 90 fps 和 60 fps 相互切換的邏輯

參考資料

  1. https://www.jianshu.com/p/304f56f5d486
  2. http://gityuan.com/2017/02/25/choreographer/
  3. https://developer.android.com/reference/android/view/Choreographer
  4. https://www.jishuwen.com/d/2Vcc
  5. https://juejin.im/entry/5c8772eee51d456cda2e8099
  6. Android 開發高手課

關於我

小廠系統研發工程師 , 更多信息可以點擊 關於我 , 非常希望和大家一起交流 , 共同進步 .

一個人可以走的更快 , 一羣人可以走的更遠

本文使用 mdnice 排版

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