Matrix 之 StartupTracer 源碼分析

目的

image.png

  • 統計出 Application 的啓動時間
  • 統計出應用到開屏頁的時間
  • 統計出冷啓動時間
  • 統計出頁面的熱啓動時間

配置

配置部分在 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測試

image.png

該處是經過 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 的條件有:

  • 是不是啓動 Activity 操做
  • 是不是建立 service 服務
  • 是不是處理廣播操做

知足以上條件中的一個,即爲 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 打印出來

總結

總體的分析結束了,整體來看,函數插樁的設計仍是比較巧妙的,能在發生問題的時候,導出緣由進行分析。話很少,仍是照舊,就這樣吧

相關文章
相關標籤/搜索