Matrix系列文章(一) 卡頓分析工具之Trace Canary

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

本篇爲卡頓分析系列文章之二,分析Trace Canary相關的原理,基於版本0.5.2.43。文章有點長,建議你先大體瀏覽一遍再細看,對你必定有幫助。第一篇傳送門Android卡頓檢測工具(一)BlockCanarygit

Matrix內容概覽

Matrix.png

可見Matrix做爲一個APM工具,在性能檢測方面仍是很是全面的,系列文章將會一一對它們進行分析。github

爲理清源代碼結構咱們先從初始化流程講起,項目地址Matrixjson

Matrix初始化流程

Matrix.Builder內部類配置Plugins。數組

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

//可選 感知插件狀態變化,onReportIssue獲取/處理issue
builder.patchListener(...);

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

//完成初始化
Matrix.init(builder.build());
複製代碼

Plugin結構

plugin類圖.png

目前配置的pluginbash

  • TracePlugin
  • ResourcePlugin
  • IOCanaryPlugin
  • SQLiteLintPlugin
  • ThreadWatcher
  • BatteryCanaryPlugin
  • MemoryCanaryPlugin

本篇分析的是TracePlugin,它與卡頓/UI渲染效率相關。微信

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

private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) {
    this.application = app;
    this.pluginListener = listener;
    this.plugins = plugins;
    //(1)
    AppActiveMatrixDelegate.INSTANCE.init(application);
    for (Plugin plugin : plugins) {
        //(2)
        plugin.init(application, pluginListener);
        pluginListener.onInit(plugin);
    }
}
複製代碼
  1. AppActiveMatrixDelegate是一個枚舉類(使人費解,枚舉性能並很差,此類做用跟普通的單例類同樣),在其init方法中爲application註冊了ActivityLifecycle和ComponentCallbacks2監聽,可見它是爲了拿到應用內部全部Activity生命週期狀態和內存緊缺狀態(onTrimMemory/onLowMemory)以供後續使用。框架

  2. 內部遍歷全部插件,並調用其init方法進行初始化,以後通知pluginListener生命週期方法onInit。異步

PluginListener包含的生命週期以下:

# -> PluginListener
public interface PluginListener {
    //初始化
    void onInit(Plugin plugin);
    //插件開始運行
    void onStart(Plugin plugin);
    //插件中止運行
    void onStop(Plugin plugin);
    //插件銷燬
    void onDestroy(Plugin plugin);
    //插件捕捉到Issue,包括卡頓、ANR等等
    void onReportIssue(Issue issue);
}
複製代碼

通常來講上層須要自定義一個的PluginListener,由於onReportIssue方法是具體處理Issue的關鍵方法,官方sample的作法是收到issue時彈出一個IssuesListActivity展現issue具體信息,而Matrix框架定義的DefaultPluginListener什麼都沒作。做爲接入方咱們可能會作更豐富的處理,好比序列化到本地、上傳雲端等等,全部的這一切都要從自定義PluginListener並實現onReportIssue方法開始。

patchListener方法簡單的爲成員變量賦值。

# -> 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;
}
複製代碼

Matrix結構

Matrix類圖.png

能夠看到Matrix提供了日誌管理器MatrixLogImpl,以及操做其內部全部plugin的各類方法。

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

TracePlugin

它是tracer管理器,其內部定義了四個跟蹤器。

  • AnrTracer ANR監測
  • EvilMethodTracer 耗時函數監測
  • FrameTracer 幀率監測
  • StartupTracer 啓動耗時

來看一下類圖:

tracer.png

這些跟蹤器都繼承於Tracer,它是一個抽象類,但不含抽象方法,已對繼承來的接口都作了默認實現。

爲了瞭解這些Tracer能實現哪些功能,咱們先來看看Tracer繼承父類和實現的接口。

1. LooperObserver

它是一個抽象類,內部定義了三個重要方法dispatchBegin/doFrame/dispatchEnd,但只是空實現,這三個方法都跟監聽主線程Handler的消息處理有關。當主線程處理一條消息前會回調dispatchBegin,消息處理完會先調用doFrame,而後再調用dispatchEnd。之因此這麼作是由於對於卡頓的檢測一般有兩種方式。

  1. 監聽主線程Handler的消息處理
  2. 監聽Choreographer的幀回調(doFrame)

第一種方式是經過hook Looper內部的logger對象實現的。系統Looper分發處理消息先後會經過logger對象打印日誌,hook這個logger至關於拿到了一條消息的先後時間點,根據兩者的時間差能夠作不少卡頓的分析,BlockCanary就是用此方法實現卡頓檢測,具體參看Android卡頓檢測工具(一)BlockCanary

第二種方式是Choreographer開放API,上層可設置FrameCallback監聽,從而得到每一幀繪製完畢的onFrame回調。經常使用的幀率監測工具(FPS)就是經過分析兩幀以前的時間差完成FPS的計算,好比TinyDancerTakt

實際上Matrix早期版本用的是第二種方式,最新版使用了第一種方式,由於能夠拿到更完整更清晰的堆棧信息。

至此,咱們能夠推斷Tracer具備感知幀率變化、統計卡頓的能力,因此跟幀率、函數耗時統計相關的Tracer(FrameTracer/EvilMethodTracer/AnrTracer)必定會繼續複寫doFrame方法,以實現具體功能。

2. ITracer

它是一個接口,繼承了IAppForeground接口,整體算下來一共四個抽象方法:onStartTrace、onCloseTrace、isAlive、onForeground。前三個方法是在描述Tracer自身的生命週期,由TracePlugin統一管理。當Activity先後臺狀態發生變化時回調Tracer的onForeground方法,所以Tracer具備感知Activity先後臺狀態變化的能力,它可用來作啓動分析。

在Tracer中大部分接口方法都是空實現,具體實現交由有需求的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的成員變量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插樁等等。筆者只是按源碼流程大體理出了最核心的內容,分支的技術點大多一筆略過,須要讀者自行補充,但願你們一塊兒加油,補足分支的技術棧。

相關文章
相關標籤/搜索