likes
comments
collection
share

Android性能优化系列-腾讯matrix-TracePlugin启动优化之StartupTracer源码分析

作者站长头像
站长
· 阅读数 29

前言

StartupTracer是matrix中用来监控启动速度的一个trace类,代码位于matrix-trace-canary模块,属于TracePlugin中的专门针对启动场景的一种监控能力。在TracePlugin中还有各种类型的tracer,它们肩负着对不同场景下问题的监控,但核心都是卡顿监控,如帧率监控、慢方法监控、anr监控等等。这篇文章主要分析启动场景下的速度监控。

TracePlugin

由于StartupTracer依附于TracePlugin,所以我们需要先了解一下TracePlugin,TracePlugin继承自Plugin, Plugin是matrix中所有类型插件(插件指matrix中针对各方向的监控能力如io监控、memory监控,matrix将其定义为一个插件,因为每个功能都是可插拔的)的基类,负责定义插件运行的生命周期。其中生命周期包括init、start、onForeground、stop和destroy。matrix运行每个插件时,先执行init,再执行start。

init

TracePlugin初始化时,会先执行父类Plugin的init方法。简单看下父类的init,init中一个关键点,ProcessUILifecycleOwner将当前plugin加入到集合,可以监听到onForeground方法(在ProcessUILifecycleOwner初始化的时候通过app.registerActivityLifecycleCallbacks注册页面生命周期回调的方式)。

public void init(Application app, PluginListener listener) {
    //插件运行的状态
    status = PLUGIN_INITED;
    //保存信息
    this.application = app;
    this.pluginListener = listener;
    //回调生命周期
    listener.onInit(this);
    //将当前plugin加入到集合,可以监听到onForeground方法
    ProcessUILifecycleOwner.INSTANCE.addListener(this);
}

回到TracePlugin自己的init方法,可以看到有多个tracer的初始化,本次我们只关注StartupTracer,init的时候将StartupTracer对象创建出来,看下构造方法。

@Override
public void init(Application app, PluginListener listener) {
    super.init(app, listener);
    if (sdkInt >= Build.VERSION_CODES.O) {
        //默认为false,8.0以上为true,这里涉及到一个比较关键的点,先有个印象
        supportFrameMetrics = true;
    }
    ...
    //启动监控
    startupTracer = new StartupTracer(traceConfig);
}

StartupTracer构造方法。其中ActivityThreadHacker.addListener(this)是一个关键的操作, 通过将当前plugin加入到监听中,可以拿到application创建完成的回调-onApplicationCreateEnd()。

public StartupTracer(TraceConfig config) {
    ...
    ActivityThreadHacker.addListener(this);
}

留一个疑问?ActivityThreadHacker是怎么监听到application的创建完成的? 为了使流程分析更清晰,这里会单独列一篇文章来讲。

继续看看onApplicationCreateEnd方法做了什么。

@Override
public void onApplicationCreateEnd() {
    if (!isHasActivity) {
        //拿到application创建的时长,进入分析
        long applicationCost = ActivityThreadHacker.getApplicationCost();
        analyse(applicationCost, 0, applicationCost, false);
    }
}

analyse方法中,第一个参数表示application创建消耗的时长,第二个参数表示应用启动第一个页面消耗的时长,第三个总时长,第四个参数表示是否是热启动,很明显这里是false。

private void analyse(long applicationCost, long firstScreenCost, long allCost, boolean isWarmStartUp) {
    long[] data = new long[0];
    //冷启动,如果时长超过冷启动设置的阈值(默认10s),则将这段时间内运行的所有方法的耗时信息取出来进行分析。
    if (!isWarmStartUp && allCost >= coldStartupThresholdMs) { // for cold startup
        data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sApplicationCreateBeginMethodIndex);
        ActivityThreadHacker.sApplicationCreateBeginMethodIndex.release();

    }
    //热启动时,如果时长超过热启动设置的阈值(默认4s),则将这段时间内运行的所有方法的耗时信息取出来进行分析。
    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));

}

从上边的方法中,我们提取出两个关键点问题:

  1. AppMethodBeat.getInstance().copyData()做了什么,什么逻辑?
  2. AnalyseTask内部是怎么实现的?

这两个问题后边单独来解答,先一窥全貌,然后再细化分析。

start

初始化完成,开始start方法,父类中做的很简单,不再多看,直接看TracePlugin的start方法。这里只保留跟本次分析有关的内容。

@Override
public void start() {
    super.start();
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if (willUiThreadMonitorRunning(traceConfig)) {
                if (!UIThreadMonitor.getMonitor().isInit()) {
                    try {
                        UIThreadMonitor.getMonitor().init(traceConfig, supportFrameMetrics);
                    } catch (java.lang.RuntimeException e) {
                        return;
                    }
                }
            }

            if (traceConfig.isAppMethodBeatEnable()) {
                AppMethodBeat.getInstance().onStart();
            } else {
                AppMethodBeat.getInstance().forceStop();
            }

            UIThreadMonitor.getMonitor().onStart();
            ...
            if (traceConfig.isStartupEnable()) {
                //启动插件
                startupTracer.onStartTrace();
            }


        }
    };
    ...
}

其中关键的几项:

UIThreadMonitor

UIThreadMonitor与StartupTracer关联性不强,这里先有个印象。它是traceplugin中非常关键的一个类,它的内部通过setMessageLogging的方式来实现监听主线程每一条消息起始的回调,从而获取到每一条消息执行的耗时信息;以及通过对Choreographer的操作来获取消息队列中不同类型的消息(input、animation、traversal)执行的消耗总时长。

AppMethodBeat

onStartTrace

StartupTracer终于要开始启动了。onStartTrace执行后会进入StartupTracer的onAlive方法,从下边可以看出,StartupTracer的start方法做的工作并不多,只是做了监听,

protected void onAlive() {
    super.onAlive();
    MatrixLog.i(TAG, "[onAlive] isStartupEnable:%s", isStartupEnable);
    if (isStartupEnable) {
        //注册Listener,可以拿到onActivityFocused的回调
        AppMethodBeat.getInstance().addListener(this);
        //注册activity生命周期监听
        Matrix.with().getApplication().registerActivityLifecycleCallbacks(this);
    }
}

既然如此,我们就要把目光转移到监听的回调方法上去了。

onActivityCreated

Matrix.with().getApplication().registerActivityLifecycleCallbacks的回调方法是activity的各个生命周期方法,而这里使用到的也就只有onActivityCreated和下边的onActivityDestroyed。

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    MatrixLog.i(TAG, "activeActivityCount:%d, coldCost:%d", activeActivityCount, coldCost);
    //activity数量为0,但是coldCost有值,表示热启动
    if (activeActivityCount == 0 && coldCost > 0) {
        lastCreateActivity = uptimeMillis();
        isWarmStartUp = true;
    }
    activeActivityCount++;
    if (isShouldRecordCreateTime) {
        //记录当前activity的创建时间
        createdTimeMap.put(activity.getClass().getName() + "@" + activity.hashCode(), uptimeMillis());
    }
}

onActivityFocused

AppMethodBeat的listener的回调方法是onActivityFocused。首先要了解的是这个方法究竟是什么时候回调的,matrix是将什么时机作为页面focus的时机的。找到调用的位置可以看到下边的代码。

public static void at(Activity activity, boolean isFocus) {
   synchronized (listeners) {
       for (IAppMethodBeatListener listener : listeners) {
          listener.onActivityFocused(activity);
       }
   }
}

好了,了解了它的来源,我们再看看它做了什么,只保留关键代码。可以看到这里是分为冷启动和热启动两种情况来分析的。具体的计算过程代码被我删掉了,冷启动最终会计算出这几个参数:

  1. applicationCost: application消耗的时长。
  2. firstScreenCost: 从application创建到第一个页面可见消耗的时长,一般第一个页面就是splash页面。
  3. coldCost:冷启动的总时长,是从application创建到主页页面可见消耗的时长。

而热启动只有warmCost一个值。

  1. warmCost:表示activity创建到页面可见消耗的时长。
public void onActivityFocused(Activity activity) {
    if (isColdStartup()) {
        ...
        analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, coldCost, false);
    } else if (isWarmStartUp()) {
        ...
        analyse(0, 0, warmCost, true);
    }
}

然后进入analyse进行分析,analyse方法上边提到过一次,就是在onApplicationCreateEnd方法中,也就是application执行完成后的一个时机,同样进行过分析,最终会开启一个AnalyseTask进行分析。所以AnalyseTask我们稍后统一看源码,这里先了解是在分析数据即可。

onActivityDestroyed

Matrix.with().getApplication().registerActivityLifecycleCallbacks注册后的一个回调方法之一。

@Override
public void onActivityDestroyed(Activity activity) {
    //activeActivityCount是未销毁的activity的数量,每次destroy后-1
    activeActivityCount--;
}

onForeground

跟启动分析关联不大,不深入探讨了。

stop

TracePlugin的stop方法调用了StartupTracer的onCloseTrace

final synchronized public void onCloseTrace() {
    if (isAlive) {
        this.isAlive = false;
        onDead();
    }
}

没什么关键操作,其实对StartupTracer来说也没有需要释放的资源。

@CallSuper
protected void onDead() {
    MatrixLog.i(TAG, "[onDead] %s", this.getClass().getName());
}

destroy

更新插件状态,是生命周期的处理,没有特别操作。

AnalyseTask

最后再来看一下AnalyseTask,它到底是怎么分析问题的?看一下run方法。它会将收集到的方法信息进行统计,并过滤掉部分方法,然后report,app启动的时长等信息都会被携带,最关键的还是运行期间的一系列的方法耗时信息,通过这些信息才能真正定位到执行缓慢的逻辑。

@Override
public void run() {
    LinkedList<MethodItem> stack = new LinkedList();
    if (data.length > 0) {
        //data是long数组,将它转到stack中,
        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();
                }

            }
        });
    }

    StringBuilder reportBuilder = new StringBuilder();
    StringBuilder logcatBuilder = new StringBuilder();
    long stackCost = Math.max(allCost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));
    String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);

    // for logcat
    if ((allCost > coldStartupThresholdMs && !isWarmStartUp)
            || (allCost > warmStartupThresholdMs && isWarmStartUp)) {
        MatrixLog.w(TAG, "stackKey:%s \n%s", stackKey, logcatBuilder.toString());
    }

    // report
    report(applicationCost, firstScreenCost, reportBuilder, stackKey, stackCost, isWarmStartUp, scene);
}

总结

至此,StartupTracer的源码就分析完了,StartupTracer的逻辑比较简单,它借助了系统的一些回调方法,如Activity生命周期的方法,onCreate, onWindowFocusChanged等,当然也借助了hook,从而统计app启动的时长,分别记录application启动时长,第一个activity启动的时长,以及最终冷启动热启动的时长。核心点在于,StartupTracer依赖于字节码插桩,在分析启动慢问题时需要借助字节码插桩记录的方法执行耗时信息,将耗时信息完整的展现出来,方便分析人员正确的定位到问题所在。

回顾一下,这篇文章中我们遗留了几个问题未处理: