Matrix系列文章(一) 卡顿分析工具之Trace Canary

5,762 阅读16分钟

Matrix是微信开源的一套完整的APM解决方案,内部包含Resource Canary(资源监测)/Trace Canary(卡顿监测)/IO Canary(IO监测)等。

本篇为卡顿分析系列文章之二,分析Trace Canary相关的原理,基于版本0.5.2.43。文章有点长,建议你先大致浏览一遍再细看,对你一定有帮助。第一篇传送门Android卡顿检测工具(一)BlockCanary

Matrix内容概览

Matrix.png

可见Matrix作为一个APM工具,在性能检测方面还是非常全面的,系列文章将会一一对它们进行分析。

为理清源代码结构我们先从初始化流程讲起,项目地址Matrix

Matrix初始化流程

Matrix.Builder内部类配置Plugins。

//创建builder
Matrix.Builder builder = new Matrix.Builder(this);

//可选 感知插件状态变化,onReportIssue获取/处理issue
builder.patchListener(...);

//可选 配置插件 
builder.plugin(tracePlugin);
builder.plugin(ioCanaryPlugin);

//完成初始化
Matrix.init(builder.build());

Plugin结构

plugin类图.png

目前配置的plugin

  • TracePlugin
  • ResourcePlugin
  • IOCanaryPlugin
  • SQLiteLintPlugin
  • ThreadWatcher
  • BatteryCanaryPlugin
  • MemoryCanaryPlugin

本篇分析的是TracePlugin,它与卡顿/UI渲染效率相关。

Matrix.Builder调用build方法触发Matrix构造函数。

private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) {
    this.application = app;
    this.pluginListener = listener;
    this.plugins = plugins;
    //(1)
    AppActiveMatrixDelegate.INSTANCE.init(application);
    for (Plugin plugin : plugins) {
        //(2)
        plugin.init(application, pluginListener);
        pluginListener.onInit(plugin);
    }
}
  1. AppActiveMatrixDelegate是一个枚举类(令人费解,枚举性能并不好,此类作用跟普通的单例类一样),在其init方法中为application注册了ActivityLifecycle和ComponentCallbacks2监听,可见它是为了拿到应用内部所有Activity生命周期状态和内存紧缺状态(onTrimMemory/onLowMemory)以供后续使用。

  2. 内部遍历所有插件,并调用其init方法进行初始化,之后通知pluginListener生命周期方法onInit。

PluginListener包含的生命周期如下:

# -> PluginListener
public interface PluginListener {
    //初始化
    void onInit(Plugin plugin);
    //插件开始运行
    void onStart(Plugin plugin);
    //插件停止运行
    void onStop(Plugin plugin);
    //插件销毁
    void onDestroy(Plugin plugin);
    //插件捕捉到Issue,包括卡顿、ANR等等
    void onReportIssue(Issue issue);
}

一般来说上层需要自定义一个的PluginListener,因为onReportIssue方法是具体处理Issue的关键方法,官方sample的做法是收到issue时弹出一个IssuesListActivity展示issue具体信息,而Matrix框架定义的DefaultPluginListener什么都没做。作为接入方我们可能会做更丰富的处理,比如序列化到本地、上传云端等等,所有的这一切都要从自定义PluginListener并实现onReportIssue方法开始。

patchListener方法简单的为成员变量赋值。

# -> Matrix.Builder
public Builder patchListener(PluginListener pluginListener) {
    this.pluginListener = pluginListener;
    return this;
}

最终来看Matrix的init方法,其实就是为其静态成员变量sInstance赋值。

# -> Matrix
public static Matrix init(Matrix matrix) {
    if (matrix == null) {
        throw new RuntimeException("Matrix init, Matrix should not be null.");
    }
    synchronized (Matrix.class) {
        if (sInstance == null) {
            sInstance = matrix;
        } else {
            MatrixLog.e(TAG, "Matrix instance is already set. this invoking will be ignored");
        }
    }
    return sInstance;
}

Matrix结构

Matrix类图.png

可以看到Matrix提供了日志管理器MatrixLogImpl,以及操作其内部所有plugin的各种方法。

接下来进入正题,我们来看看卡顿(UI渲染性能)分析模块TracePlugin是如何工作的。

TracePlugin

它是tracer管理器,其内部定义了四个跟踪器。

  • AnrTracer ANR监测
  • EvilMethodTracer 耗时函数监测
  • FrameTracer 帧率监测
  • StartupTracer 启动耗时

来看一下类图:

tracer.png

这些跟踪器都继承于Tracer,它是一个抽象类,但不含抽象方法,已对继承来的接口都做了默认实现。

为了了解这些Tracer能实现哪些功能,我们先来看看Tracer继承父类和实现的接口。

1. LooperObserver

它是一个抽象类,内部定义了三个重要方法dispatchBegin/doFrame/dispatchEnd,但只是空实现,这三个方法都跟监听主线程Handler的消息处理有关。当主线程处理一条消息前会回调dispatchBegin,消息处理完会先调用doFrame,然后再调用dispatchEnd。之所以这么做是因为对于卡顿的检测通常有两种方式。

  1. 监听主线程Handler的消息处理
  2. 监听Choreographer的帧回调(doFrame)

第一种方式是通过hook Looper内部的logger对象实现的。系统Looper分发处理消息前后会通过logger对象打印日志,hook这个logger相当于拿到了一条消息的前后时间点,根据二者的时间差可以做很多卡顿的分析,BlockCanary就是用此方法实现卡顿检测,具体参看Android卡顿检测工具(一)BlockCanary

第二种方式是Choreographer开放API,上层可设置FrameCallback监听,从而获得每一帧绘制完毕的onFrame回调。常用的帧率监测工具(FPS)就是通过分析两帧之前的时间差完成FPS的计算,比如TinyDancerTakt

实际上Matrix早期版本用的是第二种方式,最新版使用了第一种方式,因为可以拿到更完整更清晰的堆栈信息。

至此,我们可以推断Tracer具有感知帧率变化、统计卡顿的能力,所以跟帧率、函数耗时统计相关的Tracer(FrameTracer/EvilMethodTracer/AnrTracer)一定会继续复写doFrame方法,以实现具体功能。

2. ITracer

它是一个接口,继承了IAppForeground接口,总体算下来一共四个抽象方法:onStartTrace、onCloseTrace、isAlive、onForeground。前三个方法是在描述Tracer自身的生命周期,由TracePlugin统一管理。当Activity前后台状态发生变化时回调Tracer的onForeground方法,因此Tracer具有感知Activity前后台状态变化的能力,它可用来做启动分析。

在Tracer中大部分接口方法都是空实现,具体实现交由有需求的tracer完成。下面我们来看TraceCanary包含的具体tracer实现。

Trace Canary 结构.png

FrameTracer

我们先来看FrameTracer,它复写doFrame监听每一帧的回调,并将时间戳、掉帧情况、页面名称等信息发送给IDoFrameListener。

# -> FrameTracer -> doFrame
@Override
public void doFrame(final long lastFrameNanos, final long frameNanos) {
    if (!isDrawing) {
        return;
    }
    isDrawing = false;
    final int droppedCount = (int) ((frameNanos - lastFrameNanos) / REFRESH_RATE_MS);
    for (final IDoFrameListener listener : mDoFrameListenerList) {
        //同步发送
        listener.doFrameSync(lastFrameNanos, frameNanos, getScene(), droppedCount);
        if (null != listener.getHandler()) {
            //异步发送
            listener.getHandler().post(new AsyncDoFrameTask(listener,
                    lastFrameNanos, frameNanos, getScene(), droppedCount));
        }
    }
}

可以看到代码中分别以同步和异步的方式将回调发送出去,上层可通过FrameTracer的register方法注册监听。

# FrameTracer
public void register(IDoFrameListener listener) {
    if (FrameBeat.getInstance().isPause()) {
        FrameBeat.getInstance().resume();
    }
    if (!mDoFrameListenerList.contains(listener)) {
        mDoFrameListenerList.add(listener);
    }
}

public void unregister(IDoFrameListener listener) {
    mDoFrameListenerList.remove(listener);
    if (!FrameBeat.getInstance().isPause() && mDoFrameListenerList.isEmpty()) {
        FrameBeat.getInstance().removeListener(this);
    }
}

EvilMethodTracer

它具有检查耗时函数的功能,而ANR就是最严重的耗时情况,那我们先来看看ANR检查是如何做到的。

ANR检查

先来看构造器

public EvilMethodTracer(TracePlugin plugin, TraceConfig config) {
    super(plugin);
    this.mTraceConfig = config;
    //创建ANR延时检测工具 定时5s
    mLazyScheduler = new LazyScheduler(MatrixHandlerThread.getDefaultHandlerThread(), Constants.DEFAULT_ANR);
    mActivityCreatedInfoMap = new HashMap<>();
}

LazyScheduler是一个延时任务工具类,构造时需设定HandlerThread和delay。

LazyScheduler类图.png

内部ILazyTask接口定义了延时任务执行时的回调方法onTimeExpire。setUp方法开始埋炸弹(ANR和耗时方法),cancel方法解除炸弹。也就是说调用setUp方法后5秒内如果没有执行cancel,就会触发onTimeExpire方法。

上面的内容理解之后,我们来看doFrame方法。

# -> EvilMethodTracer
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
    if (isIgnoreFrame) {
        mActivityCreatedInfoMap.clear();
        setIgnoreFrame(false);
        getMethodBeat().resetIndex();
        return;
    }

    int index = getMethodBeat().getCurIndex();
    //两帧时间差大于卡顿阈值(默认一秒)则发出buffer信息
    //若满足一系列校验工作则触发卡顿检测
    if (hasEntered && frameNanos - lastFrameNanos > mTraceConfig.getEvilThresholdNano()) {
        MatrixLog.e(TAG, "[doFrame] dropped frame too much! lastIndex:%s index:%s", 0, index);
        handleBuffer(Type.NORMAL, 0, index - 1, getMethodBeat().getBuffer(), (frameNanos - lastFrameNanos) / Constants.TIME_MILLIS_TO_NANO);
    }
    getMethodBeat().resetIndex();
    mLazyScheduler.cancel();
    //埋ANR炸弹
    mLazyScheduler.setUp(this, false);
}

如果5秒内还没执行下一次doFrame,就会回调到EvilMethodTracer的onTimeExpire方法。

# -> EvilMethodTracer
@Override
public void onTimeExpire() {
    // maybe ANR
    if (isBackground()) {
        MatrixLog.w(TAG, "[onTimeExpire] pass this time, on Background!");
        return;
    }
    long happenedAnrTime = getMethodBeat().getCurrentDiffTime();
    MatrixLog.w(TAG, "[onTimeExpire] maybe ANR!");
    setIgnoreFrame(true);
    getMethodBeat().lockBuffer(false);
    //处于前台就会发送ANR消息
    handleBuffer(Type.ANR, 0, getMethodBeat().getCurIndex() - 1, getMethodBeat().getBuffer(), null, Constants.DEFAULT_ANR, happenedAnrTime, -1);
}

对于普通耗时函数又是如何检测的呢?EvilMethodTracer的工作流程是这样的:

  1. 首先要记录各个函数的执行时间,这里需要在每个函数的入口和出口做插桩工作,最终写入MethodBeat 中的成员变量sBuffer,它的类型为long型数组,通过不同位描述了函数id和函数的耗时。之所以用一个long型值记录耗时结果是为了压缩数据、节省内存,官方数据是预先分配记录数据的buffer长度为100w内存占用约7.6M。
    buffer结构.png
  2. doFrame检查两帧之间的时间差,如果大于卡顿阈值(默认为1s),则会调用handleBuffer触发统计排查任务。
  3. handlerBuffer中启动AnalyseTask任务分析过滤method调用stack、函数耗时等,并保存在jsonObject中。
  4. 调用sendReport将jsonObject转为Issue对象发送事件给PluginListener。

函数插桩

MethodTracer的内部类TraceMethodAdapter负责为每个方法执行前插入MethodBeat的i方法,方法执行后插入o方法。插桩使用的是ASM实现的,ASM是一种常用的操作字节码的动态化技术,可以用做无侵入的埋点统计。EvilMethodTracer也是用它做耗时函数的分析。

# -> MethodTracer.TraceMethodAdapter
@Override
protected void onMethodEnter() {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if (traceMethod != null) {
        traceMethodCount.incrementAndGet();
        mv.visitLdcInsn(traceMethod.id);
        //入口插桩
        mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
    }
}

@Override
protected void onMethodExit(int opcode) {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if (traceMethod != null) {
        if (hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap)
                && mCollectedMethodMap.containsKey(traceMethod.getMethodName())) {
            TraceMethod windowFocusChangeMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
                    TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
            if (windowFocusChangeMethod.equals(traceMethod)) {
                traceWindowFocusChangeMethod(mv);
            }
        }

        traceMethodCount.incrementAndGet();
        mv.visitLdcInsn(traceMethod.id);
        //出口插桩
        mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
    }
}

Matrix通过代理编译期间的任务 transformClassesWithDexTask,将全局 class 文件作为输入,利用 ASM 工具,高效地对所有 class 文件进行扫描及插桩。为了尽可能的降低性能损耗扫描过程会过滤掉一些默认或匿名的构造函数以及get/set等简单而不耗时的函数。

为了方便及高效记录函数执行过程,Matrix插件为每个插桩的函数分配一个独立 ID,在插桩过程中,记录插桩的函数签名及分配的 ID,在插桩完成后输出一份 methodmap文件,作为数据上报后的解析支持,该文件在apk构建时生成,目录位于build/matrix_output下,名为Debug_methodmap(debug构建),而那些被过滤掉的方法被记录在Debug_ignoremethodmap文件中。文件生成规则在MethodCollector类中,感兴趣的小伙伴可以继续研究。

那接下来我们来看一下生成文件的内容。

methodmap.png

文件每一行代表一个插桩方法。 以第一行为例:

-1,1,sample.tencent.matrix.io.TestIOActivity onWindowFocusChanged (Z)V
  • -1 第一个数字表示分配方法的Id,-1表示插桩为activity加入的onWindowFocusChanged方法。其他方法从1开始计数。
  • 1 表示方法权限修饰符,常见的值为ACC_PUBLIC = 1; ACC_PRIVATE = 2;ACC_PROTECTED = 4; ACC_STATIC = 8等等。1即表示public方法。
  • 类名 sample.tencent.matrix.io.TestIOActivity
  • 方法名 onWindowFocusChanged
  • 参数及返回值类型Z表示参数为boolean类型,V表示返回值为空。

接下来我们来看一下实践是什么效果,我们模拟了一个耗时函数,当点击按钮时调用。

//点击按钮触发 为放大耗时,循环执行200次
public void testJank(View view) {
    for (int i = 0; i < 200; i++) {
        wrapper();
    }
}

//包装方法用于测试调用深度
void wrapper() {
    tryHeavyMethod();
}

//dump内存是耗时方法
private void tryHeavyMethod() {
    Debug.getMemoryInfo(new Debug.MemoryInfo());
}

运行后得到以下Issue:

evil_method_trace.png

我们重点关心的是

  1. cost bad函数表示总耗时。
  2. stack bad函数调用栈。
  3. stackKey bad函数入口方法Id

例子中stack(0,28,1,1988\n 1,31,1,136)如何解读呢?四个数为一组每组用换行符分隔,其中一组四个数分别表示为:

  • 0 方法调用深度,比如a调用b,b调用c,则a,b,c的调用深度分别为0,1,2。
  • 28 methodId,与上述生成的methodmap文件中第一列对应。
  • 1 调用次数
  • 1998 函数总耗时,包含子函数的调用耗时。

我们通过反查methodmap函数可验证结果。

函数记录.png

实测发现stack存在bug,我们的代码中最终的耗时方法是tryHeavyMethod,只不过中间包了一层wrapper方法,stack就不能识别到了。这一点Matrix官方可能会后续修复吧。

stackKey就是耗时函数的入口。本例中testJank调用wrapper,wrapper调用tryHeavyMethod,统计stackKey时以深度为0的函数为准,28就对应testJank方法。

FPSTracer

同其他类似的fps检测工具原理一样,监听Choreographer.FrameCallback回调,回调方法doFrame在每次Vsync信号即将来临时被调用,上层监听此回调接口并计算两次回调之前的时间差,Android系统默认的刷新频率是16.6ms一次,时间差除以刷新频率即为掉帧情况。

FPSTracer不同的点在于其内部能统计一段时间的平均帧率,并定义了帧率好坏的梯度。

# -> FPSTracer.DropStatus
private enum DropStatus {
    DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0);
    int index;

    DropStatus(int index) {
        this.index = index;
    }
}
  • DROPPED_FROZEN 掉42帧及以上(70%掉帧)
  • DEFAULT_DROPPED_HIGH 掉24帧以上42帧以下(40%掉帧)
  • DEFAULT_DROPPED_MIDDLE 掉9帧以上24帧以下(15%掉帧)
  • DEFAULT_DROPPED_NORMAL 掉3帧以上9帧以下(5%掉帧)
  • DROPPED_BEST 掉3帧以内

核心方法代码片段

# FPSTracer -> doReport
private void doReport() {
    LinkedList<Integer> reportList;
    synchronized (this.getClass()) {
        if (mFrameDataList.isEmpty()) {
            return;
        }
        reportList = mFrameDataList;
        mFrameDataList = new LinkedList<>();
    }

    //数据转储到mPendingReportSet集合中
    for (int trueId : reportList) {
        int scene = trueId >> 22;
        int durTime = trueId & 0x3FFFFF;
        LinkedList<Integer> list = mPendingReportSet.get(scene);
        if (null == list) {
            list = new LinkedList<>();
            mPendingReportSet.put(scene, list);
        }
        list.add(durTime);
    }
    reportList.clear();

    //统计分析
    for (int i = 0; i < mPendingReportSet.size(); i++) {
        int key = mPendingReportSet.keyAt(i);
        LinkedList<Integer> list = mPendingReportSet.get(key);
        if (null == list) {
            continue;
        }
        int sumTime = 0;
        int markIndex = 0;
        int count = 0;

        int[] dropLevel = new int[DropStatus.values().length]; // record the level of frames dropped each time
        int[] dropSum = new int[DropStatus.values().length]; // record the sum of frames dropped each time
        int refreshRate = (int) Constants.DEFAULT_DEVICE_REFRESH_RATE * OFFSET_TO_MS;
        for (Integer period : list) {
            sumTime += period;
            count++;
            int tmp = period / refreshRate - 1;
            //将掉帧情况写入数组
            if (tmp >= Constants.DEFAULT_DROPPED_FROZEN) {
                dropLevel[DropStatus.DROPPED_FROZEN.index]++;
                dropSum[DropStatus.DROPPED_FROZEN.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_HIGH) {
                dropLevel[DropStatus.DROPPED_HIGH.index]++;
                dropSum[DropStatus.DROPPED_HIGH.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_MIDDLE) {
                dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
                dropSum[DropStatus.DROPPED_MIDDLE.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_NORMAL) {
                dropLevel[DropStatus.DROPPED_NORMAL.index]++;
                dropSum[DropStatus.DROPPED_NORMAL.index] += tmp;
            } else {
                dropLevel[DropStatus.DROPPED_BEST.index]++;
                dropSum[DropStatus.DROPPED_BEST.index] += (tmp < 0 ? 0 : tmp);
            }
            //达到分片时间 sendReport一次
            if (sumTime >= mTraceConfig.getTimeSliceMs() * OFFSET_TO_MS) { // if it reaches report time
                float fps = Math.min(60.f, 1000.f * OFFSET_TO_MS * (count - markIndex) / sumTime);
                MatrixLog.i(TAG, "scene:%s fps:%s sumTime:%s [%s:%s]", mSceneIdToSceneMap.get(key), fps, sumTime, count, markIndex);
                try {
                    JSONObject dropLevelObject = new JSONObject();
                    ...

                    JSONObject dropSumObject = new JSONObject();
                    ...

                    JSONObject resultObject = new JSONObject();
                    resultObject = DeviceUtil.getDeviceInfo(resultObject, getPlugin().getApplication());

                    resultObject.put(SharePluginInfo.ISSUE_SCENE, mSceneIdToSceneMap.get(key));
                    resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);
                    resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);
                    resultObject.put(SharePluginInfo.ISSUE_FPS, fps);
                    sendReport(resultObject);
                } catch (JSONException e) {
                    MatrixLog.e(TAG, "json error", e);
                }


                dropLevel = new int[DropStatus.values().length];
                dropSum = new int[DropStatus.values().length];
                markIndex = count;
                sumTime = 0;
            }
        }

        // delete has reported data
        if (markIndex > 0) {
            for (int index = 0; index < markIndex; index++) {
                list.removeFirst();
            }
        }
        ...
    }
}

整个流程如下

  1. FPSTracer中定义类型为LinkedList的成员变量mFrameDataList,用于记录时间差和scene(activity或fragment名)信息。
  2. 计算两次两次doFrame时间差,记录在一个int数中。其中高10位表示sceneId,低22位表示耗时ms*OFFSET_TO_MS(默认为100)。
    frame数据存储.png
  3. 以两分钟(getFPSReportInterval默认值,官方sample为10秒)为一个周期统计frame信息,计时结束后触发onTimeExpire回调方法。
  4. onTimeExpire调用doReport做统计分析。
  5. 同一个场景下累计frame耗时超过分片时间(getTimeSliceMs默认为6秒,官方sample为1秒)则触发一次sendReport将统计到的各个级别的掉帧数和掉帧时间发送出去。

这里有一个细节问题需要处理,比如页面没有静止没有UI绘制任务,这段时间的帧率统计也没意义。事实上,FPSTracer对上述用于存储每帧耗时信息的mFrameDataList的插入做个一个过滤。

# FPSTracer -> doFrame
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
    //满足判断条件才handleDoFrame
    if (!isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())) {
        handleDoFrame(lastFrameNanos, frameNanos, getScene());
    }
    isDrawing = false;
}

private void handleDoFrame(long lastFrameNanos, long frameNanos, String scene) {
    int sceneId;
    ... //获取scene信息
    int trueId = 0x0;
    //位运算,将sceneId和耗时信息写入一个int
    trueId |= sceneId;
    trueId = trueId << 22;
    long offset = frameNanos - lastFrameNanos;
    trueId |= ((offset / FACTOR) & 0x3FFFFF);
    if (offset >= 5 * 1000000000L) {
        MatrixLog.w(TAG, "[handleDoFrame] WARNING drop frame! offset:%s scene%s", offset, scene);
    }
    //添加到mFrameDataList
    synchronized (this.getClass()) {
        mFrameDataList.add(trueId);
    }
}

看条件!isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())

  1. isInvalid 表示是否非法,当activity resume后为false,pause后为true。也即只统计resume阶段,因为activity真正绘制是从onResume开始。
  2. isDrawing 表示是否处理draw状态,FPSTracer在onActivityResume时为DecorView添加了draw listener(getDecorView().getViewTreeObserver().addOnDrawListener())监听view的绘制,当回调onDraw时将此变量设为true,onFrame结束设置为false。因此处于静止状态的时间段不会统计帧信息。
  3. isEnterAnimationComplete 入场动画执行完。
  4. isTargetScene FPSTrace可配置监控界面白名单,默认全部监控。

这样真个fps检测流程也就结束了,我们来看一下官方sample汇总的report展现。

fps_tracer_issue.png

StartUpTrace 应用启动统计

首先要明确的是统计的是应用的启动,这包括application创建过程而不单纯是activity启动。统计触发一次就会销毁,因此如果想统计activity之间跳转的情况需手动获取StartUpTrace并调用onCreate方法。

具体的统计指标如下:

统计项目 含义
appCreateTime application创建时长
betweenCost application创建完成到第一个Activity create完成
activityCreate activity 执行完super.oncreate()至window获取焦点
splashCost splash界面创建时长
allCost 到主界面window focused总时长
isWarnStartUp 是否为热启动(application存在)

时间轴大致是这样的:

startup时间轴.png

为了实现上述统计指标需要hook ActivityThread中消息处理内部类H(成员变量mH),它是一个Handler对象,activity的创建与生命周期的处理都是通过它完成的,如果你熟悉activity的启动流程那么对mH成员变量一定不陌生。ApplicationThread作为binder通信的信使,接收AMS的调度事件,比如scheduleLaunchActivity,此方法内部会通过mH对象发送 H.LAUNCH_ACTIVITY消息,mH接收到此消息便会调用handleLaunchActivity创建activity对象。

这属于Activity启动流程范畴,本篇不再讨论。重点关注hook动作。

hook系统handler mH

# -> StartUpHacker
public class StartUpHacker {
    private static final String TAG = "Matrix.Hacker";
    public static boolean isEnterAnimationComplete = false;
    public static long sApplicationCreateBeginTime = 0L;
    public static int sApplicationCreateBeginMethodIndex = 0;
    public static long sApplicationCreateEndTime = 0L;
    public static int sApplicationCreateEndMethodIndex = 0;
    public static int sApplicationCreateScene = -100;

    //此方法被静态代码块调用 在被类resolve时执行
    public static void hackSysHandlerCallback() {
        try {
            sApplicationCreateBeginTime = System.currentTimeMillis();
            sApplicationCreateBeginMethodIndex = MethodBeat.getCurIndex();
            Class<?> forName = Class.forName("android.app.ActivityThread");
            Field field = forName.getDeclaredField("sCurrentActivityThread");
            field.setAccessible(true);
            Object activityThreadValue = field.get(forName);
            Field mH = forName.getDeclaredField("mH");
            mH.setAccessible(true);
            Object handler = mH.get(activityThreadValue);
            Class<?> handlerClass = handler.getClass().getSuperclass();
            Field callbackField = handlerClass.getDeclaredField("mCallback");
            callbackField.setAccessible(true);
            Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
            HackCallback callback = new HackCallback(originalCallback);
            callbackField.set(handler, callback);
            MatrixLog.i(TAG, "hook system handler completed. start:%s", sApplicationCreateBeginTime);
        } catch (Exception e) {
            MatrixLog.e(TAG, "hook system handler err! %s", e.getCause().toString());
        }
    }
}

代码比较简单,就是取出mH对象内部原有的Handler.Callback,将它换成成新的HackCallback。

# StartUpHacker.HackCallback
private final static class HackCallback implements Handler.Callback {
   private final Handler.Callback mOriginalCallback;

    HackCallback(Handler.Callback callback) {
        this.mOriginalCallback = callback;
    }

    @Override
    public boolean handleMessage(Message msg) {
        ...
        //优先处理 设置一些值
        boolean isLaunchActivity = isLaunchActivity(msg);
        if (isLaunchActivity) {
            StartUpHacker.isEnterAnimationComplete = false;
        } else if (msg.what == ENTER_ANIMATION_COMPLETE) {
            //记录activity转场动画结束标志
            StartUpHacker.isEnterAnimationComplete = true;
        }
        if (!isCreated) {
            if (isLaunchActivity || msg.what == CREATE_SERVICE || msg.what == RECEIVER) {
                //以第一个Activity LAUNCH_ACTIVITY消息为止,记录application创建结束时间
                StartUpHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
                StartUpHacker.sApplicationCreateEndMethodIndex = MethodBeat.getCurIndex();
                StartUpHacker.sApplicationCreateScene = msg.what;
                isCreated = true;
            }
        }
        if (null == mOriginalCallback) {
            return false;
        }
        //最终让原有的callback处理消息
        return mOriginalCallback.handleMessage(msg);
    }
}

了解了hook原理,我们来看一下统计时间的几个关键节点是如何获得的。

  1. 程序启动 实际上是MethodBeat类的一段静态代码块,我们知道静态代码块在解析类的时候就执行了,拿它作为程序计时的起点也算正常。
  2. 系统LAUNCH_ACTIVITY消息发出 通过hook mH类完成。
  3. 收到onActivityCreated回调 通过为aplication注册registerActivityLifecycleCallbacks来感知应用内activity生命周期。
  4. Activity对应window获取焦点 通过ASM动态复写activity的onWindowFocusChanged方法。

写到这,整个Trace Canary的内容就算大致讲完了,其中涉及的知识点非常多,包括UI绘制流程、Activity启动流程、应用启动流程、打包流程、ASM插桩等等。笔者只是按源码流程大致理出了最核心的内容,分支的技术点大多一笔略过,需要读者自行补充,希望大家一起加油,补足分支的技术栈。