Android 性能監控框架 Matrix(5)卡頓監控源碼解析

監控主線程

TraceCanary 模塊只能在 API 16 以上的設備運行,內部分爲 ANR、幀率、慢方法、啓動四個監測模塊,核心接口是 LooperObserver。java

LooperObserver 是一個抽象類,顧名思義,它是 Looper 的觀察者,在 Looper 分發消息、刷新 UI 時回調,這幾個回調方法也是 ANR、慢方法等模塊的判斷依據:android

public abstract class LooperObserver {

    // 分發消息前
    @CallSuper
    public void dispatchBegin(long beginMs, long cpuBeginMs, long token) {
    }

    // UI 刷新
    public void doFrame(String focusedActivityName, long start, long end, long frameCostMs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    }

    // 分發消息後
    @CallSuper
    public void dispatchEnd(long beginMs, long cpuBeginMs, long endMs, long cpuEndMs, long token, boolean isBelongFrame) {
    }
}
複製代碼

Looper 監控

Looper 的監控是由類 LooperMonitor 實現的,原理很簡單,爲主線程 Looper 設置一個 Printer 便可,但值得一提的是,LooperMonitor 不會直接設置 Printer,而是先獲取舊對象,並建立代理對象,避免影響到其它用戶設置的 Printer:markdown

private synchronized void resetPrinter() {
    Printer originPrinter = ReflectUtils.get(looper.getClass(), "mLogging", looper);;
    looper.setMessageLogging(printer = new LooperPrinter(originPrinter));
}

class LooperPrinter implements Printer {
    @Override
    public void println(String x) {
        if (null != origin) {
            origin.println(x); // 保證原對象正常執行
        }
        dispatch(x.charAt(0) == '>', x); // 分發,經過第一個字符判斷是開始分發,仍是結束分發
    }
}
複製代碼

UI 刷新監控

UI 刷新監控是基於 Choreographer 實現的,TracePlugin 初始化時,UIThreadMoniter 就會經過反射的方式往 Choreographer 添加回調:app

public class UIThreadMonitor implements BeatLifecycle, Runnable {

    // Choreographer 中一個內部類的方法,用於添加回調
    private static final String ADD_CALLBACK = "addCallbackLocked";

    // 回調類型,分別爲輸入事件、動畫、View 繪製三種
    public static final int CALLBACK_INPUT = 0;
    public static final int CALLBACK_ANIMATION = 1;
    public static final int CALLBACK_TRAVERSAL = 2;

    public void init(TraceConfig config) {
        choreographer = Choreographer.getInstance();
        // 回調隊列
        callbackQueues = reflectObject(choreographer, "mCallbackQueues");
        // 反射,找到在 Choreographer 上添加回調的方法
        addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);
        addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);
        addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);
    }
}
複製代碼

之因此經過反射的方式實現,而不是經過 postCallback,是爲了把咱們的 callback 放到頭部,這樣才能計算系統提交的輸入事件、動畫、View 繪製等事件的耗時。ide

這樣,等 Choreographer 監聽到 vsync 信號時,UIThreadMonitor 和系統添加的回調都會被執行(好比在繪製 View 的時候,系統會往 Choreographer 添加一個 traversal callback):oop

public final class Choreographer {

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
        @Override
        public void run() {
            doFrame(mTimestampNanos, mFrame);
        }
    }

    void doFrame(long frameTimeNanos, int frame) {
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        ...
    }
}
複製代碼

由於 UIThreadMonitor 添加的回調在隊列頭部,可用於記錄開始時間,而其它系統方法,好比 View 的 postOnAnimation 添加的回調在後面,所以全部同類型回調執行完畢後,就能夠計算對應的事件(輸入事件、動畫、View 繪製等)的耗時。post

ANR 監控

ANR 監控原理:在 Looper 分發消息時,日後臺線程插入一個延時(5s 後執行)任務,Looper 消息分發完畢後就刪除,若是過了 5s,該任務未被刪除,就認爲出現了 ANR。動畫

public class AnrTracer extends Tracer {

    // onAlive 時初始化,onDead 時退出
    private Handler anrHandler;
    private volatile AnrHandleTask anrTask;

    public void dispatchBegin(long beginMs, long cpuBeginMs, long token) {
        // 插入方法結點,若是出現了 ANR,就從該結點開始收集方法執行記錄
        anrTask = new AnrHandleTask(AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin"), token);
        // 5 秒後執行
        // token 和 beginMs 相等,所以後一個減式用於減去回調該方法過程當中所消耗的時間
        anrHandler.postDelayed(anrTask, Constants.DEFAULT_ANR - (SystemClock.uptimeMillis() - token));
    }

    @Override
    public void dispatchEnd(long beginMs, long cpuBeginMs, long endMs, long cpuEndMs, long token, boolean isBelongFrame) {
        if (null != anrTask) {
            anrTask.getBeginRecord().release();
            anrHandler.removeCallbacks(anrTask);
        }
    }
}
複製代碼

若是 5s 後該任務未被刪除,那麼 AnrTracer 就會開始收集進程、線程、內存、堆棧等信息,並上報。spa

啓動監控

應用的啓動監控以第一個執行的方法爲起點:線程

public class AppMethodBeat implements BeatLifecycle {

    private static volatile int status = STATUS_DEFAULT;

    // 該方法會被插入到每個方法的開頭執行
    public static void i(int methodId) {

        if (status == STATUS_DEFAULT) { // 若是是默認狀態,則說明是第一個方法
            realExecute();
            status = STATUS_READY;
        }
    }

    private static void realExecute() {
        // 記錄時間戳
        ActivityThreadHacker.hackSysHandlerCallback();
        // 開始監控主線程 Looper
        LooperMonitor.register(looperMonitorListener);
    }
}
複製代碼

記錄了第一個方法開始執行時的時間戳後,Matrix 還會經過反射的方式,接管 ActivityThread 的 Handler 的 Callback:

public class ActivityThreadHacker {

    public static void hackSysHandlerCallback() {
        // 記錄時間戳,做爲應用啓用的開始時間
        sApplicationCreateBeginTime = SystemClock.uptimeMillis();
        // 反射 ActivityThread,接管 Handler
        Class<?> forName = Class.forName("android.app.ActivityThread");
        ...
    }
}
複製代碼

這樣就能知道第一個 Activity 或 Service 或 Receiver 啓動的具體時間了,這個時間戳能夠做爲 Application 啓動的結束時間:

private final static class HackCallback implements Handler.Callback {
    private static final int LAUNCH_ACTIVITY = 100;
    private static final int CREATE_SERVICE = 114;
    private static final int RECEIVER = 113;
    private static boolean isCreated = false;

    @Override
    public boolean handleMessage(Message msg) {
        boolean isLaunchActivity = isLaunchActivity(msg);
        // 若是是第一個啓動的 Activity 或 Service 或 Receiver,則以該時間戳做爲 Application 啓動的結束時間
        if (!isCreated) {
            if (isLaunchActivity || msg.what == CREATE_SERVICE || msg.what == RECEIVER) { // todo for provider
                ActivityThreadHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
                ActivityThreadHacker.sApplicationCreateScene = msg.what;
                isCreated = true;
            }
        }
    }
}
複製代碼

最後以主 Activity(閃屏頁以後的第一個 Activity)的 onWindowFocusChange 方法做爲終點,記錄時間戳——Activity 的啓動耗時能夠經過 onWindowFocusChange 方法回調時的時間戳減去其啓動時的時間戳。收集到上述信息以後便可統計啓動耗時:

firstMethod.i       LAUNCH_ACTIVITY   onWindowFocusChange   LAUNCH_ACTIVITY    onWindowFocusChange
^                         ^                   ^                     ^                  ^
|                         |                   |                     |                  |
|---------app---------|---|---firstActivity---|---------...---------|---careActivity---|
|<--applicationCost-->|
|<--------------firstScreenCost-------------->|
|<---------------------------------------coldCost------------------------------------->|
.                         |<-----warmCost---->|
複製代碼

若是冷啓動/暖啓動耗時超過某個閾值(可經過 IDynamicConfig 設置,默認分別爲 10s、4s),那麼就會從 AppMethodBeat 收集啓動過程當中的方法執行記錄並上報,不然只會簡單地上報耗時信息。

慢方法監控

慢方法監測的原理是在 Looper 分發消息時,計算分發耗時(endMs - beginMs),若是大於閾值(可經過 IDynamicConfig 設置,默認爲 700ms),就收集信息並上報。

ublic class EvilMethodTracer extends Tracer {

    @Override
    public void dispatchBegin(long beginMs, long cpuBeginMs, long token) {
        super.dispatchBegin(beginMs, cpuBeginMs, token);
        // 插入方法結點,若是出現了方法執行過慢的問題,就從該結點開始收集方法執行記錄
        indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");
    }

    @Override
    public void dispatchEnd(long beginMs, long cpuBeginMs, long endMs, long cpuEndMs, long token, boolean isBelongFrame) {
        long dispatchCost = endMs - beginMs;
        // 耗時大於慢方法閾值
        if (dispatchCost >= evilThresholdMs) {
            long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
            MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(...);
        }
    }

    private class AnalyseTask implements Runnable {
        void analyse() {
            // 收集進程與 CPU 信息
            int[] processStat = Utils.getProcessPriority(Process.myPid());
            String usage = Utils.calculateCpuUsage(cpuCost, cost);
            // 從插入結點開始收集並整理方法執行記錄
            TraceDataUtils.structuredDataToStack(data, stack, true, endMs);
            TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK,
                    new TraceDataUtils.IStructuredDataFilter() { ... }
            // 上報問題
            TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
            plugin.onDetectIssue(issue);
        }
    }
}
複製代碼

幀率監控

幀率監測的原理是監聽 Choreographer,在全部回調都執行完畢後計算當前總共花費的時間,從而計算掉幀數及掉幀程度,當同一個 Activity/Fragment 掉幀程度超過閾值時,就上報問題。關鍵源碼以下:

private class FPSCollector extends IDoFrameListener {
    @Override
    public void doFrameAsync(String visibleScene, long taskCost, long frameCostMs, int droppedFrames, boolean isContainsFrame) {
        // 使用 Map 保存同一 Activity/Fragment 的掉幀信息
        FrameCollectItem item = map.get(visibleScene);
        if (null == item) {
            item = new FrameCollectItem(visibleScene);
            map.put(visibleScene, item);
        }

        // 累計
        item.collect(droppedFrames, isContainsFrame);

        // 若是掉幀程度超過必定閾值,就上報問題,並從新計算
        // 總掉幀時間 sumFrameCost = 掉幀數 * 16.66ms
        // 掉幀上報閾值 timeSliceMs 可經過 IDynamicConfig 設置,默認爲 10s
        if (item.sumFrameCost >= timeSliceMs) { // report
            map.remove(visibleScene);
            item.report();
        }
    }
}
複製代碼

但這裏存在一個問題,那就是 Matrix 計算 UI 刷新耗時時,每次都會在掉幀數的基礎上加 1:

private class FrameCollectItem {

    void collect(int droppedFrames, boolean isContainsFrame) {
        // 即便掉幀數爲 0,這個值也會不斷增長
        sumFrameCost += (droppedFrames + 1) * frameIntervalCost / Constants.TIME_MILLIS_TO_NANO;
    }
}
複製代碼

並且,doFrame 方法不是隻在 UI 刷新時回調,而是每次 Looper 分發消息完畢後都會回調,而 Lopper 分發消息的頻率可能遠遠大於幀率,這就致使即便實際上沒有出現掉幀的狀況,但因爲 Looper 不斷分發消息的緣故,sumFrameCost 的值也會不斷累加,很快就突破了上報的閾值,進而頻繁地上報:

private void dispatchEnd() {
    ...
    synchronized (observers) {
        for (LooperObserver observer : observers) {
            if (observer.isDispatchBegin()) {
                observer.doFrame(...);
            }
        }
    }
}
複製代碼

解決方法是在 PluginListener 中手動過濾,或者修改源碼。

總結

TraceCanary 分爲慢方法、啓動、ANR、幀率四個模塊,每一個模塊的功能都是經過監聽接口 LooperObserver 實現的,LooperObserver 用於對主線程的 Looper 和 Choreographer 進行監控。

Looper 的監控是經過 Printer 實現的,每次事件分發都會回調 LooperObserver 的 dispatchBegin、dispatchEnd 方法,計算這兩個方法的耗時能夠檢測慢方法和 ANR 等問題。

Choreographer 的監控則是經過添加 input、animation、traversal 等各個類型的回調到 Choreographer 頭部實現的,vsync 信號觸發後,Choreographer 中各個類型的回調會被執行,兩種類型的回調的開始時間的間隔就至關於第一種類型的事件的耗時(即 input.cost = animation.begin - input.begiin),最後一種事件(traversal)執行完畢後,Looper 的 diaptchEnd 方法也會被執行,所以 traversal.cost = Looper.dispatchEnd -traversal.begin。

各個模塊的實現原理以下:

  1. ANR:在 Looper 開始分發消息時,日後臺線程插入一個延時(5s 後執行)任務,Looper 消息分發完畢後就刪除,若是過了 5s,該任務未被刪除,就認爲出現了 ANR,收集信息,報告問題

  2. 慢方法:在 Looper 分發消息時,計算分發耗時(endMs - beginMs),若是大於閾值(可經過 IDynamicConfig 設置,默認爲 700ms),就收集信息並上報

  3. 啓動:以第一個執行的方法爲起點記錄時間戳,接着記錄第一個 Activity 或 Service 或 Receiver 啓動時的時間戳,做爲 Application 啓動的結束時間。最後以主 Activity(閃屏頁以後的第一個 Activity)的 onWindowFocusChange 方法做爲終點,記錄時間戳。Activity 的啓動耗時能夠經過 onWindowFocusChange 方法回調時的時間戳減去其啓動時的時間戳。收集到上述信息以後便可統計啓動耗時。

  4. 掉幀:監聽 Choreographer,doFrame 回調時統計 UI 刷新耗時,計算掉幀數及掉幀程度,當同一個 Activity/Fragment 掉幀程度超過閾值時,就上報。但 Matrix 的計算方法存在問題,可能出現頻繁上報的狀況,須要自行手動過濾。

相關文章
相關標籤/搜索