目的
配置
配置部分在 TracePlugin 的初始化阶段:
TracePlugin.java
public void init(Application app, PluginListener listener) { ... startupTracer = new StartupTracer(traceConfig); ... } 复制代码
启动
启动部分也是通过 TracePlugin 来触发启动:
TracePlugin.java
@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
方法:
@Override protected void onAlive() { super.onAlive(); if (isStartupEnable) { // ②、注册监听 AppMethodBeat.getInstance().addListener(this); // ①、注册 Activity 的创建过程 Matrix.with().getApplication().registerActivityLifecycleCallbacks(this); } } 复制代码
①注册 Activity 的监听
先来看下面那段代码,该监听只实现了 onActivityCreated 方法:
@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
方法:
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
的调用源 :
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 的条件有:
- 是否是启动 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 打印出来