Matrix 之 StartupTracer 源码分析

2,317 阅读7分钟

目的

image.png

  • 统计出 Application 的启动时间
  • 统计出应用到开屏页的时间
  • 统计出冷启动时间
  • 统计出页面的热启动时间

配置

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

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 打印出来

总结

整体的分析结束了,总体来看,函数插桩的设计还是比较巧妙的,能在发生问题的时候,导出原因进行分析。话不多,还是照旧,就这样吧