美文网首页
Android 卡顿方案研究

Android 卡顿方案研究

作者: DoneWillianm | 来源:发表于2020-11-20 16:45 被阅读0次

    Android 卡顿研究

    [TOC]

    稳定化,不是说说而已

    基础概念

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

    散列知识点

    • JVM中的线程切换大概花费CPU 20000个时钟周期

    CPU

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

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

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

    • 时钟周期:CPU每秒可以完成几个时钟周期,如
      2.4GHz = 2.4 * 10^9
      可以完成这么多个时钟周期

    • 机器周期:主存中读取一个指令字的最短时间(由于CPU内部的操作速度较快.而CPU访问一次主存所花的时间较长,因此机器周期通常用主存中读取一个指令字的最短时间来规定。),所以 机器周期 = 时钟周期 * n(n >= 1)

    • 指令周期:完成一个指令需要的时间,一般由 几个或者一个机器周期组成,相当于 指令周期 = 机器周期 * n(n >= 1)

    方法论

    指标

    • CPU使用率
    adb查看CPU.png

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

    • 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
    • 主线程抓取堆栈的部分

    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的堆栈实现主要分为两块

    • ASM插桩记录方法
    • Java侧关于记录方法耗时以及方法记录的实现
    编译期:

    通过代理编译期间的任务 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的慢函数监控至此结束~

    相关文章

      网友评论

          本文标题:Android 卡顿方案研究

          本文链接:https://www.haomeiwen.com/subject/zythiktx.html