Matrix 之 StartupTracer 源码分析

简介: Matrix 之 StartupTracer 源码分析

目的


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);
        }
    }
复制代码


启动部分是在主线程启动监测, onStartTraceStartupTracer 的父类方法,该方法最终会调用子类实现的 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 打印出来

目录
相关文章
|
存储 NoSQL API
[Eigen中文文档] Matrix类
在Eigen中,所有矩阵和向量都是Matrix模板类的对象。向量只是行数或者列数为1的特殊矩阵。
408 1
|
监控 数据可视化 Java
Matrix原理分析系列之开篇
Matrix原理分析系列之开篇
478 0
Matrix原理分析系列之开篇
|
存储 JSON 算法
Matrix源码分析系列-如何解析应用安装包(一)
Matrix源码分析系列-如何解析应用安装包
238 0
Matrix源码分析系列-如何解析应用安装包(一)
|
数据可视化
Matrix源码分析系列-如何计算启动耗时(二)
Matrix源码分析系列-如何计算启动耗时
179 0
Matrix源码分析系列-如何计算启动耗时(二)
|
监控 Java 测试技术
Matrix源码分析系列-如何计算启动耗时(一)
Matrix源码分析系列-如何计算启动耗时
186 0
Matrix源码分析系列-如何计算启动耗时(一)
|
存储 Java API
Matrix 之 ResourceCanary 源码分析
Matrix 之 ResourceCanary 源码分析
259 0
Matrix 之 ResourceCanary 源码分析
|
Java Android开发 开发者
Matrix 之 TraceCanary 源码分析
Matrix 之 TraceCanary 源码分析
471 0
Matrix 之 TraceCanary 源码分析
|
索引
Matrix源码分析系列-如何解析应用安装包(二)
Matrix源码分析系列-如何解析应用安装包
85 0
|
JSON Java Android开发
Matrix 之 ApkChecker 的使用
Matrix 之 ApkChecker 的使用
257 0
Matrix 之 ApkChecker 的使用
|
JSON 数据格式
Matrix 之 FrameTracer 源码分析
Matrix 之 FrameTracer 源码分析
169 0