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作為一個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
- 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提供了日志管理器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 啟動耗時
來看一下類圖:

這些跟蹤器都繼承于BaseTracer,BaseTracer為抽象類,唯一的抽象方法是getTag方法。子類實現(xiàn)僅僅定義一個名稱即可。
再來看看BaseTracer實現(xiàn)的接口
ApplicationLifeObserver.IObserver
當(dāng)activity前后臺切換或者生命周期發(fā)生變化時會回調(diào)接口方法。至于是如何監(jiān)控的,邏輯都在ApplicationLifeObserver中,這個我們稍后分析。因此BaseTracer具有感知activity生命周期及應(yīng)用前后臺狀態(tài)變化的能力。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ù)寫了此方法。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)。

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。

內(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的工作流程是這樣的:
-
首先要記錄各個函數(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 - doFrame檢查兩幀之間的時間差,如果大于卡頓閾值(默認(rèn)為1s),則會調(diào)用handleBuffer觸發(fā)統(tǒng)計排查任務(wù)。
- handlerBuffer中啟動AnalyseTask任務(wù)分析過濾method調(diào)用stack、函數(shù)耗時等,并保存在jsonObject中。
- 調(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)容。

文件每一行代表一個插樁方法。
以第一行為例:
-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:

我們重點關(guān)心的是
- cost bad函數(shù)表示總耗時。
- stack bad函數(shù)調(diào)用棧。
- 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é)果。

實測發(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();
}
}
...
}
}
整個流程如下
- FPSTracer中定義類型為LinkedList<Integer>的成員變量mFrameDataList,用于記錄時間差和scene(activity或fragment名)信息。
-
計算兩次兩次doFrame時間差,記錄在一個int數(shù)中。其中高10位表示sceneId,低22位表示耗時ms*OFFSET_TO_MS(默認(rèn)為100)。
frame數(shù)據(jù)存儲.png - 以兩分鐘(getFPSReportInterval默認(rèn)值,官方sample為10秒)為一個周期統(tǒng)計frame信息,計時結(jié)束后觸發(fā)onTimeExpire回調(diào)方法。
- onTimeExpire調(diào)用doReport做統(tǒng)計分析。
- 同一個場景下累計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())
- isInvalid 表示是否非法,當(dāng)activity resume后為false,pause后為true。也即只統(tǒng)計resume階段,因為activity真正繪制是從onResume開始。
- 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)計幀信息。 - isEnterAnimationComplete 入場動畫執(zhí)行完。
- isTargetScene FPSTrace可配置監(jiān)控界面白名單,默認(rèn)全部監(jiān)控。
這樣真?zhèn)€fps檢測流程也就結(jié)束了,我們來看一下官方sample匯總的report展現(xiàn)。

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存在) |
時間軸大致是這樣的:

為了實現(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é)點是如何獲得的。
- 程序啟動 實際上是MethodBeat類的一段靜態(tài)代碼塊,我們知道靜態(tài)代碼塊在解析類的時候就執(zhí)行了,拿它作為程序計時的起點也算正常。
- 系統(tǒng)LAUNCH_ACTIVITY消息發(fā)出 通過hook mH類完成。
- 收到onActivityCreated回調(diào) 通過為aplication注冊registerActivityLifecycleCallbacks來感知應(yīng)用內(nèi)activity生命周期。
- 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ù)棧。

