微信自研APM利器Matrix 卡顿分析工具之(二)TraceCanary

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

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

Matrix内容概览

Matrix.png

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

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

Matrix初始化流程

Matrix.Builder内部类配置Plugins。

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

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

//可选 感知插件状态变化
builder.patchListener(...);

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

Plugin结构

plugin类图.png

目前配置的plugin

  • TracePlugin
  • ResourcePlugin
  • IOCanaryPlugin
  • SQLiteLintPlugin

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

private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) {
    this.application = app;
    this.pluginListener = listener;
    this.plugins = plugins;
    for (Plugin plugin : plugins) {
        plugin.init(application, pluginListener);
        pluginListener.onInit(plugin);
    }
}

内部遍历所有插件,并调用其init方法进行初始化。之后通知pluginListener生命周期。
上层可自定义pluginListener感知plugin生命周期。

# -> 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;
}

plugin包含的生命周期:

# -> PluginListener
public interface PluginListener {
    void onInit(Plugin plugin);

    void onStart(Plugin plugin);

    void onStop(Plugin plugin);

    void onDestroy(Plugin plugin);

    void onReportIssue(Issue issue);
}

Matrix结构

Matrix类图.png

可以看到Matrix提供了日志管理器MatrixLogImpl,以及控制其内部所有plugin的开关方法startAllPlugins/stopAllPlugins。

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

TracePlugin

其内部定义了一些跟踪器

  • FPSTracer 帧率监测
  • EvilMethodTracer 耗时函数监测
  • FrameTracer 逐帧监测
  • StartUpTracer 启动耗时

来看一下类图:

tracer类图.png

这些跟踪器都继承于BaseTracer,BaseTracer为抽象类,唯一的抽象方法是getTag方法。子类实现仅仅定义一个名称即可。

再来看看BaseTracer实现的接口

  1. ApplicationLifeObserver.IObserver
    当activity前后台切换或者生命周期发生变化时会回调接口方法。至于是如何监控的,逻辑都在ApplicationLifeObserver中,这个我们稍后分析。因此BaseTracer具有感知activity生命周期及应用前后台状态变化的能力

  2. IFrameBeatListener
    当绘制完毕每一帧会回调onFrame方法,当activity处于后台或被销毁会回调cancelFrame方法。
    因此BaseTracer具有感知帧率变化、统计卡顿的能力,所以跟帧率、函数统计相关的Tracer(FPSTracer/FrameTracer/EvilMethodTracer)都复写了此方法。

  3. IMethodBeatListener
    接口方法主要有pushFullBuffer和onActivityEntered,先看pushFullBuffer方法,统计函数耗时是通过插桩完成的,matrix会记录每个方法执行的时间,并写入一个long型数组,当数组容量满后会发一次pushFullBuffer回调,收到回调后可统计函数耗时情况。再看onActivityEntered方法,每个activity启动后会对调此方法,因此可用于统计activity启动时间。因此BaseTracer具有统计函数耗时和Activity启动耗时的能力,而在tracer体系内EvilMethodTracer是用于侦查耗时函数(邪恶函数),StartUpTracer用于统计Activity启动时间,所以二者一定会复写这两个方法。

在BaseTracer的onCreate方法中完成了对上述接口的监听。

# -> BaseTracer
public void onCreate() {
    if (isEnableMethodBeat()) {
        if (!getMethodBeat().isHasListeners()) {
            getMethodBeat().onCreate();
        }
        //监听IMethodBeatListener
        getMethodBeat().registerListener(this);
    }
    //监听ApplicationLifeObserver.IObserver
    ApplicationLifeObserver.getInstance().register(this);
    //监听IFrameBeatListener
    FrameBeat.getInstance().addListener(this);
    isCreated = true;
}

对应的在onDestroy方法中取消了这些监听。

# -> BaseTracer
public void onDestroy() {
    if (isEnableMethodBeat()) {
        getMethodBeat().unregisterListener(this);
        if (!getMethodBeat().isHasListeners()) {
            getMethodBeat().onDestroy();
        }
    }
    ApplicationLifeObserver.getInstance().unregister(this);
    FrameBeat.getInstance().removeListener(this);
    isCreated = false;
}

在BaseTracer中大部分接口方法都是空实现,具体实现交由有需求的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<Integer>的成员变量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插桩等等。笔者只是按源码流程大致理出了最核心的内容,分支的技术点大多一笔略过,需要读者自行补充,希望大家一起加油,补足分支的技术栈。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容