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

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

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

Matrix內(nèi)容概覽

Matrix.png

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

為理清源代碼結(jié)構(gòu)我們先從初始化流程講起,項目地址Matrix。

Matrix初始化流程

Matrix.Builder內(nèi)部類配置Plugins。

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

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

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

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

Plugin結(jié)構(gòu)

plugin類圖.png

目前配置的plugin

  • TracePlugin
  • ResourcePlugin
  • IOCanaryPlugin
  • SQLiteLintPlugin

Matrix.Builder調(diào)用build方法觸發(fā)Matrix構(gòu)造函數(shù)。

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

內(nèi)部遍歷所有插件,并調(diào)用其init方法進(jìn)行初始化。之后通知pluginListener生命周期。
上層可自定義pluginListener感知plugin生命周期。

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

最終來看Matrix的init方法,其實就是為其靜態(tài)成員變量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結(jié)構(gòu)

Matrix類圖.png

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

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

TracePlugin

其內(nèi)部定義了一些跟蹤器

  • FPSTracer 幀率監(jiān)測
  • EvilMethodTracer 耗時函數(shù)監(jiān)測
  • FrameTracer 逐幀監(jiān)測
  • StartUpTracer 啟動耗時

來看一下類圖:

tracer類圖.png

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

再來看看BaseTracer實現(xiàn)的接口

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

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

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

在BaseTracer的onCreate方法中完成了對上述接口的監(jiān)聽。

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

對應(yīng)的在onDestroy方法中取消了這些監(jiān)聽。

# -> 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中大部分接口方法都是空實現(xiàn),具體實現(xiàn)交由有需求的tracer完成。下面我們來看TraceCanary包含的具體tracer實現(xiàn)。

Trace Canary 結(jié)構(gòu).png

FrameTracer

我們先來看FrameTracer,它復(fù)寫doFrame監(jiān)聽每一幀的回調(diào),并將時間戳、掉幀情況、頁面名稱等信息發(fā)送給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) {
        //同步發(fā)送
        listener.doFrameSync(lastFrameNanos, frameNanos, getScene(), droppedCount);
        if (null != listener.getHandler()) {
            //異步發(fā)送
            listener.getHandler().post(new AsyncDoFrameTask(listener,
                    lastFrameNanos, frameNanos, getScene(), droppedCount));
        }
    }
}

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

# 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

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

ANR檢查

先來看構(gòu)造器

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

LazyScheduler是一個延時任務(wù)工具類,構(gòu)造時需設(shè)定HandlerThread和delay。

LazyScheduler類圖.png

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

上面的內(nèi)容理解之后,我們來看doFrame方法。

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

    int index = getMethodBeat().getCurIndex();
    //兩幀時間差大于卡頓閾值(默認(rèn)一秒)則發(fā)出buffer信息
    //若滿足一系列校驗工作則觸發(fā)卡頓檢測
    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秒內(nèi)還沒執(zhí)行下一次doFrame,就會回調(diào)到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);
    //處于前臺就會發(fā)送ANR消息
    handleBuffer(Type.ANR, 0, getMethodBeat().getCurIndex() - 1, getMethodBeat().getBuffer(), null, Constants.DEFAULT_ANR, happenedAnrTime, -1);
}

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

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


    buffer結(jié)構(gòu).png
  2. doFrame檢查兩幀之間的時間差,如果大于卡頓閾值(默認(rèn)為1s),則會調(diào)用handleBuffer觸發(fā)統(tǒng)計排查任務(wù)。
  3. handlerBuffer中啟動AnalyseTask任務(wù)分析過濾method調(diào)用stack、函數(shù)耗時等,并保存在jsonObject中。
  4. 調(diào)用sendReport將jsonObject轉(zhuǎn)為Issue對象發(fā)送事件給PluginListener。

函數(shù)插樁

MethodTracer的內(nèi)部類TraceMethodAdapter負(fù)責(zé)為每個方法執(zhí)行前插入MethodBeat的i方法,方法執(zhí)行后插入o方法。插樁使用的是ASM實現(xiàn)的,ASM是一種常用的操作字節(jié)碼的動態(tài)化技術(shù),可以用做無侵入的埋點統(tǒng)計。EvilMethodTracer也是用它做耗時函數(shù)的分析。

# -> 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通過代理編譯期間的任務(wù) transformClassesWithDexTask,將全局 class 文件作為輸入,利用 ASM 工具,高效地對所有 class 文件進(jìn)行掃描及插樁。為了盡可能的降低性能損耗掃描過程會過濾掉一些默認(rèn)或匿名的構(gòu)造函數(shù)以及get/set等簡單而不耗時的函數(shù)。

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

那接下來我們來看一下生成文件的內(nèi)容。


methodmap.png

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

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

接下來我們來看一下實踐是什么效果,我們模擬了一個耗時函數(shù),當(dāng)點擊按鈕時調(diào)用。

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

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

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

運(yùn)行后得到以下Issue:

evil_method_trace.png

我們重點關(guān)心的是

  1. cost bad函數(shù)表示總耗時。
  2. stack bad函數(shù)調(diào)用棧。
  3. stackKey bad函數(shù)入口方法Id

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

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

我們通過反查methodmap函數(shù)可驗證結(jié)果。

函數(shù)記錄.png

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

stackKey就是耗時函數(shù)的入口。本例中testJank調(diào)用wrapper,wrapper調(diào)用tryHeavyMethod,統(tǒng)計stackKey時以深度為0的函數(shù)為準(zhǔn),28就對應(yīng)testJank方法。

FPSTracer

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

FPSTracer不同的點在于其內(nèi)部能統(tǒng)計一段時間的平均幀率,并定義了幀率好壞的梯度。

# -> 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幀以內(nèi)

核心方法代碼片段

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

    //數(shù)據(jù)轉(zhuǎn)儲到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();

    //統(tǒng)計分析
    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;
            //將掉幀情況寫入數(shù)組
            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);
            }
            //達(dá)到分片時間 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數(shù)中。其中高10位表示sceneId,低22位表示耗時ms*OFFSET_TO_MS(默認(rèn)為100)。


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

這里有一個細(xì)節(jié)問題需要處理,比如頁面沒有靜止沒有UI繪制任務(wù),這段時間的幀率統(tǒng)計也沒意義。事實上,F(xiàn)PSTracer對上述用于存儲每幀耗時信息的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;
    //位運(yùn)算,將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 表示是否非法,當(dāng)activity resume后為false,pause后為true。也即只統(tǒng)計resume階段,因為activity真正繪制是從onResume開始。
  2. isDrawing 表示是否處理draw狀態(tài),F(xiàn)PSTracer在onActivityResume時為DecorView添加了draw listener(getDecorView().getViewTreeObserver().addOnDrawListener())監(jiān)聽view的繪制,當(dāng)回調(diào)onDraw時將此變量設(shè)為true,onFrame結(jié)束設(shè)置為false。因此處于靜止?fàn)顟B(tài)的時間段不會統(tǒng)計幀信息。
  3. isEnterAnimationComplete 入場動畫執(zhí)行完。
  4. isTargetScene FPSTrace可配置監(jiān)控界面白名單,默認(rèn)全部監(jiān)控。

這樣真?zhèn)€fps檢測流程也就結(jié)束了,我們來看一下官方sample匯總的report展現(xiàn)。

fps_tracer_issue.png

StartUpTrace 應(yīng)用啟動統(tǒng)計

首先要明確的是統(tǒng)計的是應(yīng)用的啟動,這包括application創(chuàng)建過程而不單純是activity啟動。統(tǒng)計觸發(fā)一次就會銷毀,因此如果想統(tǒng)計activity之間跳轉(zhuǎn)的情況需手動獲取StartUpTrace并調(diào)用onCreate方法。

具體的統(tǒng)計指標(biāo)如下:

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

時間軸大致是這樣的:


startup時間軸.png

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

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

hook系統(tǒng)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;

    //此方法被靜態(tài)代碼塊調(diào)用 在被類resolve時執(zhí)行
    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對象內(nèi)部原有的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) {
        ...
        //優(yōu)先處理 設(shè)置一些值
        boolean isLaunchActivity = isLaunchActivity(msg);
        if (isLaunchActivity) {
            StartUpHacker.isEnterAnimationComplete = false;
        } else if (msg.what == ENTER_ANIMATION_COMPLETE) {
            //記錄activity轉(zhuǎn)場動畫結(jié)束標(biāo)志
            StartUpHacker.isEnterAnimationComplete = true;
        }
        if (!isCreated) {
            if (isLaunchActivity || msg.what == CREATE_SERVICE || msg.what == RECEIVER) {
                //以第一個Activity LAUNCH_ACTIVITY消息為止,記錄application創(chuàng)建結(jié)束時間
                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原理,我們來看一下統(tǒng)計時間的幾個關(guān)鍵節(jié)點是如何獲得的。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容