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

前言

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注册页面生命周期回调的方式)。

kotlin 复制代码
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对象创建出来,看下构造方法。

typescript 复制代码
@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()。

arduino 复制代码
public StartupTracer(TraceConfig config) {
    ...
    ActivityThreadHacker.addListener(this);
}

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

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

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

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

scss 复制代码
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方法。这里只保留跟本次分析有关的内容。

scss 复制代码
@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

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

AppMethodBeat

Android性能优化系列-腾讯matrix-卡顿监控-gradle插件- 字节码插桩代码分析中分析字节码插桩时提到过,编译期为符合条件的方法入口插入AppMethodBeat.i(), 出口插入AppMethodBeat.o()用于统计方法执行耗时,这里就是从AppMethodBeat中取方法执行耗时信息。

onStartTrace

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

scss 复制代码
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。

ini 复制代码
@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的时机的。找到调用的位置可以看到下边的代码。

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

调用处是AppMethodBeat的at方法,在前边文章分析matrix字节码插桩时,Android性能优化系列-腾讯matrix-卡顿监控-gradle插件- 字节码插桩代码分析中提到过,AppMethodBeat的at方法是被插入到Activity的onWindowFocusChanged方法中的,所以这里的onActivityFocused其实就是对应于 Activity的onWindowFocusChanged方法,matrix将这一时机作为页面可见的点,所以onActivityFocused执行时,onActivityCreated已经执行过了。

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

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

而热启动只有warmCost一个值。

  1. warmCost:表示activity创建到页面可见消耗的时长。
scss 复制代码
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注册后的一个回调方法之一。

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

onForeground

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

stop

TracePlugin的stop方法调用了StartupTracer的onCloseTrace

arduino 复制代码
final synchronized public void onCloseTrace() {
    if (isAlive) {
        this.isAlive = false;
        onDead();
    }
}

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

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

destroy

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

AnalyseTask

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

arduino 复制代码
@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依赖于字节码插桩,在分析启动慢问题时需要借助字节码插桩记录的方法执行耗时信息,将耗时信息完整的展现出来,方便分析人员正确的定位到问题所在。

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

  • ActivityThreadHacker是怎么监听到application的创建完成的?
  • AppMethodBeat.getInstance().copyData()做了什么,什么逻辑?
  • UIThreadMonitor做了什么?
  • 另外,我们对AnalyseTask的分析其实不够深入,这里的逻辑也比较关键

这四个问题将会分别进行独立的分析,敬请期待。

相关推荐
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
2501_915918418 小时前
Fiddler中文版全面评测:功能亮点、使用场景与中文网资源整合指南
android·ios·小程序·https·uni-app·iphone·webview
wen's9 小时前
React Native安卓刘海屏适配终极方案:仅需修改 AndroidManifest.xml!
android·xml·react native
编程乐学10 小时前
网络资源模板--基于Android Studio 实现的聊天App
android·android studio·大作业·移动端开发·安卓移动开发·聊天app
EndingCoder11 小时前
搜索算法在前端的实践
前端·算法·性能优化·状态模式·搜索算法
没有了遇见12 小时前
Android 通过 SO 库安全存储敏感数据,解决接口劫持问题
android
hsx66612 小时前
使用一个 RecyclerView 构建复杂多类型布局
android
hsx66612 小时前
利用 onMeasure、onLayout、onDraw 创建自定义 View
android
守城小轩12 小时前
Chromium 136 编译指南 - Android 篇:开发工具安装(三)
android·数据库·redis
whysqwhw13 小时前
OkHttp平台抽象机制分析
android