配置部分在 TracePlugin 的初始化階段:java
TracePlugin.javaandroid
public void init(Application app, PluginListener listener) {
...
startupTracer = new StartupTracer(traceConfig);
...
}
複製代碼
啓動部分也是經過 TracePlugin 來觸發啓動:git
TracePlugin.javagithub
@Override
public void start() {
super.start();
...
Runnable runnable = new Runnable() {
@Override
public void run() {
...
startupTracer.onStartTrace();
...
}
};
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
runnable.run();
} else {
MatrixHandlerThread.getDefaultMainHandler().post(runnable);
}
}
複製代碼
啓動部分是在主線程啓動監測, onStartTrace
是 StartupTracer
的父類方法,該方法最終會調用子類實現的 onAlive
方法:app
@Override
protected void onAlive() {
super.onAlive();
if (isStartupEnable) {
// ②、註冊監聽
AppMethodBeat.getInstance().addListener(this);
// ①、註冊 Activity 的建立過程
Matrix.with().getApplication().registerActivityLifecycleCallbacks(this);
}
}
複製代碼
①註冊 Activity 的監聽
先來看下面那段代碼,該監聽只實現了 onActivityCreated 方法:ide
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// Activity 啓動數量爲 0 ,冷啓動的時間大於 0,則開啓熱啓動可用的開關
if (activeActivityCount == 0 && coldCost > 0) {
isWarmStartUp = true;
}
// 記錄 Activity 的啓動數量
activeActivityCount++;
}
複製代碼
這個地方須要注意一下,在 sample 代碼中,你會發現沒法啓用熱啓動的時間監測,主要緣由是由於:熱啓動每次檢測時都會給本身設置一個 false 重置,但每次打開頁面 activeActivityCount 又會累加,activeActivityCount == 0 就會爲 false,因此沒法設置熱啓用,若是想測試每一個頁面的熱啓動的話,能夠註釋掉 activeActivityCount == 0 的判斷。函數
②註冊監聽:
該監聽會被 AppMethodBeat 註冊在一個集合當中,在翻閱是誰來觸發該監聽時,咱們找到 at
方法:oop
public static void at(Activity activity, boolean isFocus) {
String activityName = activity.getClass().getName();
if (isFocus) {
if (sFocusActivitySet.add(activityName)) {
synchronized (listeners) {
for (IAppMethodBeatListener listener : listeners) {
listener.onActivityFocused(activityName);
}
}
}
} else {
if (sFocusActivitySet.remove(activityName)) {
MatrixLog.i(TAG, "[at] visibleScene[%s] has %s focus!", getVisibleScene(), "detach");
}
}
}
複製代碼
而後在繼續追蹤 at
的調用處時發現,並未有調用源頭,既然沒有,那能夠推測是不是插件代替咱們完成了該事情,而後咱們在追蹤 matrix-gradle-plugin 的插件源碼時,發現了 at
的調用源 :post
MethodTracer.java測試
該處是經過 ASM 爲每一個 Activity 的 onWindowFocusChanged 方法注入一個 AppMethodBeat.getInstantce().at()
調用代碼,那麼這裏就能夠解釋清了,在 Activity 觸發 onWindowFocusChanged 方法時,就會遍歷調用全部註冊的監聽,監聽的方式是 onActivityFocused ,這個方法是最重要的部分,咱們來看看他的實現過程:
@Override
public void onActivityFocused(String activity) {
// 是不是冷啓動
if (isColdStartup()) {
if (firstScreenCost == 0) {
// ①、計算第一屏花費時間
this.firstScreenCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime();
}
// 是否已經展現過了 Splash 頁面,默認是 false
if (hasShowSplashActivity) {
// ②、設置冷啓動時間
coldCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime();
} else {
// 是否包含 SplashActivity
if (splashActivities.contains(activity)) {
// 設置已經展現了 Splash 頁面
hasShowSplashActivity = true;
} else if (splashActivities.isEmpty()) {
// ③、設置冷啓動時間
coldCost = firstScreenCost;
} else {
MatrixLog.w(TAG, "pass this activity[%s] at duration of start up! splashActivities=%s", activity, splashActivities);
}
}
if (coldCost > 0) {
// 分析冷啓動時間
analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, coldCost, false);
}
} else if (isWarmStartUp()) {
// 熱啓動重置
isWarmStartUp = false;
// ④、獲取熱啓動時間
long warmCost = uptimeMillis() - ActivityThreadHacker.getLastLaunchActivityTime();
if (warmCost > 0) {
//⑤、分析熱啓動時間
analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, warmCost, true);
}
}
}
複製代碼
①計算第一屏時間:
這個地方須要看一下 ActivityThreadHacker.getEggBrokenTime()
的時間戳,根據代碼追蹤,能夠找到以下代碼:
ActivityThreadHacker.java
public static void hackSysHandlerCallback() {
try {
sApplicationCreateBeginTime = SystemClock.uptimeMillis();
...
}
複製代碼
看這個方法名,確定是在作 hook ActivityThread 的 mH 類,而後設置了 mH 的 Callback 來監聽 Activity 的啓動,這個代碼相信學過插件化的同窗會十分熟悉,然我咱們繼續追蹤他的調用源在哪,而後查到了以下:
AppMethodBeat.java
private static void realExecute() {
...
ActivityThreadHacker.hackSysHandlerCallback();
...
}
public static void i(int methodId) {
...
realExecute();
...
}
複製代碼
最後追蹤到靜態方法 i ,咱們經過文檔知道,靜態 i 方法是 AppMethodBeat 用來統計方法耗時的,但我想看他的調用源是從哪一個方法執行的,目前從源碼來看沒法知道靜態方法 i 調用 realExecute 的是哪一個方法觸發的,不要緊,咱們能夠用 StackTraceElement 來查看調用棧,更改靜態 i 方法以下:
public static void i(int methodId) {
...
realExecute();
StackTraceElement ss = new Throwable().getStackTrace()[1];
Log.e(TAG, "StackTraceElement: "+methodId+"--"+ss.getClassName()+"--"+ss.getMethodName()+"--"+ss.getLineNumber() );
...
}
複製代碼
打印信息以下:
StackTraceElement: 2--sample.tencent.matrix.MatrixApplication--onCreate--1
能夠看出,觸發 hackSysHandlerCallback 的是 Application 的 onCreate 方法,那麼,ActivityThreadHacker.getEggBrokenTime()
的時間就是 Application 的 onCreate 時的時間,那麼第一屏的計算時間爲:
第一屏時間 = Activity onActivityFocused 時間戳 - Application onCreate 時間戳
這和文章開頭圖的 firstScreenCost 是一致的
②③ 設置冷啓動時間:
這個地方 ② 和 ③ 一塊兒看吧,這兩處都是設置冷啓動時間,其實,從 sample demo 來看,僅僅只有 ② 處的纔會被設置,在 MatrixApplication 中,咱們能夠看到配置代碼:
TraceConfig traceConfig = new TraceConfig.Builder()
...
.splashActivities("sample.tencent.matrix.SplashActivity;")
...
.build();
複製代碼
TracePlugin 配置了 splash 頁面, hasShowSplashActivity
默認爲 false ,sample 啓動了 SplashActivity 頁面,這時候代碼走到了 splashActivities.contains(activity)
的狀況,而且設置了 hasShowSplashActivity
爲 true,這時候的 coldCost 仍然是爲 0 ,冷啓動的過程在下面頁面過來時,仍然走這個判斷,待 MainActivity 頁面進入時,hasShowSplashActivity
爲 true,設置了冷啓動時間爲:
冷啓動時間 = MainActivity onActivityFocused 時間戳 - Application onCreate 時間戳
若是 MatrixApplication 未配置 splashActivities,冷啓動時間 == 第一屏時間
④熱啓動時間:
熱啓動時間的計算在前面說過,因爲 onActivityCreated
方法中的 activeActivityCount == 0
判斷,致使 isWarmStartUp
一直沒法爲 true,因此,熱啓動沒法使用,咱們來更改下源碼:
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// 打開 activeActivityCount 便可查看熱啓動
if (coldCost > 0) {
Log.e(TAG, "onActivityCreated: " + activity.getComponentName().getClassName());
isWarmStartUp = true;
}
}
複製代碼
將 activeActivityCount == 0
的代碼移除,咱們知道,coldCost 是在 MainActivity 啓動以後纔會設置,因此,想看 warmStartUp 的啓動時間,只須要點擊 MainActivity 中的其餘跳轉頁面,便可查看
⑤:分析熱啓動時間
分析熱啓動時間和冷啓動時間是同樣的代碼的,因此,咱們就分析一處就好了,先來看一下調用 analyse 處的 ActivityThreadHacker.getApplicationCost()
代碼,看方法名稱是獲取 Application 的啓動時間,繼續追蹤下去來到:
ActivityThreadHacker.java
public static long getApplicationCost() {
return ActivityThreadHacker.sApplicationCreateEndTime - ActivityThreadHacker.sApplicationCreateBeginTime;
}
複製代碼
sApplicationCreateBeginTime
開始時間咱們知道,在 ① 中已經分析過,而後咱們繼續追蹤 sApplicationCreateEndTime
是在哪被賦值的,最終走到以下代碼:
private final static class HackCallback implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
...
boolean isLaunchActivity = isLaunchActivity(msg);
...
if (!isCreated) {
if (isLaunchActivity || msg.what == CREATE_SERVICE || msg.what == RECEIVER) { // todo for provider
// 設置 application 結束時間
ActivityThreadHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
ActivityThreadHacker.sApplicationCreateScene = msg.what;
isCreated = true;
}
}
...
複製代碼
HackCallback 是 hook ActivityThread 的 mH 設置的,判斷爲 true 的條件有:
知足以上條件中的一個,即爲 Application 已經初始化結束
而後咱們繼續看 analyse 方法:
private void analyse(long applicationCost, long firstScreenCost, long allCost, boolean isWarmStartUp) {
long[] data = new long[0];
// ①
if (!isWarmStartUp && allCost >= coldStartupThresholdMs) { // for cold startup
data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sApplicationCreateBeginMethodIndex);
ActivityThreadHacker.sApplicationCreateBeginMethodIndex.release();
// ②
} else if (isWarmStartUp && allCost >= warmStartupThresholdMs) {
data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sLastLaunchActivityMethodIndex);
ActivityThreadHacker.sLastLaunchActivityMethodIndex.release();
}
// ③
MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(data, applicationCost, firstScreenCost, allCost, isWarmStartUp, ActivityThreadHacker.sApplicationCreateScene));
}
複製代碼
①:
!isWarmStartUp 表示當前不是熱啓動,那也就意味着進入的是冷啓動,allCost 即爲 coldCost 冷啓動時間,coldStartupThresholdMs 是 TraceConfig 配置的時間,追蹤代碼發現,默認的冷啓動時間爲 4 * 1000
毫秒,若是超過這個值,即爲冷啓動發生了異常,須要分析緣由,因爲篇幅緣由,分析緣由就再也不追溯下去了,我這裏給個大概意思:將 Application 中全部插樁函數的 methodId 給查出來
②:
當前是熱啓動,allCost 即爲 warmCost,warmStartupThresholdMs 是 TraceConfig 配置的時間,追蹤代碼發現,默認的熱啓動時間爲 10 * 1000
毫秒,道理和上面同樣,大概意思是:會檢查最後一個啓動的 Activity 的全部插樁函數,拿到 methodId 集
③:
執行分析任務,分析任務就是啓動發生異常時,分析 data 數據,而後將啓動數據封裝成 JsonObject,分發給 onDetectIssue 回調,這個地方咱們就看看 data 異常分析部分吧,封裝回調部分就不看了
AnalyseTask.java
LinkedList<MethodItem> stack = new LinkedList();
if (data.length > 0) {
// ①
TraceDataUtils.structuredDataToStack(data, stack, false, -1);
// ②
TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {
@Override
public boolean isFilter(long during, int filterCount) {
return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS;
}
@Override
public int getFilterMaxCount() {
return Constants.FILTER_STACK_MAX_COUNT;
}
@Override
public void fallback(List<MethodItem> stack, int size) {
MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack);
Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK));
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
}
});
}
...
複製代碼
①:
根據以前 data 查到的 methodId ,拿到對應插樁函數的執行時間、執行深度,將每一個函數的信息封裝成 MethodItem,而後存儲到 stack 集合當中
②:
根據 stack 集合數據,分析出是哪一個方法是慢函數,並回調給 fallback 打印出來
總體的分析結束了,整體來看,函數插樁的設計仍是比較巧妙的,能在發生問題的時候,導出緣由進行分析。話很少,仍是照舊,就這樣吧