Android 卡顿方案研究

2020-11-20  本文已影响0人  DoneWillianm

Android 卡顿研究

[TOC]

稳定化,不是说说而已

基础概念

这里主要是根据张绍文老师的文章做的笔记,根据张绍文老师的文笔去实践具体卡顿监控的内容

散列知识点

CPU

这里CPU需要单独搞出来提一下,卡顿优化前需要搞清楚CPU是什么,能干什么,正在干什么,然后才是“什么”这个区间里面应用程序此时的参数是否合理,优化的空间又是多少

查看一个CPU的参数需要看CPU的频率,核心等参数,具体参考 Wiki

这里就仅仅点相对重要的一些参数含义

方法论

指标

adb查看CPU.png

如果 CPU 使用率长期大于 60% ,表示系统处于繁忙状态,就需要进一步分析用户时间和系统时间的比例。对于普通应用程序,系统时间不会长期高于 30%,如果超过这个值,就得考虑是否I/O调用过多或者锁调用的过于频繁的问题。利用Android Studio的profile也能查看CPU的使用率

CPU 饱和度反映的是线程排队等待 CPU 的情况,也就是 CPU 的负载情况。

CPU 饱和度首先会跟应用的线程数有关,如果启动的线程过多,易导致系统不断地切换执行的线程,把大量的时间浪费在上下文切换,要知道每一次 CPU 上下文切换都需要刷新寄存器和计数器,至少需要几十纳秒的时间。

可以通过vmstat命令查看CPU上下文切换次数

proc/self/sched:
    nr_voluntary_switches:主动上下文切换次数,因为线程无法获取资源导致上下文切换,最普遍的就是IO
    nr_involuntary_switches:被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占CPU
    se.statistics.iowait_count:IO 等待次数
    se.statistics.iowait_sum:IO 等待时间

此外也可以通过 uptime 命令可以检查 CPU 在 1 分钟、5 分钟和 15 分钟内的平均负载。比如一个 4 核的 CPU,如果当前平均负载是 8,这意味着每个 CPU 上有一个线程在运行,还有一个线程在等待。一般平均负载建议控制在“0.7 × 核数”以内。

00:02:39 up 7 days, 46 min,  0 users,  
load average: 13.91, 14.70, 14.32

另外一个会影响 CPU 饱和度的是线程优先级,线程优先级会影响 Android 系统的调度策略,它主要由 nice 和 cgroup 类型共同决定。nice 值越低,抢占 CPU 时间片的能力越强。当 CPU 空闲时,线程的优先级对执行效率的影响并不会特别明显,但在 CPU 繁忙的时候,线程调度会对执行效率有非常大的影响。

关于线程优先级,你需要注意是否存在高优先级的线程空等低优先级线程,例如主线程等待某个后台线程的锁。从应用程序的角度来看,无论是用户时间、系统时间,还是等待 CPU 的调度,都是程序运行花费的时间。

市场调研

Traceview 和 systrace 都是我们比较熟悉的排查卡顿的工具,从实现上这些工具分为两个流派。

第一个流派是 instrument。获取一段时间内所有函数的调用过程,可以通过分析这段时间内的函数调用流程,再进一步分析待优化的点。

第二个流派是 sample。有选择性或者采用抽样的方式观察某些函数调用过程,可以通过这些有限的信息推测出流程中的可疑点,然后再继续细化分析。

根据流派,对目前市场上的性能监控工具做一些调研和使用,包括但不限于官方提供的性能监控工具,如systrace,Matrix等,关于Android上Systrace的使用可以参考我之前写过的一个blog里面有提到过如何使用Android 性能优化

选择哪种工具,需要看具体的场景。如果需要分析 Native 代码的耗时,可以选择 Simpleperf;如果想分析系统调用,可以选择 systrace;如果想分析整个程序执行流程的耗时,可以选择 Traceview 或者插桩版本的 systrace。

对目前市场上的一些性能监控框架做基本调研,如DoraemonKitMatrixBlockCanary

BlockCanary & DoraemonKit

这里之所以把两个都放到一起,是因为滴滴的哆啦A梦的卡顿检测其实就是blockCanary,实现很简单,但是思路很巧妙~

想要检测卡顿,其实就是检测主线程的运行情况,为什么这么说呢,因为每一帧渲染数据的创建,就依托于主线程来创建,而想要保证每一帧CPU都能在16.7ms内(这里仅限于60帧这种情况,如果是90或者120,可以反推的哈~)完成工作,这样就不会出现丢帧的现象,也就不会造成卡顿,而我们就监测每个而如何监测主线程的运行情况呢?这里需要知道安卓中的handler机制,通过检测每次处理主线程消息的耗时情况,就能够知道是否产生了卡顿,而在发生卡顿的时候,同时抓取此时主线程的堆栈,那么就更能方便的定位到需要优化的代码。

BlockCanary核心的地方,主要分为两个部分:

handleMessage

在主线程Looper每次处理消息的过程中,通过hook主线程Looper每次处理消息的过程,在处理消息之前记录一个时间戳,处理完消息之后记录一个时间戳,那么两个时间的差值,就是处理一条消息所花费的时间。通过给这个时间设置阈值,如:处理时间 > 阈值时间(430ms > 200ms)那么就认为是发生了卡顿

Looper处理消息.png
这里的hook其实非常简单,因为framework给咱们预留了这样的口子,可以看下在handlemessage这里的源码:
public void loop(){
    for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            ......
            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            msg.recycleUnchecked();
        }
}

......

/**
 * Control logging of messages as they are processed by this Looper.  If
 * enabled, a log message will be written to <var>printer</var>
 * at the beginning and ending of each message dispatch, identifying the
 * target Handler and message contents.
 *
 * @param printer A Printer object that will receive log messages, or
 * null to disable message logging.
 */
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

可见们只需要手动调用Looper.setMessageLogging方法就能给线程looper对象设置printer对象,在每次处理消息的时候,通过监听printer的println回调,解析出内容,就能知道知道分发消息的开始和结束了

//Printer 接口
public interface Printer {
    /**
     * Write a line of text to the output.  There is no need to terminate
     * the given string with a newline.
     */
    void println(String x);
}

通过过滤printer.println打印的内容,判断是否是在消息分发处理相关的内容,然后进行时间差的计算,来判断卡顿是否发生

//LooperPrinter
class LooperPrinter implements Printer {
    public Printer origin;
    boolean isHasChecked = false;
    boolean isValid = false;

    LooperPrinter(Printer printer) {
        this.origin = printer;
    }

    @Override
    public void println(String x) {
        if (null != origin) {
            origin.println(x);
            if (origin == this) {
                throw new RuntimeException(MonitorConstants.LOG_TAG + " origin == this");
            }
        }

        if (!isHasChecked) {
            isValid = x.charAt(0) == '>' || x.charAt(0) == '<';
            isHasChecked = true;
            if (!isValid) {
                InsectLogger.ne("[println] Printer is inValid! x:%s", x);
            }
        }

        if (isValid) {
            dispatch(x.charAt(0) == '>', x);
        }

    }
}

那么依据主线程卡顿的监控就已经完成了,接下来是对于卡顿问题的定位,也就是对主线程堆栈的抓取

dumpStack

这里不完全参照blockCanary的实现,但是大家都是为了解决能够抓取到问题发生的堆栈,这里先说一下对于主线程堆栈dump需要关注的问题。不同的抓取策略也是为了解决这个问题,此处先不考虑对性能带来的影响
假设此时发生了卡顿,那么在调用getStackTrace的时候,这时候虚拟机中所跟踪的堆栈中会把当前记录的一些堆栈返回。通过在发生卡顿的时候,dump出当前的堆栈,记录下来,再追溯问题的时候直接看存储下来的堆栈信息,那么定位问题就会方便很多,而实际情况下并不能如此理想,因为从VM中取出的堆栈dalvik.system.VMStack#getThreadStackTrace返回的数据是未知的,不能保证里面到底有多少内容,可能只有一部分,这样就可能会遗漏真正的问题所在,可以参考下图~

函数执行火图?.png
可以看到真正有问题的函数其实是FunctionA-1,而如果捞出来的堆栈只有FunctionA-2或者A-3的话,当然可以优化A-3,但是会漏掉真正发生问题的函数。所以对于堆栈的抓取,基于VMStack抓取堆栈的方式下,笔者思考了两种方案来解决这样的问题,这两种应该也是市面上基于VMStack方式的大概方案,再深入往VM中去研究感觉可以有,但是不推荐,因为成本高,且回报的话不太会有预期中的高。
周期性Dump

通过每个一段时间从VM中获取主线程的堆栈,在发生卡顿的时候,过滤出时间,然后直接取出这段时间内的堆栈来进行问题排查。


周期Dmp.png

在实现的时候需要注意的一些小细节:

起止Dump

这里可以“忽略”多线程的特性,因为我们关注的仅仅是主线程,那么只需要在消息分发之初dump一次堆栈,然后再消息处理之后再dump一次堆栈,这样既能在dump出来的堆栈中发现可能存在的问题,同时又能自行推断这中间的执行过程来观测代码中出现的问题。当然不可缺少一个代码耗时检测的小工具~

起止Dmp.png

Matrix

关于matrix-traceCanary原理

关于matrix解剖,需要先了解定义,再根据具体代码进行分析,最后根据代码梳理出实现的思路

卡顿定义

微信开发者对于卡顿的定义,很简单,很清晰,很明了,这里就cv过来了,一定要仔细读对卡顿的定义

什么是卡顿,很多人能马上联系到的是帧率 FPS (每秒显示帧数)。那么多低的 FPS 才是卡顿呢?又或者低 FPS 真的就是卡顿吗?(以下 FPS 默认指平均帧率)

其实并非如此,举个例子,游戏玩家通常追求更流畅的游戏画面体验一般要达到 60FPS 以上,但我们平时看到的大部分电影或视频 FPS 其实不高,一般只有 25FPS ~ 30FPS,而实际上我们也没有觉得卡顿。 在人眼结构上看,当一组动作在 1 秒内有 12 次变化(即 12FPS),我们会认为这组动作是连贯的;而当大于 60FPS 时,人眼很难区分出来明显的变化,所以 60FPS 也一直作为业界衡量一个界面流畅程度的重要指标。一个稳定在 30FPS 的动画,我们不会认为是卡顿的,但一旦 FPS 很不稳定,人眼往往容易感知到。

FPS 低并不意味着卡顿发生,而卡顿发生 FPS 一定不高。 FPS 可以衡量一个界面的流程性,但往往不能很直观的衡量卡顿的发生,这里有另一个指标(掉帧程度)可以更直观地衡量卡顿。

什么是掉帧(跳帧)? 按照理想帧率 60FPS 这个指标,计算出平均每一帧的准备时间有 1000ms/60 = 16.6667ms,如果一帧的准备时间超出这个值,则认为发生掉帧,超出的时间越长,掉帧程度越严重。假设每帧准备时间约 32ms,每次只掉一帧,那么 1 秒内实际只刷新 30 帧,即平均帧率只有 30FPS,但这时往往不会觉得是卡顿。反而如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到。所以界面的掉帧程度,往往可以更直观的反映出卡顿。

流畅性

综上所述,其实可以明白对于卡顿的定义,衡量流畅性的指标可以简单理解为:

Best Normal Middle High Frozen
[0:3) [3:9) [9:24) [24:42) [42:∞)

Code实现

关于反射 Choreographer 来做到如何监测用户触发后开始计算平均帧率

关于Choreographer的知识相关,这里不做赘述,只根据实现原理来对使用的地方做说明

  1. 用户触发刷新

了解下源码中callbackType的含义

    /**
     * Callback type: Input callback.  Runs first.
     * @hide
     */
    public static final int CALLBACK_INPUT = 0;

    /**
     * Callback type: Animation callback.  Runs before traversals.
     * @hide
     */
    @TestApi
    public static final int CALLBACK_ANIMATION = 1;

    /**
     * Callback type: Traversal callback.  Handles layout and draw.  Runs
     * after all other asynchronous messages have been handled.
     * @hide
     */
    public static final int CALLBACK_TRAVERSAL = 2;

显而易见,CALLBACK_INPUT都已经注释好了,首先run的是这个类型的回调,然后我们平时注册的又是什么样子呢?

/**
 * Posts a frame callback to run on the next frame.
 * <p>
 * The callback runs once then is automatically removed.
 * </p>
 *
 * @param callback The frame callback to run during the next frame.
 *
 * @see #postFrameCallbackDelayed
 * @see #removeFrameCallback
 */
public void postFrameCallback(FrameCallback callback) {
    postFrameCallbackDelayed(callback, 0);
}

/**
 * Posts a frame callback to run on the next frame after the specified delay.
 * <p>
 * The callback runs once then is automatically removed.
 * </p>
 *
 * @param callback The frame callback to run during the next frame.
 * @param delayMillis The delay time in milliseconds.
 *
 * @see #postFrameCallback
 * @see #removeFrameCallback
 */
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    if (callback == null) {
        throw new IllegalArgumentException("callback must not be null");
    }

    postCallbackDelayedInternal(CALLBACK_ANIMATION,
            callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

注册类型的callbackType为ANIMATION的,而ANIMATION的type又是什么时候回调呢?

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
    ......
    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
        //优先执行CALLBACK_INPUT类型链表里面的回调
        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
        AnimationUtils.unlockAnimationClock();
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    ......
}

可见是在执行完优先级最高的输入类型的回调才会回调ANIMATION的(注意这里提供的两个参数,第一个是执行该frame的时间戳,第二个是当前帧号,是native调用,在DisplayEventReceiver事件中收到后维护的一个成员变量,具体实现类也在Choreagrapher中),而显然不能够符合我们的要求,我们是期望在用户有操作的情况下是否发生丢帧情况

Choreographer类结构.png

而如何计算input时机的帧率呢? 势必需要在input类型中添加自己实现的callback,在animation开始的执行的时候,标识为input执行结束

Hook时机.png

好了,原理分析完毕,接下来看一下在Matrix中带佬如何实现的,核心类主要是com.tencent.matrix.trace.core.UIThreadMonitor

初始化中先拿到需要hook的方法,然后模拟顺序进行执行

public void init(TraceConfig config) {
    ......
    //反射同步锁对象和对应doFrame的所有回调数组链表对象
    choreographer = Choreographer.getInstance();
    callbackQueueLock = reflectObject(choreographer, "mLock");
    callbackQueues = reflectObject(choreographer, "mCallbackQueues");

    //先拿到添加对应回调的可执行反射方法
    addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);
    addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);
    addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);
    frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos");
        ......
    this.isInit = true;
    ......
}

通过上面分析可得,doframe的回调执行是顺序执行下来,也就是说一个类型的callback执行结束时间,就是下一个类型的开始时间,那么在addCallback的时机也是如此,最开始要添加的则是input类型回调

public void init(TraceConfig config) {
    ......
    choreographer = Choreographer.getInstance();
    callbackQueueLock = ReflectUtils.reflectObject(choreographer, "mLock", new Object());
    //反射获取回调的数组链表对象,可以理解为单object的hashMap  数组+链表实现
    callbackQueues = ReflectUtils.reflectObject(choreographer, "mCallbackQueues", null);
    if (null != callbackQueues) {
        addInputQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);
        addAnimationQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);
        addTraversalQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);
    }
    //主要是拿vsync回调上来的信号开始绘制的时间戳,可以分析出来丢帧数,源码也是这么干的
    vsyncReceiver = ReflectUtils.reflectObject(choreographer, "mDisplayEventReceiver", null);
    //产生vsync信号的时间戳
    frameIntervalNanos = ReflectUtils.reflectObject(choreographer, "mFrameIntervalNanos", Constants.DEFAULT_FRAME_DURATION);

    LooperMonitor.register(new LooperMonitor.LooperDispatchListener() {
        @Override
        public boolean isValid() {
            return isAlive;
        }

        @Override
        public void dispatchStart() {
            super.dispatchStart();
            UIThreadMonitor.this.dispatchBegin();
        }

        @Override
        public void dispatchEnd() {
            super.dispatchEnd();
            UIThreadMonitor.this.dispatchEnd();
        }

    });
    ......
}

public synchronized void onStart() {
    ......
    if (!isAlive) {
        this.isAlive = true;
        synchronized (this) {
            MatrixLog.i(TAG, "[onStart] callbackExist:%s %s", Arrays.toString(callbackExist), Utils.getStack());
            callbackExist = new boolean[CALLBACK_LAST + 1];
        }
        //为三种callback增加状态维护数组
        queueStatus = new int[CALLBACK_LAST + 1];
        //为三种callback增加耗时数组
        queueCost = new long[CALLBACK_LAST + 1];
        //首次添加input类型callback
        addFrameCallback(CALLBACK_INPUT, this, true);
    }
}

可以看到,在添加input类型的回调时,传的是自己,那么来分析一下接下来的run实现

@Override
public void run() {
    //来自vsync信号开始
    doFrameBegin(token);
    //维护input类型数组们的状态
    doQueueBegin(CALLBACK_INPUT);
    //animation回调注册回调
    addFrameCallback(CALLBACK_ANIMATION, new Runnable() {

        @Override
        public void run() {
            //animation回调,input结束
            doQueueEnd(CALLBACK_INPUT);
            doQueueBegin(CALLBACK_ANIMATION);
        }
    }, true);

    addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {

        @Override
        public void run() {
            //traversal类型回调,animation结束
            doQueueEnd(CALLBACK_ANIMATION);
            doQueueBegin(CALLBACK_TRAVERSAL);
        }
    }, true);
}

可以看到,如此就能得到一个Vsync信号过来的轮回,但是走到这里只能完成一次,matrix如何把每一次串起来的咧?

还记得上面初始化的时候注册looper监听,每次消息的处理开始和结束都会激活一次dispatchStart和dispatchEnd,start这里就不分析了,其实就是往外回调,主要是end

private void dispatchEnd() {
    ......
    //在第一次Vsync开始的时候赋值为true,直接进来
    if (isVsyncFrame) {
        doFrameEnd(token);
        intendedFrameTimeNs = getIntendedFrameTimeNs(startNs);
    }
    ......
    //来自一次vsync信号结束
    this.isVsyncFrame = false;
}

private void doFrameEnd(long token) {
    //下一次input开始,上一次的traversal结束
    doQueueEnd(CALLBACK_TRAVERSAL);
    ......
    //开启下一次input轮回
    addFrameCallback(CALLBACK_INPUT, this, true);
}

可以看到,这里才是真正的结束,一个完整的Choreographer循环~

卡顿策略

Matrix的文档里面已经非常清楚的用文字描述一个卡顿是如何产生的,以及卡顿的定义

FPS 低并不意味着卡顿发生,而卡顿发生 FPS 一定不高。 FPS 可以衡量一个界面的流程性,但往往不能很直观的衡量卡顿的发生,这里有另一个指标(掉帧程度)可以更直观地衡量卡顿。

什么是掉帧(跳帧)? 按照理想帧率 60FPS 这个指标,计算出平均每一帧的准备时间有 1000ms/60 = 16.6667ms,如果一帧的准备时间超出这个值,则认为发生掉帧,超出的时间越长,掉帧程度越严重。假设每帧准备时间约 32ms,每次只掉一帧,那么 1 秒内实际只刷新 30 帧,即平均帧率只有 30FPS,但这时往往不会觉得是卡顿。反而如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到。所以界面的掉帧程度,往往可以更直观的反映出卡顿。

然后分析下关于瞬时平均帧率的代码需要重点关注的就是com.tencent.matrix.trace.tracer.FrameTracer这个类

在每一次doFrame的回调中去分析这个参数

@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    if (isForeground()) {
        notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
    }
}


private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,
                            final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {
    try {
        //计算丢帧,跟源码的计算方式一致,上一个版本局部变量的声明并没有如此直观,这个版本改了,清爽许多
        final long jiter = endNs - intendedFrameTimeNs;
        final int dropFrame = (int) (jiter / frameIntervalNs);
       
        synchronized (listeners) {
            //listeners目前注册进来的就俩,一个内部类FPSCollect,一个用于UI展示的FrameDecorator
            for (final IDoFrameListener listener : listeners) {
                if (config.isDevEnv()) {
                    listener.time = SystemClock.uptimeMillis();
                }
                if (null != listener.getExecutor()) {
                    if (listener.getIntervalFrameReplay() > 0) {
                        //数据收集部分
                        listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                    } else {
                        //卡顿分析部分
                        listener.getExecutor().execute(new Runnable() {
                            @Override
                            public void run() {
                                listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                        intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                            }
                        });
                    }
                } else {
                    listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                            intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                }
            }
        }
    } finally {
    }
}

为什么说丢帧计算和源码一致呢? 这里我们可以和源码对比一下:

final long jiter = endNs - intendedFrameTimeNs;
final int dropFrame = (int) (jiter / frameIntervalNs);

//源码 android.view.Choreographer#doFrame
final long jitterNanos = startNanos - frameTimeNanos;
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
  Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
        + "The application may be doing too much work on its main thread.");
}

这么看上去,瞬间就友好了很多~

从注释里面也能看到,注册的俩监听,一个用于记录,一个用于展示,记录其实就是填充此时此刻的关于FPS的快照,没什么可看的,学习而言,展示的要好一些,因为他需要分析数据,然后展示到UI上,那么接下来就直接看下Matix中的com.tencent.matrix.trace.view.FrameDecorator#doFrameAsync,源码过长,这里就一步一步分析代码是如何体现上面的表和描述

计算出平均每一帧的准备时间有 1000ms/60 = 16.6667ms,如果一帧的准备时间超出这个值,则认为发生掉帧,超出的时间越长,掉帧程度越严重。假设每帧准备时间约 32ms,每次只掉一帧,那么 1 秒内实际只刷新 30 帧,即平均帧率只有 30FPS,但这时往往不会觉得是卡顿。反而如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到

Best Normal Middle High Frozen
[0:3) [3:9) [9:24) [24:42) [42:∞)
/**
 * 流畅指标,佳0,正常1,中等2,严重3,冻帧4
 */
public enum DropStatus {
    DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0);
    public int index;

    DropStatus(int index) {
        this.index = index;
    }
}

在回调回来的函数中,分析流畅指标

卡顿分布数据.png
@Override
public void doFrameAsync(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    super.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
    ......
    if (dropFrame >= Constants.DEFAULT_DROPPED_FROZEN) { //冻帧
        dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_HIGH) { //严重
        dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_MIDDLE) { //中等
        dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_NORMAL) { //正常
        dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;
    } else {
        dropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;
    }

    ......
}

这里的代码非常简单,接下来是分析造成严重卡顿的情况,也就是严重丢帧的时候,也是文章中所分析的内容:

如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到

sumFrameCost += (dropFrame + 1) * frameIntervalMs;
sumFrames += 1;
float duration = sumFrameCost - lastCost[0];
long collectFrame = sumFrames - lastFrames[0];
if (duration >= 200) {
    //更新视图
}

综上,就是Matrix中对页面流畅性分析的核心代码,而对于Matrix中精准命中堆栈则可以取自冻帧或者所谓的duration >= 200这个条件下dump一次主线程的堆栈来获取

慢函数

其实关于上述严重掉帧情况下的抓取堆栈的数量不多,同样避不开上面提到的漏掉其他耗时代码的情况,不过笔者认为这样的情况不会特别多,因为卡顿发生的时候,大概率避免不了正在执行一个耗时操作,那么这个耗时操作的堆栈出现在此刻dump出来的堆栈里面的可能性很大,所以Matrix干脆利落的出了一个慢函数检测,这样感觉就无孔不入了,对所有在主线程上运行的函数耗时进行收集,和BlockCanary检测卡顿的策略一样,但是堆栈的抓取就要复杂一些,这也是为什么Matrix性能更好的原因,能在灰度下上线的能力。同时也能有对应的聚合策略,这样结合后端的能力方便我们分析代码运行情况,然后做 “狭义” 上的优化,这里推荐一个在解决卡顿时候您可以用到的方法耗时小插件

Matrix中的慢函数和BlockCanary的卡顿堆栈获取时机和检测卡顿或者慢的策略一致,下面可以简单看下,关于Matrix中如何去捞堆栈的~

@Override
public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) {
    super.dispatchEnd(beginNs, cpuBeginMs, endNs, cpuEndMs, token, isVsyncFrame);
    long start = config.isDevEnv() ? System.currentTimeMillis() : 0;
    long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;
    try {
        //查过阈值,和BlockCanary一样com.tencent.matrix.trace.constants.Constants.DEFAULT_EVIL_METHOD_THRESHOLD_MS = 700
        if (dispatchCost >= evilThresholdMs) {
            long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
            long[] queueCosts = new long[3];
            System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3);
            //scene拿的是当前的activity
            String scene = AppMethodBeat.getVisibleScene();
            MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO));
        }
    } finally {
        indexRecord.release();
    }
}

可以看到,慢函数的阈值是700ms,如果超出阈值,则会从AppMethod中拿取相关的堆栈数据,同时记录下当前的页面然后上传一波,那么关键的地方就在于这个

AppMethodBeat.getInstance().copyData(indexRecord)

关于Matrix的堆栈实现主要分为两块

编译期:

通过代理编译期间的任务 transformClassesWithDexTask,将全局 class 文件作为输入,利用 ASM 工具,高效地对所有 class 文件进行扫描及插桩。

插桩过程有几个关键点:

1、选择在该编译任务执行时插桩,是因为 proguard 操作是在该任务之前就完成的,意味着插桩时的 class 文件已经被混淆过的。而选择 proguard 之后去插桩,是因为如果提前插桩会造成部分方法不符合内联规则,没法在 proguard 时进行优化,最终导致程序方法数无法减少,从而引发方法数过大问题。

2、为了减少插桩量及性能损耗,通过遍历 class 方法指令集,判断扫描的函数是否只含有 PUT/READ FIELD 等简单的指令,来过滤一些默认或匿名构造函数,以及 get/set 等简单不耗时函数。

3、针对界面启动耗时,因为要统计从 Activity#onCreate 到 Activity#onWindowFocusChange 间的耗时,所以在插桩过程中需要收集应用内所有 Activity 的实现类,并覆盖 onWindowFocusChange 函数进行打点。

4、为了方便及高效记录函数执行过程,我们为每个插桩的函数分配一个独立 ID,在插桩过程中,记录插桩的函数签名及分配的 ID,在插桩完成后输出一份 mapping,作为数据上报后的解析支持。

插桩流程.png

下面重点介绍一下关于Matrix中对性能考量以及具体的业务插桩代码

选择在该编译任务执行时插桩,是因为 proguard 操作是在该任务之前就完成的,意味着插桩时的 class 文件已经被混淆过的。而选择 proguard 之后去插桩,是因为如果提前插桩会造成部分方法不符合内联规则,没法在 proguard 时进行优化,最终导致程序方法数无法减少,从而引发方法数过大问题。

解释一下什么叫做方法内联,说白了其实就是你写了两个方法,在编译时会将其中一个方法的实现直接放到调用的地方,这样就无需去走一遍调用其他方法,从而达到优化的目的,因为每个方法会生成一个栈帧,然后进行压栈出栈的操作,过程比较反复,这里用代码解释一下编译优化之一,方法内联:

//编译前,source.java
public int doubleNum(int num){
    return num * 2;
}

public void method1(){
    int a = 10;
    int b = doubleNum(2);
    System.out.println(String.format("a:%s, b:%s", a, b));
}

//编译后, source.class
public void method1(){
    int a = 10;
    int b = a * 2;//doubleNum(2);
    System.out.println(String.format("a:%s, b:%s", a, b));
}

可以看到在编译之后就没有了doubleNum这个方法,在method1中调用的时候直接变成了 a * 2 , 这样就无需在运行的过程中去对doubleNum压栈操作

这里有一篇介绍jvm中关于内联的blog

虽然能保证proguard时候优化能正常进行,不过优化程度上来讲,笔者也没有统计过哈,也不晓得从哪里能够看到,因为目前在7.0之后,安装的时候会进行JIT和AOT混合的方式,这个结果应该不是很好看,在JIT选择编译热代码的时候,优化的那部分内联又该如何考虑呢?想想太复杂了。。。大家感兴趣的话可以统计下在打出来的release包中,可以看日志,优化的效果~

那接下里就是Matrix中如何做到在proguard的transform后进行插桩呢,核心工程是matrix-gradle-plugin

入口:

//com.tencent.matrix.plugin.MatrixPlugin#apply

/**
 * <p>Adds a closure to be called immediately after this project has been evaluated. The project is passed to the
 * closure as a parameter. Such a listener gets notified when the build file belonging to this project has been
 * executed. A parent project may for example add such a listener to its child project. Such a listener can further
 * configure those child projects based on the state of the child projects after their build files have been
 * run.</p>
 *
 * @param closure The closure to call.
 */
void afterEvaluate(Closure closure);

@Override
void apply(Project project) {
    ......
    //完成所有transform之后执行
    project.afterEvaluate {
        def android = project.extensions.android
        def configuration = project.matrix
        android.applicationVariants.all { variant ->

            if (configuration.trace.enable) {
                //代码插桩入口
                MatrixTraceTransform.inject(project, configuration.trace, variant.getVariantData().getScope())
            }
            ......
        }
    }
}

在afterEvaluate后传入闭包,开始插桩代码,为什么是这个时机,可以参考上面的源码注释哈~

接下来就是注入代码,因为不依赖于自定义的transformTask,Matrix的实现是通过往transformTask里面去注入执行事件,这里的写法也是个新姿势~

//com.tencent.matrix.trace.transform.MatrixTraceTransform#inject

public static void inject(Project project, MatrixTraceExtension extension, VariantScope variantScope) {
    ......
    try {
        String[] hardTask = getTransformTaskName(extension.getCustomDexTransformName(), variant.getName());
        for (Task task : project.getTasks()) {
            for (String str : hardTask) {
                if (task.getName().equalsIgnoreCase(str) && task instanceof TransformTask) {
                    //如果确实是Transform的task后进来执行反射hook注入matrix的transform任务
                    TransformTask transformTask = (TransformTask) task;
                    Log.i(TAG, "successfully inject task:" + transformTask.getName());
                    Field field = TransformTask.class.getDeclaredField("transform");
                    field.setAccessible(true);
                    //这里就是注入自定义的任务,在编译执行的时候
                    field.set(task, new MatrixTraceTransform(config, transformTask.getTransform()));
                    break;
                }
            }
        }
    } catch (Exception e) {
        Log.e(TAG, e.toString());
    }

}

//源码中可以看到执行transformTask的时候调用这个注入的transform执行处,com.android.build.gradle.internal.pipeline.TransformTask#transform
@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs)
        throws IOException, TransformException, InterruptedException {
    ......
    ThreadRecorder.get().record(ExecutionType.TASK_TRANSFORM,
        new Recorder.Block<Void>() {
            @Override
            public Void call() throws Exception {
                //这里就是调用transform.transform的时机
                transform.transform(new TransformInvocationBuilder(TransformTask.this)
                        .addInputs(consumedInputs.getValue())
                        .addReferencedInputs(referencedInputs.getValue())
                        .addSecondaryInputs(changedSecondaryInputs.getValue())
                        .addOutputProvider(outputStream != null
                                ? outputStream.asOutput()
                                : null)
                        .setIncrementalMode(isIncremental.getValue())
                        .build());
                return null;
            }
        },
        new Recorder.Property("project", getProject().getName()),
        new Recorder.Property("transform", transform.getName()),
        new Recorder.Property("incremental", Boolean.toString(transform.isIncremental())));
}

[手动表情秒啊~],简直了。。。 不愧是微信的带佬,如果不是对编译任务的task有一定了解的话,要做到这里感觉不太可能。。。 不过还好能站在巨人的肩膀上~

这里注意下关于MatrixTraceTransform的构造,这里也是一个优秀的细节处理,虽然hook了系统transform的task,但是不会扔弃系统的transformTask,而是会传递进来,接下来在代码中分析这个伪代理的使用~

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation);
    long start = System.currentTimeMillis();
    try {
        //在执行系统的transform之前,执行自己的transform
        doTransform(transformInvocation); // hack
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    long cost = System.currentTimeMillis() - start;
    long begin = System.currentTimeMillis();
    //ok 接下来,执行系统原来内置的orignTransformTask, 优秀的细节,不过貌似也只能这么干,可以考虑到卡顿hook looer中printer的时候也可以这么干~
    origTransform.transform(transformInvocation);
}

最后就是插桩的核心代码了,这里分为两个部分来进行介绍,一个是如何过滤简单方法,一个是插桩细节

matrix编译工作.png

总的来说,流程也比较简单,整个方法分为三个过程

首先解析mapping文件,毕竟是对混淆过的代码插桩,这里要能知道自己到底插的哪个方法

然后是收集要插桩的方法,就是过滤,过滤黑名单中不需要插桩的类或者方法

对收集后的方法开始插桩

精简一下代码:

//com.tencent.matrix.trace.transform.MatrixTraceTransform#doTransform
private void doTransform(TransformInvocation transformInvocation) throws ExecutionException, InterruptedException {
    final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental();

    /**
     * step 1 把编译后的mapping文件对应的混淆方法关系记录下来,如:a() -> onCreate()
     */
    List<Future> futures = new LinkedList<>();
    //...干掉乱八七糟的代码干掉哈,主要就是生成方法id,然后解析~
    //拿到需要查找的类文件
    for (TransformInput input : inputs) {

        for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
            futures.add(executor.submit(new CollectDirectoryInputTask(dirInputOutMap, directoryInput, isIncremental)));
        }

        for (JarInput inputJar : input.getJarInputs()) {
            futures.add(executor.submit(new CollectJarInputTask(inputJar, isIncremental, jarInputOutMap, dirInputOutMap)));
        }
    }
    //这里调用get方法就是执行
    for (Future future : futures) {
        future.get();
    }
    futures.clear();

    /**
     * step 2 这里就是收集下来需要进行插桩的方法,过滤出来黑名单或者是简单方法,这些方法不需要插桩~  这里这个判断再一次[手动秒啊~]
     */
    MethodCollector methodCollector = new MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap);
    methodCollector.collect(dirInputOutMap.keySet(), jarInputOutMap.keySet());

    /**
     * step 3 对收集的方法进行插桩~
     */
    MethodTracer methodTracer = new MethodTracer(executor, mappingCollector, config, methodCollector.getCollectedMethodMap(), methodCollector.getCollectedClassExtendMap());
    methodTracer.trace(dirInputOutMap, jarInputOutMap);

}

mapping的解析就是工作量问题,我们可以看下输入结果:

1,1,sample.tencent.matrix.listener.TestPluginListener <init> (Landroid.content.Context;)V
2,0,sample.tencent.matrix.trace.TestFpsActivity$4 <init> (Lsample.tencent.matrix.trace.TestFpsActivity;Landroid.content.Context;I[Ljava.lang.Object;)V
3,1,sample.tencent.matrix.trace.TestTraceFragmentActivity <init> ()V
4,1,sample.tencent.matrix.trace.TestTraceFragmentActivity$2 onClick (Landroid.view.View;)V
5,1,sample.tencent.matrix.listener.TestPluginListener onReportIssue (Lcom.tencent.matrix.report.Issue;)V

然后分析函数是否简单的地方,先看一下文档中的定义

为了减少插桩量及性能损耗,通过遍历 class 方法指令集,判断扫描的函数是否只含有 PUT/READ FIELD 等简单的指令,来过滤一些默认或匿名构造函数,以及 get/set 等简单不耗时函数。

简单方法过滤

内部获取方法同样利用ASM那一套逻辑,上面介绍耗时插件已经介绍过了哈~

可以重点关注核心代码:

//com.tencent.matrix.trace.MethodCollector.TraceClassAdapter#visitMethod
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
                                 String signature, String[] exceptions) {
    //抽象类不做处理
    if (isABSClass) {
        return super.visitMethod(access, name, desc, signature, exceptions);
    } else {
        //判断该方法是不是onWindowFocus
        if (!hasWindowFocusMethod) {
            hasWindowFocusMethod = isWindowFocusChangeMethod(name, desc);
        }
        //真正核心处理过滤逻辑
        return new CollectMethodNode(className, access, name, desc, signature, exceptions);
    }
}

//com.tencent.matrix.trace.MethodCollector.CollectMethodNode#visitEnd
@Override
public void visitEnd() {
    super.visitEnd();
    TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);

    if ("<init>".equals(name)) {
        isConstructor = true;
    }
        //判断是否是黑名单里面的,这里和黑名单中进行了互斥,在插桩中也会判断
    //所以可以不用在意这个细节,说白了就是判断这个方法不在黑名单中而已
    boolean isNeedTrace = isNeedTrace(configuration, traceMethod.className, mappingCollector);
    // filter simple methods 这里就是过滤简单方法,我们重点关注这里
    if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
            && isNeedTrace) {
        ignoreCount.incrementAndGet();
        collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
        return;
    }
    ......
}

上面截取出来的额低吗可见端倪,一个是判断构造方法,然后就是isEmptyMethodisGetSetMethodisSingleMethod,接下来就分析下这个所谓的过滤简单方法到底🐂🍺与否

//com.tencent.matrix.trace.MethodCollector.CollectMethodNode#isEmptyMethod
/**
 * 检测空方法,不知道这里为什么这么写。。。反正我验证这个方法基本没有用
 * -1 是F_NEW指令,不应该是判断return指令么?
 *
 * @return
 */
private boolean isEmptyMethod() {
    ListIterator<AbstractInsnNode> iterator = instructions.iterator();
    while (iterator.hasNext()) {
        //逻辑就是过滤掉是new指令?  说白了就是如果指令集不为空,就不是空方法
        AbstractInsnNode insnNode = iterator.next();
        int opcode = insnNode.getOpcode();
        //-1对应的opcode是NEW这个指令
        if (-1 == opcode) {
            continue;
        } else {
            return false;
        }
    }
    return true;
}

/*
 这里是空方法反编译后的字节码
 public void logClueMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 22: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/done/testlibrary/Utils;

*/

首先空方法的判断不够牛批哈~

接下来看第二个isGetSetMethod

private boolean isGetSetMethod() {
  int ignoreCount = 0;
  ListIterator<AbstractInsnNode> iterator = instructions.iterator();
  while (iterator.hasNext()) {
    AbstractInsnNode insnNode = iterator.next();
    int opcode = insnNode.getOpcode();
    if (-1 == opcode) {
      continue;
    }
    if (opcode != Opcodes.GETFIELD
        && opcode != Opcodes.GETSTATIC
        && opcode != Opcodes.H_GETFIELD
        && opcode != Opcodes.H_GETSTATIC

        && opcode != Opcodes.RETURN
        && opcode != Opcodes.ARETURN
        && opcode != Opcodes.DRETURN
        && opcode != Opcodes.FRETURN
        && opcode != Opcodes.LRETURN
        && opcode != Opcodes.IRETURN

        && opcode != Opcodes.PUTFIELD
        && opcode != Opcodes.PUTSTATIC
        && opcode != Opcodes.H_PUTFIELD
        && opcode != Opcodes.H_PUTSTATIC
        && opcode > Opcodes.SALOAD) {
      if (isConstructor && opcode == Opcodes.INVOKESPECIAL) {
        ignoreCount++;
        if (ignoreCount > 1) {
          return false;
        }
        continue;
      }
      return false;
    }
  }
  return true;
}

最后是判断是不是简单方法

private boolean isSingleMethod() {
  ListIterator<AbstractInsnNode> iterator = instructions.iterator();
  while (iterator.hasNext()) {
    AbstractInsnNode insnNode = iterator.next();
    int opcode = insnNode.getOpcode();
    if (-1 == opcode) {
      continue;
      //出现这个指令区间内,都标识调用了其他方法,会出现压栈的情况
      // 调用了别的方法,自然就不是简单的方法,不过这里没有判断指令的数量,感觉也不是一定可靠
    } else if (Opcodes.INVOKEVIRTUAL <= opcode && opcode <= Opcodes.INVOKEDYNAMIC) {
      return false;
    }
  }
  return true;
}

三个过滤的函数都看完了,感觉不一定可取,可以借鉴和参考,但不一定准,可能是在下没有严谨的编译看吧。。。不过有这种思路也还好,可以自己定义所谓的简单方法吧~

插桩代码

这里只简单看一下插桩的代码,具体计算耗时的功能逻辑实现后面会继续介绍,那部分也不在plugin的工程中~

3、针对界面启动耗时,因为要统计从 Activity#onCreate 到 Activity#onWindowFocusChange 间的耗时,所以在插桩过程中需要收集应用内所有 Activity 的实现类,并覆盖 onWindowFocusChange 函数进行打点。

4、为了方便及高效记录函数执行过程,我们为每个插桩的函数分配一个独立 ID,在插桩过程中,记录插桩的函数签名及分配的 ID,在插桩完成后输出一份 mapping,作为数据上报后的解析支持。

插桩对外的类是com.tencent.matrix.trace.MethodTracer,这里就简单看一个插桩source代码的,因为到最后不管是jar还是source,可都是对class文件进行处理~

//com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromSrc
private void innerTraceMethodFromSrc(File input, File output) {
    ArrayList<File> classFileList = new ArrayList<>();
    if (input.isDirectory()) {
        listClassFiles(classFileList, input);
    } else {
        classFileList.add(input);
    }
    for (File classFile : classFileList) {
        InputStream is = null;
        FileOutputStream os = null;
        try {
            ......
            if (MethodCollector.isNeedTraceFile(classFile.getName())) {
                is = new FileInputStream(classFile);
                ClassReader classReader = new ClassReader(is);
                ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                //关注这里插桩访问者访问类,然后里面插桩
                ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
                //调用这里后开始插桩
                classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                ......
            } else {
                FileUtil.copyFileUsingStream(classFile, changedFileOutput);
            }
        } catch (Exception e) {
            ......
        } finally {
            .......
        }
    }
}

代码里面关键的实现还是asm的classVisitor,我们只需要关注这里面的实现即可~

这里由于封装的路径还是有个两三层,我就简单说下调用关系,这里代码咱们还是看核心实现哈~

//com.tencent.matrix.trace.MethodTracer.TraceMethodAdapter
//1. 调用com.tencent.matrix.trace.MethodTracer.TraceClassAdapter#visitMethod
//2. 调用com.tencent.matrix.trace.MethodTracer.TraceMethodAdapter#TraceMethodAdapter
//3. 利用AdviceAdapter的方法进入和退出回调插桩

public final static String MATRIX_TRACE_CLASS = "com/tencent/matrix/trace/core/AppMethodBeat";

@Override
protected void onMethodEnter() {
    //这里的插桩结果就是在方法进入的时候插入 AppMethodBeat.i(methodid);
    TraceMethod traceMethod = collectedMethodMap.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 = collectedMethodMap.get(methodName);
    if (traceMethod != null) {
        if (hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) {
            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)) {
                //如果是onWindowFocusChanged,那么还会插入AppMethodBeat.at(activity, isFocus)
                traceWindowFocusChangeMethod(mv, className);
            }
        }
        //无论是不是 onWindowFocusChanged,都会插入AppMethodBeat.o(methodid);
        traceMethodCount.incrementAndGet();
        mv.visitLdcInsn(traceMethod.id);
        mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
    }
}

插装部分就是纯工作量的事情了,有了前面的判断和过滤逻辑,这里就非常简单,主要插入 i 方法和 o 方法就行具体可以看上面的代码注释 多余的这里就不展开去看代码了

JavaLib AppMethodBeat 实现

​ 上面就是trace插件在编译时所做的工作,可以看到插桩时期丝毫没有进行任何系统方法的调用,如:SystemClock.time或者System.nanoTime这些获取时间戳的native方法,这样可以理解为一个小优化的点~不通过系统方法获取时间,matrix利用很巧妙的方式来获取时间

考虑到每个方法执行前后都获取系统时间(System.nanoTime)会对性能影响比较大,而实际上,单个函数执行耗时小于 5ms 的情况,对卡顿来说不是主要原因,可以忽略不计,如果是多次调用的情况,则在它的父级方法中可以反映出来,所以为了减少对性能的影响,通过另一条更新时间的线程每 5ms 去更新一个时间变量,而每个方法执行前后只读取该变量来减少性能损耗。

具体就在java lib中去实现,下面我们就分析下java中的实现,其实上面插桩的时候就已经知道具体的实现的核心类是哪个了 => com.tencent.matrix.trace.core.AppMethodBeat

这个类的逻辑还是有一小丢丢绕,因为其中不仅仅包含了计算方法耗时,还兼顾了查看生命周期相关的,包括activity、service的生命周期,目测了下,contentProvider的还没完成,感兴趣的同学可以具体查看下内部关于hook mH相关的代码,这里就贴一下关键性的代码哈~

//com.tencent.matrix.trace.hacker.ActivityThreadHacker#hackSysHandlerCallback
public static void hackSysHandlerCallback() {
    try {
        Class<?> forName = Class.forName("android.app.ActivityThread");
        Field field = forName.getDeclaredField("sCurrentActivityThread");
        field.setAccessible(true);
        Object activityThreadValue = field.get(forName);
        //hook mH这个handler
        Field mH = forName.getDeclaredField("mH");
        mH.setAccessible(true);
        Object handler = mH.get(activityThreadValue);
        Class<?> handlerClass = handler.getClass().getSuperclass();
        if (null != handlerClass) {
            //接着hook系统的callback,方便内部调用从而不影响系统调动过程
            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);
        }
    } catch (Exception e) {
    }
}

进入正题,我们先从插桩中的方法入口和方法出口来分析

编译期已经对全局的函数进行插桩,在运行期间每个函数的执行前后都会调用 MethodBeat.i/o 的方法,如果是在主线程中执行,则在函数的执行前后获取当前距离 MethodBeat 模块初始化的时间 offset(为了压缩数据,存进一个long类型变量中),并将当前执行的是 MethodBeat i或者o、mehtod id 及时间 offset,存放到一个 long 类型变量中,记录到一个预先初始化好的数组 long[] 中 index 的位置(预先分配记录数据的 buffer 长度为 100w,内存占用约 7.6M)。数据存储如下图:

![方法数据逻辑.png](https://img.haomeiwen.com/i2822814/fecb3594fa74d08c.png?imageMogr2/aut
o-orient/strip%7CimageView2/2/w/820)

在搞清楚内部的逻辑之前,笔者认为直接贴代码不是很好理解,所以我们先来配个图,然后结合图来理解这个过程:

private static final int STATUS_DEFAULT = Integer.MAX_VALUE;
private static final int STATUS_STARTED = 2;
private static final int STATUS_READY = 1;
private static final int STATUS_STOPPED = -1;
private static final int STATUS_EXPIRED_START = -2;
private static final int STATUS_OUT_RELEASE = -3;


public static void i(int methodId) {
    ......
    //正式开始 step 1
    if (status == STATUS_DEFAULT) {
        synchronized (statusLock) {
            if (status == STATUS_DEFAULT) {
                realExecute();
                status = STATUS_READY;
            }
        }
    }
    long threadId = Thread.currentThread().getId();
    if (threadId == sMainThreadId) {
        //合并方法堆栈 step 2
        if (sIndex < Constants.BUFFER_SIZE) {
            mergeData(methodId, sIndex, true);
        } else {
            sIndex = 0;
            mergeData(methodId, sIndex, true);
        }
        ++sIndex;
    }
}

private static void realExecute() {
    //记录开始执行的时间戳
    sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;

    sHandler.removeCallbacksAndMessages(null);
    //启动计时器
    sHandler.postDelayed(sUpdateDiffTimeRunnable, Constants.TIME_UPDATE_CYCLE_MS);
    //状态维护,延迟15s后执行,由上可知有i首次进来以后状态是STATUS_READY
    //这里得结合looper的监听来说,后面分析捕捉细节再详说,这里记得这个状态维护
    sHandler.postDelayed(checkStartExpiredRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (statusLock) {
                MatrixLog.i(TAG, "[startExpired] timestamp:%s status:%s", System.currentTimeMillis(), status);
                if (status == STATUS_DEFAULT || status == STATUS_READY) {
                    status = STATUS_EXPIRED_START;
                }
            }
        }
    }, Constants.DEFAULT_RELEASE_BUFFER_DELAY);
    //注册监听
    LooperMonitor.register(looperMonitorListener);
}

然后可以看到时间更新的方法为,主要工作其实就是更新时间戳,每次更新后睡5s,然后挂起自己,等待被主线程分发消息时候再唤醒:

/**
 * 计时器
 * update time runnable
 */
private static Runnable sUpdateDiffTimeRunnable = new Runnable() {
    @Override
    public void run() {
        try {
            while (true) {
                while (!isPauseUpdateTime && status > STATUS_STOPPED) {
                    sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
                    SystemClock.sleep(Constants.TIME_UPDATE_CYCLE_MS);
                }
                synchronized (updateTimeLock) {
                    updateTimeLock.wait();
                }
            }
        } catch (Exception e) {
            MatrixLog.e(TAG, "" + e.toString());
        }
    }
};

private static void dispatchBegin() {
    sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
    isPauseUpdateTime = false;

    synchronized (updateTimeLock) {
        updateTimeLock.notify();
    }
}

最后在合并数据的时候把方法进入退出和方法id以及时间戳带上,组成一个long变量:

/**
 * merge trace info as a long data
 *
 * @param methodId
 * @param index
 * @param isIn
 */
private static void mergeData(int methodId, int index, boolean isIn) {
    //如果是分发的函数过来的,更新一下时刻,以示尊重~  就是更新时戳,这个时间可不能是简单的5的倍数[手动抠鼻]
    if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
        sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
    }
    //可见输入为1输出为0
    long trueId = 0L;
    if (isIn) {
        trueId |= 1L << 63;
    }
    trueId |= (long) methodId << 43;
    //将方法id和时间戳合并成一个long值,以达到8字节存储的目的
    trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
    sBuffer[index] = trueId;
    checkPileup(index);
    sLastIndex = index;
}

最后就把数据合并完成了。matrix的慢函数监控至此结束~

上一篇下一篇

猜你喜欢

热点阅读