微信自研APM利器Matrix 卡頓分析工具之(二)TraceCanary

Matrix是微信開源的一套完整的APM解決方案,內部包含Resource Canary(資源監測)/Trace Canary(卡頓監測)/IO Canary(IO監測)等。

本篇為卡頓分析系列文章之二,分析Trace Canary相關的原理。文章有點長,你可以先大致瀏覽一遍再細看,對你一定有幫助。第一篇傳送門Android卡頓檢測工具(一)BlockCanary

Matrix內容概覽

Matrix.png

可見Matrix作為一個APM工具,在性能檢測方面還是非常全面的,系列文章將會一一對它們進行分析。

為理清源代碼結構我們先從初始化流程講起,項目地址Matrix

Matrix初始化流程

Matrix.Builder內部類配置Plugins。

//創建builder
Matrix.Builder builder = new Matrix.Builder(this);

//可選 配置插件 
builder.plugin(tracePlugin);
builder.plugin(ioCanaryPlugin);

//可選 感知插件狀態變化
builder.patchListener(...);

//完成初始化
Matrix.init(builder.build());

Plugin結構

plugin類圖.png

目前配置的plugin

  • TracePlugin
  • ResourcePlugin
  • IOCanaryPlugin
  • SQLiteLintPlugin

Matrix.Builder調用build方法觸發Matrix構造函數。

private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) {
    this.application = app;
    this.pluginListener = listener;
    this.plugins = plugins;
    for (Plugin plugin : plugins) {
        plugin.init(application, pluginListener);
        pluginListener.onInit(plugin);
    }
}

內部遍歷所有插件,并調用其init方法進行初始化。之后通知pluginListener生命周期。
上層可自定義pluginListener感知plugin生命周期。

# -> Matrix.Builder
public Builder patchListener(PluginListener pluginListener) {
    this.pluginListener = pluginListener;
    return this;
}

最終來看Matrix的init方法,其實就是為其靜態成員變量sInstance賦值。

# -> Matrix
public static Matrix init(Matrix matrix) {
    if (matrix == null) {
        throw new RuntimeException("Matrix init, Matrix should not be null.");
    }
    synchronized (Matrix.class) {
        if (sInstance == null) {
            sInstance = matrix;
        } else {
            MatrixLog.e(TAG, "Matrix instance is already set. this invoking will be ignored");
        }
    }
    return sInstance;
}

plugin包含的生命周期:

# -> PluginListener
public interface PluginListener {
    void onInit(Plugin plugin);

    void onStart(Plugin plugin);

    void onStop(Plugin plugin);

    void onDestroy(Plugin plugin);

    void onReportIssue(Issue issue);
}

Matrix結構

Matrix類圖.png

可以看到Matrix提供了日志管理器MatrixLogImpl,以及控制其內部所有plugin的開關方法startAllPlugins/stopAllPlugins。

接下來進入正題,我們來看看卡頓(UI渲染性能)分析模塊TracePlugin是如何工作的。

TracePlugin

其內部定義了一些跟蹤器

  • FPSTracer 幀率監測
  • EvilMethodTracer 耗時函數監測
  • FrameTracer 逐幀監測
  • StartUpTracer 啟動耗時

來看一下類圖:

tracer類圖.png

這些跟蹤器都繼承于BaseTracer,BaseTracer為抽象類,唯一的抽象方法是getTag方法。子類實現僅僅定義一個名稱即可。

再來看看BaseTracer實現的接口

  1. ApplicationLifeObserver.IObserver
    當activity前后臺切換或者生命周期發生變化時會回調接口方法。至于是如何監控的,邏輯都在ApplicationLifeObserver中,這個我們稍后分析。因此BaseTracer具有感知activity生命周期及應用前后臺狀態變化的能力

  2. IFrameBeatListener
    當繪制完畢每一幀會回調onFrame方法,當activity處于后臺或被銷毀會回調cancelFrame方法。
    因此BaseTracer具有感知幀率變化、統計卡頓的能力,所以跟幀率、函數統計相關的Tracer(FPSTracer/FrameTracer/EvilMethodTracer)都復寫了此方法。

  3. IMethodBeatListener
    接口方法主要有pushFullBuffer和onActivityEntered,先看pushFullBuffer方法,統計函數耗時是通過插樁完成的,matrix會記錄每個方法執行的時間,并寫入一個long型數組,當數組容量滿后會發一次pushFullBuffer回調,收到回調后可統計函數耗時情況。再看onActivityEntered方法,每個activity啟動后會對調此方法,因此可用于統計activity啟動時間。因此BaseTracer具有統計函數耗時和Activity啟動耗時的能力,而在tracer體系內EvilMethodTracer是用于偵查耗時函數(邪惡函數),StartUpTracer用于統計Activity啟動時間,所以二者一定會復寫這兩個方法。

在BaseTracer的onCreate方法中完成了對上述接口的監聽。

# -> BaseTracer
public void onCreate() {
    if (isEnableMethodBeat()) {
        if (!getMethodBeat().isHasListeners()) {
            getMethodBeat().onCreate();
        }
        //監聽IMethodBeatListener
        getMethodBeat().registerListener(this);
    }
    //監聽ApplicationLifeObserver.IObserver
    ApplicationLifeObserver.getInstance().register(this);
    //監聽IFrameBeatListener
    FrameBeat.getInstance().addListener(this);
    isCreated = true;
}

對應的在onDestroy方法中取消了這些監聽。

# -> BaseTracer
public void onDestroy() {
    if (isEnableMethodBeat()) {
        getMethodBeat().unregisterListener(this);
        if (!getMethodBeat().isHasListeners()) {
            getMethodBeat().onDestroy();
        }
    }
    ApplicationLifeObserver.getInstance().unregister(this);
    FrameBeat.getInstance().removeListener(this);
    isCreated = false;
}

在BaseTracer中大部分接口方法都是空實現,具體實現交由有需求的tracer完成。下面我們來看TraceCanary包含的具體tracer實現。

Trace Canary 結構.png

FrameTracer

我們先來看FrameTracer,它復寫doFrame監聽每一幀的回調,并將時間戳、掉幀情況、頁面名稱等信息發送給IDoFrameListener。

# -> FrameTracer -> doFrame
@Override
public void doFrame(final long lastFrameNanos, final long frameNanos) {
    if (!isDrawing) {
        return;
    }
    isDrawing = false;
    final int droppedCount = (int) ((frameNanos - lastFrameNanos) / REFRESH_RATE_MS);
    for (final IDoFrameListener listener : mDoFrameListenerList) {
        //同步發送
        listener.doFrameSync(lastFrameNanos, frameNanos, getScene(), droppedCount);
        if (null != listener.getHandler()) {
            //異步發送
            listener.getHandler().post(new AsyncDoFrameTask(listener,
                    lastFrameNanos, frameNanos, getScene(), droppedCount));
        }
    }
}

可以看到代碼中分別以同步和異步的方式將回調發送出去,上層可通過FrameTracer的register方法注冊監聽。

# FrameTracer
public void register(IDoFrameListener listener) {
    if (FrameBeat.getInstance().isPause()) {
        FrameBeat.getInstance().resume();
    }
    if (!mDoFrameListenerList.contains(listener)) {
        mDoFrameListenerList.add(listener);
    }
}

public void unregister(IDoFrameListener listener) {
    mDoFrameListenerList.remove(listener);
    if (!FrameBeat.getInstance().isPause() && mDoFrameListenerList.isEmpty()) {
        FrameBeat.getInstance().removeListener(this);
    }
}

EvilMethodTracer

它具有檢查耗時函數的功能,而ANR就是最嚴重的耗時情況,那我們先來看看ANR檢查是如何做到的。

ANR檢查

先來看構造器

public EvilMethodTracer(TracePlugin plugin, TraceConfig config) {
    super(plugin);
    this.mTraceConfig = config;
    //創建ANR延時檢測工具 定時5s
    mLazyScheduler = new LazyScheduler(MatrixHandlerThread.getDefaultHandlerThread(), Constants.DEFAULT_ANR);
    mActivityCreatedInfoMap = new HashMap<>();
}

LazyScheduler是一個延時任務工具類,構造時需設定HandlerThread和delay。

LazyScheduler類圖.png

內部ILazyTask接口定義了延時任務執行時的回調方法onTimeExpire。setUp方法開始埋炸彈(ANR和耗時方法),cancel方法解除炸彈。也就是說調用setUp方法后5秒內如果沒有執行cancel,就會觸發onTimeExpire方法。

上面的內容理解之后,我們來看doFrame方法。

# -> EvilMethodTracer
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
    if (isIgnoreFrame) {
        mActivityCreatedInfoMap.clear();
        setIgnoreFrame(false);
        getMethodBeat().resetIndex();
        return;
    }

    int index = getMethodBeat().getCurIndex();
    //兩幀時間差大于卡頓閾值(默認一秒)則發出buffer信息
    //若滿足一系列校驗工作則觸發卡頓檢測
    if (hasEntered && frameNanos - lastFrameNanos > mTraceConfig.getEvilThresholdNano()) {
        MatrixLog.e(TAG, "[doFrame] dropped frame too much! lastIndex:%s index:%s", 0, index);
        handleBuffer(Type.NORMAL, 0, index - 1, getMethodBeat().getBuffer(), (frameNanos - lastFrameNanos) / Constants.TIME_MILLIS_TO_NANO);
    }
    getMethodBeat().resetIndex();
    mLazyScheduler.cancel();
    //埋ANR炸彈
    mLazyScheduler.setUp(this, false);
}

如果5秒內還沒執行下一次doFrame,就會回調到EvilMethodTracer的onTimeExpire方法。

# -> EvilMethodTracer
@Override
public void onTimeExpire() {
    // maybe ANR
    if (isBackground()) {
        MatrixLog.w(TAG, "[onTimeExpire] pass this time, on Background!");
        return;
    }
    long happenedAnrTime = getMethodBeat().getCurrentDiffTime();
    MatrixLog.w(TAG, "[onTimeExpire] maybe ANR!");
    setIgnoreFrame(true);
    getMethodBeat().lockBuffer(false);
    //處于前臺就會發送ANR消息
    handleBuffer(Type.ANR, 0, getMethodBeat().getCurIndex() - 1, getMethodBeat().getBuffer(), null, Constants.DEFAULT_ANR, happenedAnrTime, -1);
}

對于普通耗時函數又是如何檢測的呢?EvilMethodTracer的工作流程是這樣的:

  1. 首先要記錄各個函數的執行時間,這里需要在每個函數的入口和出口做插樁工作,最終寫入MethodBeat 中的成員變量sBuffer,它的類型為long型數組,通過不同位描述了函數id和函數的耗時。之所以用一個long型值記錄耗時結果是為了壓縮數據、節省內存,官方數據是預先分配記錄數據的buffer長度為100w內存占用約7.6M。


    buffer結構.png
  2. doFrame檢查兩幀之間的時間差,如果大于卡頓閾值(默認為1s),則會調用handleBuffer觸發統計排查任務。
  3. handlerBuffer中啟動AnalyseTask任務分析過濾method調用stack、函數耗時等,并保存在jsonObject中。
  4. 調用sendReport將jsonObject轉為Issue對象發送事件給PluginListener。

函數插樁

MethodTracer的內部類TraceMethodAdapter負責為每個方法執行前插入MethodBeat的i方法,方法執行后插入o方法。插樁使用的是ASM實現的,ASM是一種常用的操作字節碼的動態化技術,可以用做無侵入的埋點統計。EvilMethodTracer也是用它做耗時函數的分析。

# -> MethodTracer.TraceMethodAdapter
@Override
protected void onMethodEnter() {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if (traceMethod != null) {
        traceMethodCount.incrementAndGet();
        mv.visitLdcInsn(traceMethod.id);
        //入口插樁
        mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
    }
}

@Override
protected void onMethodExit(int opcode) {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if (traceMethod != null) {
        if (hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap)
                && mCollectedMethodMap.containsKey(traceMethod.getMethodName())) {
            TraceMethod windowFocusChangeMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
                    TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
            if (windowFocusChangeMethod.equals(traceMethod)) {
                traceWindowFocusChangeMethod(mv);
            }
        }

        traceMethodCount.incrementAndGet();
        mv.visitLdcInsn(traceMethod.id);
        //出口插樁
        mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
    }
}

Matrix通過代理編譯期間的任務 transformClassesWithDexTask,將全局 class 文件作為輸入,利用 ASM 工具,高效地對所有 class 文件進行掃描及插樁。為了盡可能的降低性能損耗掃描過程會過濾掉一些默認或匿名的構造函數以及get/set等簡單而不耗時的函數。

為了方便及高效記錄函數執行過程,Matrix插件為每個插樁的函數分配一個獨立 ID,在插樁過程中,記錄插樁的函數簽名及分配的 ID,在插樁完成后輸出一份 methodmap文件,作為數據上報后的解析支持,該文件在apk構建時生成,目錄位于build/matrix_output下,名為Debug_methodmap(debug構建),而那些被過濾掉的方法被記錄在Debug_ignoremethodmap文件中。文件生成規則在MethodCollector類中,感興趣的小伙伴可以繼續研究。

那接下來我們來看一下生成文件的內容。


methodmap.png

文件每一行代表一個插樁方法。
以第一行為例:

-1,1,sample.tencent.matrix.io.TestIOActivity onWindowFocusChanged (Z)V
  • -1 第一個數字表示分配方法的Id,-1表示插樁為activity加入的onWindowFocusChanged方法。其他方法從1開始計數。
  • 1 表示方法權限修飾符,常見的值為ACC_PUBLIC = 1; ACC_PRIVATE = 2;ACC_PROTECTED = 4; ACC_STATIC = 8等等。1即表示public方法。
  • 類名 sample.tencent.matrix.io.TestIOActivity
  • 方法名 onWindowFocusChanged
  • 參數及返回值類型Z表示參數為boolean類型,V表示返回值為空。

接下來我們來看一下實踐是什么效果,我們模擬了一個耗時函數,當點擊按鈕時調用。

//點擊按鈕觸發 為放大耗時,循環執行200次
public void testJank(View view) {
    for (int i = 0; i < 200; i++) {
        wrapper();
    }
}

//包裝方法用于測試調用深度
void wrapper() {
    tryHeavyMethod();
}

//dump內存是耗時方法
private void tryHeavyMethod() {
    Debug.getMemoryInfo(new Debug.MemoryInfo());
}

運行后得到以下Issue:

evil_method_trace.png

我們重點關心的是

  1. cost bad函數表示總耗時。
  2. stack bad函數調用棧。
  3. stackKey bad函數入口方法Id

例子中stack(0,28,1,1988\n 1,31,1,136)如何解讀呢?四個數為一組每組用換行符分隔,其中一組四個數分別表示為:

  • 0 方法調用深度,比如a調用b,b調用c,則a,b,c的調用深度分別為0,1,2。
  • 28 methodId,與上述生成的methodmap文件中第一列對應。
  • 1 調用次數
  • 1998 函數總耗時,包含子函數的調用耗時。

我們通過反查methodmap函數可驗證結果。

函數記錄.png

實測發現stack存在bug,我們的代碼中最終的耗時方法是tryHeavyMethod,只不過中間包了一層wrapper方法,stack就不能識別到了。這一點Matrix官方可能會后續修復吧。

stackKey就是耗時函數的入口。本例中testJank調用wrapper,wrapper調用tryHeavyMethod,統計stackKey時以深度為0的函數為準,28就對應testJank方法。

FPSTracer

同其他類似的fps檢測工具原理一樣,監聽Choreographer.FrameCallback回調,回調方法doFrame在每次Vsync信號即將來臨時被調用,上層監聽此回調接口并計算兩次回調之前的時間差,Android系統默認的刷新頻率是16.6ms一次,時間差除以刷新頻率即為掉幀情況。

FPSTracer不同的點在于其內部能統計一段時間的平均幀率,并定義了幀率好壞的梯度。

# -> FPSTracer.DropStatus
private enum DropStatus {
    DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0);
    int index;

    DropStatus(int index) {
        this.index = index;
    }
}
  • DROPPED_FROZEN 掉42幀及以上(70%掉幀)
  • DEFAULT_DROPPED_HIGH 掉24幀以上42幀以下(40%掉幀)
  • DEFAULT_DROPPED_MIDDLE 掉9幀以上24幀以下(15%掉幀)
  • DEFAULT_DROPPED_NORMAL 掉3幀以上9幀以下(5%掉幀)
  • DROPPED_BEST 掉3幀以內

核心方法代碼片段

# FPSTracer -> doReport
private void doReport() {
    LinkedList<Integer> reportList;
    synchronized (this.getClass()) {
        if (mFrameDataList.isEmpty()) {
            return;
        }
        reportList = mFrameDataList;
        mFrameDataList = new LinkedList<>();
    }

    //數據轉儲到mPendingReportSet集合中
    for (int trueId : reportList) {
        int scene = trueId >> 22;
        int durTime = trueId & 0x3FFFFF;
        LinkedList<Integer> list = mPendingReportSet.get(scene);
        if (null == list) {
            list = new LinkedList<>();
            mPendingReportSet.put(scene, list);
        }
        list.add(durTime);
    }
    reportList.clear();

    //統計分析
    for (int i = 0; i < mPendingReportSet.size(); i++) {
        int key = mPendingReportSet.keyAt(i);
        LinkedList<Integer> list = mPendingReportSet.get(key);
        if (null == list) {
            continue;
        }
        int sumTime = 0;
        int markIndex = 0;
        int count = 0;

        int[] dropLevel = new int[DropStatus.values().length]; // record the level of frames dropped each time
        int[] dropSum = new int[DropStatus.values().length]; // record the sum of frames dropped each time
        int refreshRate = (int) Constants.DEFAULT_DEVICE_REFRESH_RATE * OFFSET_TO_MS;
        for (Integer period : list) {
            sumTime += period;
            count++;
            int tmp = period / refreshRate - 1;
            //將掉幀情況寫入數組
            if (tmp >= Constants.DEFAULT_DROPPED_FROZEN) {
                dropLevel[DropStatus.DROPPED_FROZEN.index]++;
                dropSum[DropStatus.DROPPED_FROZEN.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_HIGH) {
                dropLevel[DropStatus.DROPPED_HIGH.index]++;
                dropSum[DropStatus.DROPPED_HIGH.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_MIDDLE) {
                dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
                dropSum[DropStatus.DROPPED_MIDDLE.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_NORMAL) {
                dropLevel[DropStatus.DROPPED_NORMAL.index]++;
                dropSum[DropStatus.DROPPED_NORMAL.index] += tmp;
            } else {
                dropLevel[DropStatus.DROPPED_BEST.index]++;
                dropSum[DropStatus.DROPPED_BEST.index] += (tmp < 0 ? 0 : tmp);
            }
            //達到分片時間 sendReport一次
            if (sumTime >= mTraceConfig.getTimeSliceMs() * OFFSET_TO_MS) { // if it reaches report time
                float fps = Math.min(60.f, 1000.f * OFFSET_TO_MS * (count - markIndex) / sumTime);
                MatrixLog.i(TAG, "scene:%s fps:%s sumTime:%s [%s:%s]", mSceneIdToSceneMap.get(key), fps, sumTime, count, markIndex);
                try {
                    JSONObject dropLevelObject = new JSONObject();
                    ...

                    JSONObject dropSumObject = new JSONObject();
                    ...

                    JSONObject resultObject = new JSONObject();
                    resultObject = DeviceUtil.getDeviceInfo(resultObject, getPlugin().getApplication());

                    resultObject.put(SharePluginInfo.ISSUE_SCENE, mSceneIdToSceneMap.get(key));
                    resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);
                    resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);
                    resultObject.put(SharePluginInfo.ISSUE_FPS, fps);
                    sendReport(resultObject);
                } catch (JSONException e) {
                    MatrixLog.e(TAG, "json error", e);
                }


                dropLevel = new int[DropStatus.values().length];
                dropSum = new int[DropStatus.values().length];
                markIndex = count;
                sumTime = 0;
            }
        }

        // delete has reported data
        if (markIndex > 0) {
            for (int index = 0; index < markIndex; index++) {
                list.removeFirst();
            }
        }
        ...
    }
}

整個流程如下

  1. FPSTracer中定義類型為LinkedList<Integer>的成員變量mFrameDataList,用于記錄時間差和scene(activity或fragment名)信息。
  2. 計算兩次兩次doFrame時間差,記錄在一個int數中。其中高10位表示sceneId,低22位表示耗時ms*OFFSET_TO_MS(默認為100)。


    frame數據存儲.png
  3. 以兩分鐘(getFPSReportInterval默認值,官方sample為10秒)為一個周期統計frame信息,計時結束后觸發onTimeExpire回調方法。
  4. onTimeExpire調用doReport做統計分析。
  5. 同一個場景下累計frame耗時超過分片時間(getTimeSliceMs默認為6秒,官方sample為1秒)則觸發一次sendReport將統計到的各個級別的掉幀數和掉幀時間發送出去。

這里有一個細節問題需要處理,比如頁面沒有靜止沒有UI繪制任務,這段時間的幀率統計也沒意義。事實上,FPSTracer對上述用于存儲每幀耗時信息的mFrameDataList的插入做個一個過濾。

# FPSTracer -> doFrame
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
    //滿足判斷條件才handleDoFrame
    if (!isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())) {
        handleDoFrame(lastFrameNanos, frameNanos, getScene());
    }
    isDrawing = false;
}

private void handleDoFrame(long lastFrameNanos, long frameNanos, String scene) {
    int sceneId;
    ... //獲取scene信息
    int trueId = 0x0;
    //位運算,將sceneId和耗時信息寫入一個int
    trueId |= sceneId;
    trueId = trueId << 22;
    long offset = frameNanos - lastFrameNanos;
    trueId |= ((offset / FACTOR) & 0x3FFFFF);
    if (offset >= 5 * 1000000000L) {
        MatrixLog.w(TAG, "[handleDoFrame] WARNING drop frame! offset:%s scene%s", offset, scene);
    }
    //添加到mFrameDataList
    synchronized (this.getClass()) {
        mFrameDataList.add(trueId);
    }
}

看條件!isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())

  1. isInvalid 表示是否非法,當activity resume后為false,pause后為true。也即只統計resume階段,因為activity真正繪制是從onResume開始。
  2. isDrawing 表示是否處理draw狀態,FPSTracer在onActivityResume時為DecorView添加了draw listener(getDecorView().getViewTreeObserver().addOnDrawListener())監聽view的繪制,當回調onDraw時將此變量設為true,onFrame結束設置為false。因此處于靜止狀態的時間段不會統計幀信息。
  3. isEnterAnimationComplete 入場動畫執行完。
  4. isTargetScene FPSTrace可配置監控界面白名單,默認全部監控。

這樣真個fps檢測流程也就結束了,我們來看一下官方sample匯總的report展現。

fps_tracer_issue.png

StartUpTrace 應用啟動統計

首先要明確的是統計的是應用的啟動,這包括application創建過程而不單純是activity啟動。統計觸發一次就會銷毀,因此如果想統計activity之間跳轉的情況需手動獲取StartUpTrace并調用onCreate方法。

具體的統計指標如下:

統計項目 含義
appCreateTime application創建時長
betweenCost application創建完成到第一個Activity create完成
activityCreate activity 執行完super.oncreate()至window獲取焦點
splashCost splash界面創建時長
allCost 到主界面window focused總時長
isWarnStartUp 是否為熱啟動(application存在)

時間軸大致是這樣的:


startup時間軸.png

為了實現上述統計指標需要hook ActivityThread中消息處理內部類H(成員變量mH),它是一個Handler對象,activity的創建與生命周期的處理都是通過它完成的,如果你熟悉activity的啟動流程那么對mH成員變量一定不陌生。ApplicationThread作為binder通信的信使,接收AMS的調度事件,比如scheduleLaunchActivity,此方法內部會通過mH對象發送 H.LAUNCH_ACTIVITY消息,mH接收到此消息便會調用handleLaunchActivity創建activity對象。

這屬于Activity啟動流程范疇,本篇不再討論。重點關注hook動作。

hook系統handler mH

# -> StartUpHacker
public class StartUpHacker {
    private static final String TAG = "Matrix.Hacker";
    public static boolean isEnterAnimationComplete = false;
    public static long sApplicationCreateBeginTime = 0L;
    public static int sApplicationCreateBeginMethodIndex = 0;
    public static long sApplicationCreateEndTime = 0L;
    public static int sApplicationCreateEndMethodIndex = 0;
    public static int sApplicationCreateScene = -100;

    //此方法被靜態代碼塊調用 在被類resolve時執行
    public static void hackSysHandlerCallback() {
        try {
            sApplicationCreateBeginTime = System.currentTimeMillis();
            sApplicationCreateBeginMethodIndex = MethodBeat.getCurIndex();
            Class<?> forName = Class.forName("android.app.ActivityThread");
            Field field = forName.getDeclaredField("sCurrentActivityThread");
            field.setAccessible(true);
            Object activityThreadValue = field.get(forName);
            Field mH = forName.getDeclaredField("mH");
            mH.setAccessible(true);
            Object handler = mH.get(activityThreadValue);
            Class<?> handlerClass = handler.getClass().getSuperclass();
            Field callbackField = handlerClass.getDeclaredField("mCallback");
            callbackField.setAccessible(true);
            Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
            HackCallback callback = new HackCallback(originalCallback);
            callbackField.set(handler, callback);
            MatrixLog.i(TAG, "hook system handler completed. start:%s", sApplicationCreateBeginTime);
        } catch (Exception e) {
            MatrixLog.e(TAG, "hook system handler err! %s", e.getCause().toString());
        }
    }
}

代碼比較簡單,就是取出mH對象內部原有的Handler.Callback,將它換成成新的HackCallback。

# StartUpHacker.HackCallback
private final static class HackCallback implements Handler.Callback {
   private final Handler.Callback mOriginalCallback;

    HackCallback(Handler.Callback callback) {
        this.mOriginalCallback = callback;
    }

    @Override
    public boolean handleMessage(Message msg) {
        ...
        //優先處理 設置一些值
        boolean isLaunchActivity = isLaunchActivity(msg);
        if (isLaunchActivity) {
            StartUpHacker.isEnterAnimationComplete = false;
        } else if (msg.what == ENTER_ANIMATION_COMPLETE) {
            //記錄activity轉場動畫結束標志
            StartUpHacker.isEnterAnimationComplete = true;
        }
        if (!isCreated) {
            if (isLaunchActivity || msg.what == CREATE_SERVICE || msg.what == RECEIVER) {
                //以第一個Activity LAUNCH_ACTIVITY消息為止,記錄application創建結束時間
                StartUpHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
                StartUpHacker.sApplicationCreateEndMethodIndex = MethodBeat.getCurIndex();
                StartUpHacker.sApplicationCreateScene = msg.what;
                isCreated = true;
            }
        }
        if (null == mOriginalCallback) {
            return false;
        }
        //最終讓原有的callback處理消息
        return mOriginalCallback.handleMessage(msg);
    }
}

了解了hook原理,我們來看一下統計時間的幾個關鍵節點是如何獲得的。

  1. 程序啟動 實際上是MethodBeat類的一段靜態代碼塊,我們知道靜態代碼塊在解析類的時候就執行了,拿它作為程序計時的起點也算正常。
  2. 系統LAUNCH_ACTIVITY消息發出 通過hook mH類完成。
  3. 收到onActivityCreated回調 通過為aplication注冊registerActivityLifecycleCallbacks來感知應用內activity生命周期。
  4. Activity對應window獲取焦點 通過ASM動態復寫activity的onWindowFocusChanged方法。

寫到這,整個Trace Canary的內容就算大致講完了,其中涉及的知識點非常多,包括UI繪制流程、Activity啟動流程、應用啟動流程、打包流程、ASM插樁等等。筆者只是按源碼流程大致理出了最核心的內容,分支的技術點大多一筆略過,需要讀者自行補充,希望大家一起加油,補足分支的技術棧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容