面試官又來了:你的app卡頓過嗎?

1故事開始

面試官:

平時開發中有遇到卡頓問題嗎?你通常是如何處理的?

來面試的小夥:java

額...沒有遇到過卡頓問題,我平時寫的代碼質量比較高,不會出現卡頓。git

面試官:...github

上面對話像是開玩笑,可是前段時間真的遇到一個來面試的小夥這樣答,問他有沒有遇到過卡頓問題,通常怎麼處理的?面試

他說沒遇到過,說他寫的代碼不會出現卡頓。這回答彷佛沒啥問題,可是我會認爲你在卡頓優化這一塊是0經驗。算法

卡頓這個話題,相信大部分兩年或以上工做經驗的同窗都應該能說出個大概。微信

通常的回答可能相似這樣:架構

卡頓是因爲主線程有耗時操做,致使View繪製掉幀,屏幕每16毫秒會刷新一次,也就是每秒會刷新60次,人眼能感受到卡頓的幀率是每秒24幀。因此解決卡頓的辦法就是:耗時操做放到子線程、View的層級不能太多、要合理使用include、ViewStub標籤等等這些,來保證每秒畫24幀以上。

若是稍微問深一點,卡頓的底層原理是什麼?如何理解16毫秒刷新一次?假如界面沒有更新操做,View會每16毫秒draw一次嗎?app

這個問題相信會難倒一片人,包括大部分3年以上經驗的同窗,若是沒有去閱讀源碼,未必能答好這個問題。異步

固然,我但願你恰好是小部分人~async

接下來將從源碼角度分析屏幕刷新機制,深刻理解卡頓原理,以及介紹卡頓監控的幾種方式,但願對你有幫助。

2屏幕刷新機制

從 View#requestLayout 開始分析,由於這個方法是主動請求UI更新,從這裏分析徹底沒問題。

1. View#requestLayout

protected ViewParent mParent;
...
public void requestLayout() {
    ...

    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout(); //1
    }
}

主要看註釋1,這裏的 mParent.requestLayout(),最終會調用 ViewRootImpl 的 requestLayout 方法。

你可能會問,爲何是ViewRootImpl?

由於根View是DecorView,而DecorView的parent就是ViewRootImpl,具體看ViewRootImpl的setView方法裏調用view.assignParent(this);,能夠暫且先認爲就是這樣的,以後整理View的繪製流程的時候會詳細分析。

2. ViewRootImpl#requestLayout

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        //1 檢測線程
        checkThread();
        mLayoutRequested = true;
        //2 
        scheduleTraversals();
    }
}

註釋1 是檢測當前是否是在主線程

2.1 ViewRootImpl#checkThread

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

這個異常很熟悉吧,咱們平時說的子線程不能更新UI,會拋異常,就是在這裏判斷的ViewRootImpl#checkThread。

接着看註釋2

2.2 ViewRootImpl#scheduleTraversals

void scheduleTraversals() {
    //一、注意這個標誌位,屢次調用 requestLayout,要這個標誌位false纔有效
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 2. 同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 3. 向 Choreographer 提交一個任務
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        //繪製前發一個通知
        notifyRendererOfFramePending();
        //這個是釋放鎖,先無論
        pokeDrawLockIfNeeded();
    }
}

主要看註釋的3點:

註釋1:防止短期屢次調用 requestLayout 重複繪製屢次,假如調用requestLayout 以後尚未到這一幀繪製完成,再次調用是沒什麼意義的。

註釋2: 涉及到Handler的一個知識點,同步屏障:往消息隊列插入一個同步屏障消息,這時候消息隊列中的同步消息不會被處理,而是優先處理異步消息。

這裏很好理解,UI相關的操做優先級最高,好比消息隊列有不少沒處理完的任務,這時候啓動一個Activity,固然要優先處理Activity啓動,而後再去處理其餘的消息,同步屏障的設計堪稱一絕吧。 

同步屏障的處理代碼在MessageQueue的next方法:

Message next() {
...
    for (;;) {
       ...
      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) { //若是msg不爲空而且target爲空
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
  ...
}

邏輯就是:若是msg不爲空而且target爲空,說明是一個同步屏障消息,進入do while循環,遍歷鏈表,直到找到異步消息msg.isAsynchronous()才跳出循環交給Handler去處理這個異步消息。

回到上面的註釋3:mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);,往Choreographer 提交一個任務 mTraversalRunnable,這個任務不會立刻就執行,接着看~

3.  Choreographer

看下 mChoreographer.postCallback

3.1 Choreographer#postCallback

public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}

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) {
    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;
        //1.將任務添加到隊列
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        //2. 正常延時是0,走這裏
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            //3. 何時會有延時,繪製超時,等下一個vsync?
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

入參 callbackType 這裏傳的是 Choreographer.CALLBACK_TRAVERSAL,後面會說到,最終調用了 postCallbackDelayedInternal 方法。

註釋1:將任務添加到隊列,不會立刻執行,後面會用到。

註釋2: scheduleFrameLocked,正常的狀況下delayMillis是0,走這裏,看下面分析。

註釋3:什麼狀況下會有延時,TextView中有調用到,暫時無論。

3.2. Choreographer#scheduleFrameLocked

// Enable/disable vsync for animations and drawing. 系統屬性參數,默認true
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
        "debug.choreographer.vsync", true);
...
private void scheduleFrameLocked(long now) {
    //標誌位,避免沒必要要的屢次調用
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            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.
            //1 若是當前線程是UI線程,直接執行scheduleFrameLocked,不然經過Handler處理
            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);
        }
    }
}

這個方法有個系統參數判斷,默認true,咱們分析true的狀況。

註釋1: 判斷當前線程若是是UI線程,直接執行scheduleVsyncLocked方法,不然,經過Handler發一個異步消息到消息隊列,最終也是到主線程處理,因此直接看scheduleVsyncLocked方法。

3.3 Choreographer#scheduleVsyncLocked

private final FrameDisplayEventReceiver mDisplayEventReceiver;

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

調用 DisplayEventReceiver 的 scheduleVsync 方法

4. DisplayEventReceiver

4.1 DisplayEventReceiver#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);  //一、請求vsync
    }
}

// Called from native code. //二、vsync來的時候底層會經過JNI回調這個方法
@SuppressWarnings("unused")
private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
    onVsync(timestampNanos, builtInDisplayId, frame);
}

這裏的邏輯就是:經過JNI,跟底層說,下一個vsync脈衝信號來的時候請通知我。而後在下一個vsync信號來的時候,就會收到底層的JNI回調,也就是dispatchVsync這個方法會被調用,而後會調用onVsync這個空方法,由實現類去本身作一些處理。

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

這裏是屏幕刷新機制的重點,應用必須向底層請求vsync信號,而後下一次vsync信號來的時候會經過JNI通知到應用,而後接下來纔到應用繪製邏輯。

往回看,DisplayEventReceiver的實現類是 Choreographer 的內部類 FrameDisplayEventReceiver,代碼很少,直接貼上來

5. Choreographer

5.1 Choreographer$FrameDisplayEventReceiver

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;

    public FrameDisplayEventReceiver(Looper looper) {
        super(looper);
    }

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

        long now = System.nanoTime();
        // 更正時間戳,當前納秒
        if (timestampNanos > now) {
            Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                    + " ms in the future!  Check that graphics HAL is generating vsync "
                    + "timestamps using the correct timebase.");
            timestampNanos = now;
        }

        if (mHavePendingVsync) {
            Log.w(TAG, "Already have a pending vsync event.  There should only be "
                    + "one at a time.");
        } else {
            mHavePendingVsync = true;
        }

        mTimestampNanos = timestampNanos;
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this); //1 callback是this,會回調run方法
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame); //2
    }
}

根據上面4.1分析,收到vsync信號後,onVsync方法就會被調用,裏面主要作了什麼呢?

經過Handler,往消息隊列插入一個異步消息,指定執行的時間,而後看註釋1,callback傳this,因此最終會回調run方法,run裏面調用doFrame(mTimestampNanos, mFrame);

重點來了,若是Handler此時存在耗時操做,那麼須要等耗時操做執行完,Looper纔會輪循到下一條消息,run方法纔會調用,而後纔會調用到doFrame(mTimestampNanos, mFrame);,doFrame幹了什麼?調用慢了會怎麼樣?

繼續看

5.2 Choreographer#doFrame

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        ...

        long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        // 1 當前時間戳減去vsync來的時間,也就是主線程的耗時時間
        final long jitterNanos = startNanos - frameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            //1幀是16毫秒,計算當前跳過了多少幀,好比超時162毫秒,那麼就是跳過了10幀
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            // SKIPPED_FRAME_WARNING_LIMIT 默認是30,超時了30幀以上,那麼就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.");
            }
            // 取餘,計算離上一幀多久了,一幀是16毫秒,因此lastFrameOffset 在0-15毫秒之間,這裏單位是納秒
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
            if (DEBUG_JANK) {
                Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                        + "which is more than the 8frame interval of "
                        + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                        + "Skipping " + skippedFrames + " frames and setting frame "
                        + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
            }
            // 出現掉幀,把時間修正一下,對比的是上一幀時間
            frameTimeNanos = startNanos - lastFrameOffset;
        }
        //二、時間倒退了,多是因爲改了系統時間,此時就從新申請vsync信號(通常不會走這裏)
        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.");
            }
            //這裏申請下一次vsync信號,流程跟上面分析同樣了。
            scheduleVsyncLocked();
            return;
        }

        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        mFrameScheduled = false;
        mLastFrameTimeNanos = frameTimeNanos;
    }

    //3 能繪製的話,就走到下面
    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);
    } 
}

分析:

1. 計算收到vsync信號到doFrame被調用的時間差,vsync信號間隔是16毫秒一次,大於16毫秒就是掉幀了,若是超過30幀(默認30),就打印log提示開發者檢查主線程是否有耗時操做。

2. 若是時間發生倒退,多是修改了系統時間,就不繪製,而是從新註冊下一次vsync信號 3. 正常狀況下會走到 doCallbacks 裏去,callbackType 按順序是Choreographer.CALLBACK_INPUT、Choreographer.CALLBACK_ANIMATION、Choreographer.CALLBACK_TRAVERSAL、Choreographer.CALLBACK_COMMIT

看 doCallbacks 裏的邏輯

5.3 Choreographer#doCallbacks

void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    synchronized (mLock) {
        final long now = System.nanoTime();
        //1. 從隊列取出任務,任務何時添加到隊列的,上面有說過哈
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true;
        ...
        //2.更新這一幀的時間,確保提交這一幀的時間老是在最後一幀以後
        if (callbackType == Choreographer.CALLBACK_COMMIT) {
            final long jitterNanos = now - frameTimeNanos;
            Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
            if (jitterNanos >= 2 * mFrameIntervalNanos) {
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
                        + mFrameIntervalNanos;
                if (DEBUG_JANK) {
                    Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
                            + " ms which is more than twice the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Setting frame time to " + (lastFrameOffset * 0.000001f)
                            + " ms in the past.");
                    mDebugPrintNextFrameTimeDelta = true;
                }
                frameTimeNanos = now - lastFrameOffset;
                mLastFrameTimeNanos = frameTimeNanos;
            }
        }
    }
    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));
            }
            // 3. 執行任務,
            c.run(frameTimeNanos);
        }
    } ...
}

這裏主要就是取出對應類型的任務,而後執行任務。

註釋2:if (callbackType == Choreographer.CALLBACK_COMMIT)是流程的最後一步,數據已經繪製完準備提交的時候,會更正一下時間戳,確保提交時間老是在最後一次vsync時間以後。這裏文字可能不太好理解,引用一張圖

圖中 doCallbacks 從 frameTimeNanos2 開始執行,執行到進入 CALLBACK_COMMIT 時,通過了2.2幀,判斷 now - frameTimeNanos >= 2 * mFrameIntervalNanos,lastFrameOffset = jitterNanos % mFrameIntervalNanos取餘就是0.2了,因而修正的時間戳 frameTimeNanos = now - lastFrameOffset 恰好就是3的位置。

註釋3,還沒到最後一步的時候,取出其它任務出來run,這個任務確定就是跟View的繪製相關了,記得開始requestLayout傳過來的類型嗎,Choreographer.CALLBACK_TRAVERSAL,從隊列get出來的任務類對應是mTraversalRunnable,類型是TraversalRunnable,定義在ViewRootImpl裏面,饒了一圈,回到ViewRootImpl繼續看~

6. ViewRootImpl

剛開始看的是ViewRootImpl#scheduleTraversals,繼續往下分析

6.1 ViewRootImpl#scheduleTraversals

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        ...
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}

這個mTraversalRunnable 任務繞了一圈,經過請求vsync信號,到收到信號,而後終於被調用了。

6.2 ViewRootImpl$TraversalRunnable

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

6.3 ViewRootImpl#doTraversal

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        performTraversals();
    }
}

先移除同步屏障消息,而後調用performTraversals 方法,performTraversals 這個方法代碼有點多,挑重點看

6.4 ViewRootImpl#performTraversals

private void performTraversals() {

    // mAttachInfo 賦值給View
    host.dispatchAttachedToWindow(mAttachInfo, 0);

    // Execute enqueued actions on every traversal in case a detached view enqueued an action
    getRunQueue().executeActions(mAttachInfo.mHandler);

    ... 
  //1 測量
    if (!mStopped || mReportNextDraw) {

        // Ask host how big it wants to be
        //1.1測量一次
         performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

        / Implementation of weights from WindowManager.LayoutParams
        // We just grow the dimensions as needed and re-measure if
        // needs be

        if (lp.horizontalWeight &gt; 0.0f) {
            width += (int) ((mWidth - width) * lp.horizontalWeight);
            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                    MeasureSpec.EXACTLY);
            measureAgain = true;
        }
        if (lp.verticalWeight &gt; 0.0f) {
            height += (int) ((mHeight - height) * lp.verticalWeight);
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                    MeasureSpec.EXACTLY);
            measureAgain = true;
        }
        //1.二、若是有設置權重,好比LinearLayout設置了weight,須要測量兩次
        if (measureAgain) {
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        }

    }

    ... 
    //2.佈局
    if (didLayout) {
        // 會回調View的layout方法,而後會調用View的onLayout方法
        performLayout(lp, mWidth, mHeight);

    }

    ... 
    //3.畫
    if (!cancelDraw &amp;&amp; !newSurface) {
        performDraw();
    }

}

能夠看到,View的三個方法回調measure、layout、draw是在performTraversals 裏面,須要注意的點是LinearLayout設置權重的狀況下會measure兩次。

到這裏,屏幕刷新機制就分析完了,整個流程總結一下:

7. 小結

View 的 requestLayout 會調到ViewRootImpl 的 requestLayout方法,而後經過 scheduleTraversals 方法向Choreographer 提交一個繪製任務,而後再經過DisplayEventReceiver向底層請求vsync信號,當vsync信號來的時候,會經過JNI回調回來,經過Handler往主線程消息隊列post一個異步任務,最終是ViewRootImpl去執行那個繪製任務,調用performTraversals方法,裏面是View的三個方法的回調。

網上的流程圖雖然很漂亮,可是不如本身畫一張印象深入

認真看完,想必你們對屏幕刷新機制應該清楚了:

應用須要主動請求vsync,vsync來的時候纔會經過JNI通知到應用,而後才調用View的三個繪製方法。若是沒有發起繪製請求,例如沒有requestLayout,View的繪製方法是不會被調用的。ViewRootImpl裏面的這個View實際上是DecorView。

那麼有兩個地方會形成掉幀,一個是主線程有其它耗時操做,致使doFrame沒有機會在vsync信號發出以後16毫秒內調用,對應下圖的3;還有一個就是當前doFrame方法耗時,繪製過久,下一個vsync信號來的時候這一幀還沒畫完,形成掉幀,對應下圖的2。1是正常的

這一張圖很形象,你們能夠參考這張圖本身研究研究。

3如何監控應用卡頓?

上面從源碼角度分析了屏幕刷新機制,爲何主線程有耗時操做會致使卡頓?原理想必你們已經心中有數,那麼平時開發中如何去發現那些會形成卡頓的代碼呢?

接下來總結幾種比較流行、有效的卡頓監控方式:

2.1 基於消息隊列

2.1.1 替換 Looper 的 Printer

Looper 暴露了一個方法

public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

在Looper 的loop方法有這樣一段代碼

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

Looper輪循的時候,每次從消息隊列取出一條消息,若是logging不爲空,就會調用 logging.println,咱們能夠經過設置Printer,計算Looper兩次獲取消息的時間差,若是時間太長就說明Handler處理時間過長,直接把堆棧信息打印出來,就能夠定位到耗時代碼。

不過println 方法參數涉及到字符串拼接,考慮性能問題,因此這種方式只推薦在Debug模式下使用。基於此原理的開源庫表明是:BlockCanary,看下BlockCanary核心代碼:

https://github.com/markzhai/A...

類:LooperMonitor

public void println(String x) {
    if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
        return;
    }
    if (!mPrintingStarted) {
        //一、記錄第一次執行時間,mStartTimestamp
        mStartTimestamp = System.currentTimeMillis();
        mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
        mPrintingStarted = true;
        startDump(); //二、開始dump堆棧信息
    } else {
        //三、第二次就進來這裏了,調用isBlock 判斷是否卡頓
        final long endTime = System.currentTimeMillis();
        mPrintingStarted = false;
        if (isBlock(endTime)) {
            notifyBlockEvent(endTime);
        }
        stopDump(); //四、結束dump堆棧信息
    }
}

//判斷是否卡頓的代碼很簡單,跟上次處理消息時間比較,好比大於3秒,就認爲卡頓了
 private boolean isBlock(long endTime) {
    return endTime - mStartTimestamp > mBlockThresholdMillis;
}

原理是這樣,比較Looper兩次處理消息的時間差,好比大於3秒,就認爲卡頓了。細節的話你們能夠本身去研究源碼,好比消息隊列只有一條消息,隔了好久纔有消息入隊,這種狀況應該是要處理的,BlockCanary是怎麼處理的呢?

在Android開發高手課中張紹文說過微信內部的基於消息隊列的監控方案有缺陷:

這個我在BlockCanary 中測試,並無出現此問題,因此BlockCanary 是怎麼處理的?簡單分析一下源碼:

上面這段代碼,註釋1和註釋2,記錄第一次處理的時間,同時調用startDump()方法,startDump()最終會經過Handler 去執行一個AbstractSampler 類的mRunnable,代碼以下:

abstract class AbstractSampler {

    private static final int DEFAULT_SAMPLE_INTERVAL = 300;

    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
    protected long mSampleInterval;

    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            doSample();
            //調用startDump 的時候設置true了,stop時設置false
            if (mShouldSample.get()) {  
                HandlerThreadFactory.getTimerThreadHandler()
                        .postDelayed(mRunnable, mSampleInterval);
            }
        }
    };

能夠看到,調用doSample以後又經過Handler執行mRunnable,等因而循環調用doSample,直到stopDump被調用。

doSample方法有兩個類實現,StackSampler和CpuSampler,分析堆棧就看StackSampler的doSample方法

protected void doSample() {
        StringBuilder stringBuilder = new StringBuilder();
        // 獲取堆棧信息
        for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
            stringBuilder
                    .append(stackTraceElement.toString())
                    .append(BlockInfo.SEPARATOR);
        }

        synchronized (sStackMap) {
            // LinkedHashMap中數據超過100個就remove掉鏈表最前面的
            if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
                sStackMap.remove(sStackMap.keySet().iterator().next());
            }
            //放入LinkedHashMap,時間做爲key,value是堆棧信息
            sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
        }
    }

因此,BlockCanary 能作到連續調用幾個方法也能準確揪出耗時是哪一個方法,是採用開啓循環去獲取堆棧信息並保存到LinkedHashMap的方式,避免出現誤判或者漏判。核心代碼就先分析到這裏,其它細節你們能夠本身去看源碼。

2.1.2 插入空消息到消息隊列

這種方式能夠了解一下。

經過一個監控線程,每隔1秒向主線程消息隊列的頭部插入一條空消息。假設1秒後這個消息並無被主線程消費掉,說明阻塞消息運行的時間在0~1秒之間。換句話說,若是咱們須要監控3秒卡頓,那在第4次輪詢中,頭部消息依然沒有被消費的話,就能夠肯定主線程出現了一次3秒以上的卡頓。

2.2 插樁

編譯過程插樁(例如使用AspectJ),在方法入口和出口加入耗時監控的代碼。

原來的方法:

public void test(){
    doSomething();
}

經過編譯插樁以後的方法相似這樣

public void test(){
    long startTime = System.currentTimeMillis();
    doSomething();
    long methodTime = System.currentTimeMillis() - startTime;//計算方法耗時
}

固然,原理是這樣,實際上可能須要封裝一下,相似這樣

public void test(){
    methodStart();
    doSomething();
    methodEnd();
}

在每一個要監控的方法的入口和出口分別加上methodStart和methodEnd兩個方法,相似插樁埋點。

固然,這種插樁的方法缺點比較明顯:

  • 沒法監控系統方法
  • apk體積會增大(每一個方法都多了代碼)

須要注意:

  • 過濾簡單的方法
  • 只須要監控主線程執行的方法

2.3 其它

做爲擴展:

Facebook 開源的Profilo

https://github.com/facebookin...

4總結

這篇文章圍繞卡頓這個話題

1. 從源碼角度分析了屏幕刷新機制,底層每間隔16毫秒會發出vsyn信號,應用界面要更新,必須先向底層請求vsync信號,這樣下一個16毫秒vsync信號來的時候,底層會經過JNI通知到應用,而後經過主線程Handler執行View的繪製任務。

因此兩個地方會形成卡頓,一個是主線程在執行耗時操做致使View的繪製任務沒有及時執行,還有一個是View繪製過久,多是層級太多,或者裏面繪製算法太複雜,致使沒能在下一個vsync信號來臨以前準備完數據,致使掉幀卡頓。

2. 介紹目前比較流行的幾種卡頓監控方式,基於消息隊列的表明BlockCanary原理,以及經過編譯插樁的方式在每一個方法入口和出口加入計算方法耗時的代碼的方式。

面試中應對卡頓問題,能夠圍繞卡頓原理、屏幕刷新機制、卡頓監控這幾個方面來回答,固然,卡頓監控這一塊,還能夠經過TraceView、SysTrace等工具來找出卡頓代碼。

在BlockCanary出現以前,TraceView、Systrace是開發者必備的卡頓分析工具,而現在,能把BlockCanary原理講清楚我認爲就很不錯了,而對於廠商作系統App開發維護的,不會輕易接入開源庫,因此就有必要去了解TraceView、Systrace工具的使用。

本文主要介紹卡頓原理和卡頓監控,至於View具體是怎麼繪製的,軟件繪製和硬件繪製的區別,繪製流程走完以後,如何更新到屏幕,這個涉及到的內容不少,之後有時間會整理一下。
Android學習PDF+架構視頻+面試文檔+源碼筆記

相關文章
相關標籤/搜索