Android 流暢度檢測原理簡析

maweiliang發表於2018-05-02

android在不同的版本都會優化“UI的流暢性”問題,但是直到在android 4.1版本中做了有效的優化,這就是Project Butter。

Project Butter加入了三個核心元素:VSYNC、Triple Buffer和Choreographer。其中,VSYNC是理解Project Buffer的核心。VSYNC是Vertical Synchronization的縮寫 也就是“垂直同步”

  • VSYNC:產生一箇中斷訊號
  • Triple Buffer:當雙Buffer不夠使用時,該系統可分配第三塊Buffer
  • Choreographer:這個了用來接受一個VSYNC訊號來統一協調UI更新

檢測應用卡頓的方案,Android系統每隔16.6ms發出VSYNC訊號,來通知介面進行輸入、動畫、繪製等動作,每一次同步的週期為16.6ms,代表一幀的重新整理頻率,理論上來說兩次回撥的時間週期應該在16.6ms,如果超過了16.6ms我們則認為發生了卡頓,利用兩次回撥間的時間週期來判斷是否發生卡頓 這個方案的原理主要是通過Choreographer類設定它的FrameCallback函式,當每一幀被渲染時會觸發回撥FrameCallback, FrameCallback回撥void doFrame (long frameTimeNanos)函式。一次介面渲染會回撥doFrame方法,如果兩次doFrame之間的間隔大於16.6ms說明發生了卡頓。

監控應用的流暢度一般都是通過Choreographer類的postFrameCallback方法註冊一個VSYNC回撥事件

public static void start(final Builder builder) {
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            long lastFrameTimeNanos = 0;
            long currentFrameTimeNanos = 0;

            @Override
            public void doFrame(long frameTimeNanos) {
                if (lastFrameTimeNanos == 0) {
                    lastFrameTimeNanos = frameTimeNanos;
                    LogMonitor.getInstance().setFrequency(builder.frame * 17 / 2);
                    if (builder.targetPackageName != null) {
                        LogMonitor.getInstance().setTargetPackageName(builder.targetPackageName);
                    }
                    LogMonitor.getInstance().setDumpListener(builder.onDumpListener);
                }
                currentFrameTimeNanos = frameTimeNanos;
                skipFrameCount = skipFrameCount(lastFrameTimeNanos, currentFrameTimeNanos, deviceRefreshRateMs);
                LogMonitor.getInstance().setFrame(skipFrameCount);
                if (LogMonitor.getInstance().isMonitor()) {
                    LogMonitor.getInstance().removeMonitor();
                }
                LogMonitor.getInstance().startMonitor();
                lastFrameTimeNanos = currentFrameTimeNanos;
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }
複製程式碼

postFrameCallback(FrameCallback callback)的原始碼

 public void postFrameCallback(FrameCallback callback) {
        postFrameCallbackDelayed(callback, 0);
    }
......
 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);
    }
複製程式碼

postFrameCallback最終呼叫了postCallbackDelayedInternal()方法,我們在跟蹤進去這個方法

postCallbackDelayedInternal()
 private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        if (DEBUG_FRAMES) {
            Log.d(TAG, "PostCallback: type=" + callbackType
                    + ", action=" + action + ", token=" + token
                    + ", delayMillis=" + delayMillis);
        }

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            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方法中首選通過 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);將我們註冊的回撥介面新增到mCallbackQueues佇列中。每種型別的callback按照設定的執行時間(dueTime)順序排序分別儲存在一個單連結串列中。

之後判斷執行時間是否為當前時間,如果是直接呼叫scheduleFrameLocked(now);否則傳送一個MSG_DO_SCHEDULE_CALLBACK一個訊息,其實傳送資訊最後也是呼叫scheduleFrameLocked(now)方法,所以我們直接看這個方法的程式碼

scheduleFrameLocked
   private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {//預設為true
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "Scheduling next frame on 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,表示預設開啟垂直同步

private static final boolean USE_VSYNC = SystemProperties.getBoolean(
        "debug.choreographer.vsync", true);
複製程式碼

scheduleVsyncLocked方法的程式碼

private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }
......
/**
     * Schedules a single vertical sync pulse to be delivered when the next
     * display frame begins.
     */
    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);
        }
    }
複製程式碼

最終通過調native方法nativeScheduleVsync(mReceiverPtr)向底層註冊我們的回撥事件。 到此我們向系統註冊“垂直同步”事件的流程已經結束。所有的流程如下:

image

接收VSYNS訊號

VSYNC訊號怎麼同步到我們的程式碼的呢,Java 層接收 VSYNC 的入口是 dispatchVsync(),也就是說每當系統底層產生一個VSYNC訊號,系統都會回撥這個方法。

dispatchVsync()
  // Called from native code.
    @SuppressWarnings("unused")
    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }
複製程式碼
 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);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
    }
複製程式碼

onVsync方法中,Message.obtain(mHandler, this) 所以 msg.callback 是 this,最後會呼叫到 msg.callback.run(),也就是 FrameDisplayEventReceiver run(),進入 doFrame() mTimestampNanos,它是來自 onVsync 的 timestampNanos 引數,代表產生 VSYNC 的時間

void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }

            if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
                mDebugPrintNextFrameTimeDelta = false;
                Log.d(TAG, "Frame time delta: "
                        + ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
            }

            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();//正真開始的時間
            final long jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) {
                // 時間差除以每幀時間間隔,來計算丟掉了幾幀。其中mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());一般重新整理率為60,時間間隔為16.6ms
                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.");
                }
               // 取餘數,作為幀偏移時間
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                if (DEBUG_JANK) {
                    Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                            + "which is more than the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Skipping " + skippedFrames + " frames and setting frame "
                            + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
                }
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            if (frameTimeNanos < mLastFrameTimeNanos) {
                if (DEBUG_JANK) {
                    Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                            + "previously skipped frame.  Waiting for next vsync.");
                }
                scheduleVsyncLocked();
                return;
            }

            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        }
       //執行對應的callBack        
        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.");
        }
    }
複製程式碼

如果你平時注意卡頓的日誌資訊,那麼下面這個段log就不會陌生了

if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
     Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                           + "The application may be doing too much work on its main thread.");
複製程式碼

SKIPPED_FRAME_WARNING_LIMIT的預設值是30,也就說當我們的程式卡頓大於30時會列印這條log資訊 doFrame()方法最後呼叫doCallbacks()來處理使用者輸入,動畫,繪製等UI操作。

doCallbacks()方法
void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
        // We use "now" to determine when callbacks become due because it's possible
            // for earlier processing phases in a frame to post callbacks that should run
            // in a following phase, such as an input event that causes an animation to start.
            final long now = System.nanoTime();
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
            mCallbacksRunning = true;
            ......
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "RunCallback: type=" + callbackType
                            + ", action=" + c.action + ", token=" + c.token
                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
                }
                c.run(frameTimeNanos);
            }
        } finally {
            synchronized (mLock) {
                mCallbacksRunning = false;
                do {
                    final CallbackRecord next = callbacks.next;
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
複製程式碼

extractDueCallbacksLocked 是取出執行時間在當前時間之前的所有 CallbackRecord,CallbackRecord 是一個連結串列,然後遍歷 callbacks 執行 run 方法

  private static final class CallbackRecord {
        public CallbackRecord next;
        public long dueTime;
        public Object action; // Runnable or FrameCallback
        public Object token;

        public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
                ((Runnable)action).run();
            }
        }
    }
複製程式碼

如果們通過postFrameCallback(FrameCallback)註冊回撥事件,下一次 Choreographer doFrame 時就會呼叫 FrameCallback.doFrame,還記得剛開始我們註冊FrameCallback是系統為封裝FrameCallback的型別嗎 正是FRAME_CALLBACK_TOKEN,因此這裡會走 ((FrameCallback)action).doFrame(frameTimeNanos);,為什麼我們每次都需要註冊一個下呢,這是因為每次“垂直同步”都會刪除呼叫的註冊事件。 如果這個CallbackRecord是view動畫或繪製就會呼叫((Runnable)action).run();

下面是收到VSYNC的流程圖

image

小結
  1. Choreographer是執行緒單例的,而且必須要和一個Looper繫結,因為其內部有一個Handler需要和Looper繫結。
  2. 首先我們通過postFrameCallback(FrameCallback callback)方法,最終通過native方法 nativeScheduleVsync(mReceiverPtr)和一個VSYNC繫結。
  3. 當下一次VSYNC訊號來時,回撥我們繫結的介面,然後統計兩針之間的時間,判斷是否掉幀
  4. 如果掉幀獲取卡頓日誌,繼續監控下個VSYNC訊號

相關文章