[Matrix系列-3]: TracePlugin 之 FrameTrace 源码分析

开篇

第二篇文章中,我们分析了 TracePlugin 中的 LooperAnrTrace 类。今天这篇文章接着分析 TracePlugin 中的 FrameTrace 类 源码。

一、FrameTrace的触发时机

1.1 TracePlugin. start() 方法

注意看,在 TracePlugin 类的start() 方法中,调用了所有 Trace 类的 onStartTrace() 方法:

@Override
public void start() {
    // ... 省略
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
             
             // ... 省略
            // 1, 往addInputQueue 队列中加入inputCallback事件 
            UIThreadMonitor.getMonitor().onStart();
            if (traceConfig.isAnrTraceEnable()) {
                looperAnrTracer.onStartTrace();
            }

            if (traceConfig.isIdleHandlerEnable()) {
                idleHandlerLagTracer = new IdleHandlerLagTracer(traceConfig);
                idleHandlerLagTracer.onStartTrace();
            }

            if (traceConfig.isSignalAnrTraceEnable()) {
                if (!SignalAnrTracer.hasInstance) {
                    signalAnrTracer = new SignalAnrTracer(traceConfig);
                    signalAnrTracer.onStartTrace();
                }
            }

            if (traceConfig.isMainThreadPriorityTraceEnable()) {
                threadPriorityTracer = new ThreadPriorityTracer();
                threadPriorityTracer.onStartTrace();
            }
            // 2,这里调用,如果isFPSEnable为true 则启用fps监听
            if (traceConfig.isFPSEnable()) {
                frameTracer.onStartTrace();
            }

            if (traceConfig.isEvilMethodTraceEnable()) {
                evilMethodTracer.onStartTrace();
            }

            if (traceConfig.isStartupEnable()) {
                startupTracer.onStartTrace();
            }


        }
    };
    // ... 省略
}
复制代码
  • 说明:

由于在第一篇启动分析已经详细分析过,因此就省略了部分不相关的代码。我们主要看其中的两点:

  1. UIThreadMonitor.getMonitor().onStart();
  2. frameTracer.onStartTrace();

我们先看第一点到底做了什么工作。

1.1.1 UIThreadMonitor.onStart()

@Override
public synchronized void onStart() {
    if (!isInit) {
        MatrixLog.e(TAG, "[onStart] is never init.");
        return;
    }
    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];
        }
        //1,这个int[]数组用来保存UI渲染一帧中的三个阶段(input、animation、traversal)的开始和结束状态。
        queueStatus = new int[CALLBACK_LAST + 1];
        //2,这个long[]数组则是保存每一帧的三个阶段每个阶段对应的执行耗时
        queueCost = new long[CALLBACK_LAST + 1];
        //3,往input类型的链表中加入一个input阶段的callback。
        addFrameCallback(CALLBACK_INPUT, this, true);
    }
}
复制代码
  • 说明:

1,2 两点比较好理解,见注释。第三点需要详细分析下,先看 addFrameCallback 的源码:

private synchronized void addFrameCallback(int type, Runnable callback, boolean isAddHeader) {
   //省略...
    try {
        //1, 还记得,第一篇文章中启动分析里面,通过反射拿到的choreographer中锁对象。
        synchronized (callbackQueueLock) {
            Method method = null;
            switch (type) {
                case CALLBACK_INPUT:
              
                    method = addInputQueue;
                    break;
                case CALLBACK_ANIMATION:
                    method = addAnimationQueue;
                    break;
                case CALLBACK_TRAVERSAL:
                    method = addTraversalQueue;
                    break;
            }
            // 2,最终method就是 CallbackQueue的 addCallbackLocked 方法, 反射调用callback加入type对应的链表结构中
            if (null != method) {
                method.invoke(callbackQueues[type], !isAddHeader ? SystemClock.uptimeMillis() : -1, callback, null);
                callbackExist[type] = true;
            }
        }
    } catch (Exception e) {
        MatrixLog.e(TAG, e.toString());
    }
}
复制代码

总结:

外部需要监听每一帧的不同阶段,那么就通过反射的方法对象,注册不同类型的 callback 进去即可。

可以看到,我们在 onStart() 方法中就注册了一个input类型的callback。后面肯定会走run方法。因此我们接着看下 callback 的run 方法:

@Override
public void run() {
    final long start = System.nanoTime();
    try {
        doFrameBegin(token);
        doQueueBegin(CALLBACK_INPUT);

        addFrameCallback(CALLBACK_ANIMATION, new Runnable() {

            @Override
            public void run() {
                doQueueEnd(CALLBACK_INPUT);
                doQueueBegin(CALLBACK_ANIMATION);
            }
        }, true);

        addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {

            @Override
            public void run() {
                doQueueEnd(CALLBACK_ANIMATION);
                doQueueBegin(CALLBACK_TRAVERSAL);
            }
        }, true);

    } finally {
        if (config.isDevEnv()) {
            MatrixLog.d(TAG, "[UIThreadMonitor#run] inner cost:%sns", System.nanoTime() - start);
        }
    }
}
复制代码
private void doQueueBegin(int type) {
    queueStatus[type] = DO_QUEUE_BEGIN;
    queueCost[type] = System.nanoTime();
}

private void doQueueEnd(int type) {
    queueStatus[type] = DO_QUEUE_END;
    // 计算 耗时
    queueCost[type] = System.nanoTime() - queueCost[type];
    synchronized (this) {
        callbackExist[type] = false;
    }
}
复制代码
  • 说明:

逻辑很清晰,我们按顺序分别在input、animation、traversal三个阶段的开始和结束打点,然后统计每个阶段的耗时,存起来用作后面的上报。

好了,我们在接着看第二点: frameTracer.onStartTrace();

1.2 FrameTrace.onStartTrace() 方法

@Override
final synchronized public void onStartTrace() {
    if (!isAlive) {
        this.isAlive = true;
        onAlive();
    }
}
复制代码

onStartTrace()方法则调用了onAlive()

@Override
public void onAlive() {
    super.onAlive();
    if (isFPSEnable) {
        UIThreadMonitor.getMonitor().addObserver(this);
        Matrix.with().getApplication().registerActivityLifecycleCallbacks(this);
    }
}
复制代码
  • 说明:

如果 isFPSEnable=true,则注册监听 LooperObserver 类的回调。 至此,FrameTrace的初始化工作已经完成了。 后面就是等待回调信息过来,对数据进行处理后整合上报。所以我们来看看它的父类的父类: LooperObserver

1.3 LooperObserver 类

public abstract class LooperObserver {

    private boolean isDispatchBegin = false;

    @CallSuper
    public void dispatchBegin(long beginNs, long cpuBeginNs, long token) {
        isDispatchBegin = true;
    }

    public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {

    }

    @CallSuper
    public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) {
        isDispatchBegin = false;
    }

    public boolean isDispatchBegin() {
        return isDispatchBegin;
    }
}
复制代码

LooperObserver 是一个抽象类,它封装了 looper 消息处理开始和结束的回调,dispatchBegindispatchEnd这两个好理解,但是 doFrame 是什么回调呢? 我们一个一个来看吧~

1.3.1 dispatchBegin 方法解析

dispatchBegin(long beginNs, long cpuBeginNs, long token)

  • beginNs: 消息开始分发时候的时间点,单位为纳秒。通过 System.nanoTime() 获得。
  • cpuBeginNs: 消息开始时主线程的线程时间,单位:毫秒通过SystemClock.currentThreadTimeMillis()获得。
  • token: 跟 beginNs 一样。

1.3.2 dispatchEnd 方法解析

dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame)

  • beginNs: 消息开始分发时候的时间点,跟 dispatchBegin 中的 beginNs 是同一个值。
  • cpuBeginMs: 跟 dispatchBegin 中的 cpuBeginMs 是同一个值。
  • endNs: 消息结束分发时的时间点,单位:纳秒。
  • cpuEndMs: 消息结束分发时主线程时间点,单位:毫秒。
  • token: 跟 dispatchBegin 中的 token 是同一个值。
  • isVsyncFrame: 用来判断 looper 中消息是否是UI渲染帧。 默认为false。matrix 通过 UIThreadMonitor 类中 addFrameCallback(CALLBACK_INPUT, this, true); 开始监听input事件,在runnable 开始执行时候,说明此时looper中的消息是处理ui 绘制的。
@Override
public synchronized void onStart() {
    if (!isInit) {
        MatrixLog.e(TAG, "[onStart] is never init.");
        return;
    }
    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];
        }
        queueStatus = new int[CALLBACK_LAST + 1];
        queueCost = new long[CALLBACK_LAST + 1];
        // 
        addFrameCallback(CALLBACK_INPUT, this, true);
    }
}
复制代码

1.3.3 doFrame() 方法解析

首先,我们想要知道 doFrame方法在哪里被调用? 其实是在 UIThreadMonitor 类的 dispatchEnd方法中被调用的:

private void dispatchEnd() {
//省略...
synchronized (observers) {
    for (LooperObserver observer : observers) {
        if (observer.isDispatchBegin()) {
            observer.doFrame(AppMethodBeat.getVisibleScene(), startNs, endNs, isVsyncFrame, intendedFrameTimeNs, queueCost[CALLBACK_INPUT], queueCost[CALLBACK_ANIMATION], queueCost[CALLBACK_TRAVERSAL]);
        }
    }
}
//省略...
}
复制代码

方法签名含义如下:

doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs)

focusedActivity: 当前顶层的activity名字

  • startNs:消息开始分发的时间点 纳秒
  • endNs:消息分发结束的时间点 纳秒
  • isVsyncFrame: 是否是UI渲染帧。只要触发了input callback回调,那肯定是渲染消息。
  • intendedFrameTimeNs:当前时间,纳秒。如果反射获取失败,则把startNs作为默认值。 通过反射Choreographer内部类 FrameDisplayEventReceiver类中的变量:mTimestampNanos
  • inputCostNs:input耗时。现在这三个比较简单了吧
  • animationCostNs:animation耗时。
  • traversalCostNs:traversal耗时。

这里先提出一个问题:

dispatchEndlooper 中每个消息处理完成后都会回调的,其中一些消息肯定不是UI渲染。那么doFrame 肯定每次也会回调,也就是说每一次消息处理都当成了一帧渲染完毕。

那么这不是有问题吗?不会造成错误统计? 后面会给出答案~

1.4 总结

分析了这么久,我们终于搞懂了FrameTrace背后的运行原理,主要做了以下两点:

  1. 通过反射方式注册 callback到 Choreographer 类的callback链表中。计算input、animation、traversal三个阶段的耗时。
  2. UIThreadMonitor 中注册消息回调,主要关注的是每一帧消息处理的doFrame 方法。

在拿到这些数据后,matrix是怎么去做处理和上报的呢? 我们接着往下看。

二、 FrameTrace的数据处理

经过分析,我们只关心 doFrame 方法即可。

2.1 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);
    }
}
复制代码

只有在前台才会去做数据的收集和处理,后台情况下不会处理。为什么? 因为应用不可见的时候是不会进行UI渲染的。这是系统来控制的,避免造成资源白白浪费。

接着看 notifyListener 方法:

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) {
    long traceBegin = System.currentTimeMillis();
    try {
        final long jiter = endNs - intendedFrameTimeNs;
        //整个绘制过程的帧数: 实际上一帧的耗时/理论每一帧的时间。因此,就是卡顿导致的掉帧数
        final int dropFrame = (int) (jiter / frameIntervalNs);
        if (dropFrameListener != null) {
            //触发掉帧数阈值,则分发掉帧数回调
            if (dropFrame > dropFrameListenerThreshold) {
                try {
                    if (AppActiveMatrixDelegate.getTopActivityName() != null) {
                        // 这里可以先不管 。。。
                        long lastResumeTime = lastResumeTimeMap.get(AppActiveMatrixDelegate.getTopActivityName());
                        dropFrameListener.dropFrame(dropFrame, AppActiveMatrixDelegate.getTopActivityName(), lastResumeTime);
                    }
                } catch (Exception e) {
                    MatrixLog.e(TAG, "dropFrameListener error e:" + e.getMessage());
                }
            }
        }

        droppedSum += dropFrame;
        durationSum += Math.max(jiter, frameIntervalNs);

        synchronized (listeners) {
            for (final IDoFrameListener listener : listeners) {
                if (config.isDevEnv()) {
                    listener.time = SystemClock.uptimeMillis();
                }
                if (null != listener.getExecutor()) {
                    //当超过多少帧才会去收集
                    // 帧数阈值>0 表示只有设置了多少帧才开始收集。如果没设置,那我没必要去分析回放。
                    if (listener.getIntervalFrameReplay() > 0) {
                        //这个是开了fpsmonitor开关才会走这里,否则是默认是走else
                        listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                    } else {
                        //如果我收集了,那就不执行这里的逻辑
                        listener.getExecutor().execute(new Runnable() {
                            @Override
                            public void run() {
                                //做UI展示用,
                                listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                        intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                            }
                        });
                    }
                } else {
                    listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                            intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                }

                if (config.isDevEnv()) {
                    listener.time = SystemClock.uptimeMillis() - listener.time;
                    MatrixLog.d(TAG, "[notifyListener] cost:%sms listener:%s", listener.time, listener);
                }
            }
        }
    } finally {
        long cost = System.currentTimeMillis() - traceBegin;
        if (config.isDebug() && cost > frameIntervalNs) {
            MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync! size:%s cost:%sms", listeners.size(), cost);
        }
    }
}
复制代码

说明:

这个方法看起来比较长,主要做了两件事情:

  1. 计算掉帧数
  2. 通过异步的方式收集数据进行处理

2.1.1 掉帧数的计算

final long jiter = endNs - intendedFrameTimeNs;
//整个绘制过程的帧数: 实际上一帧的耗时/理论每一帧的时间。因此,就是卡顿导致的掉帧数
final int dropFrame = (int) (jiter / frameIntervalNs);
复制代码

原理: 理论上我们要求一帧的渲染时间为16.66ms。那么用实际上一帧花费的时间除以理论的时间就得到了这段时间的帧数。 举个例子:

  • 假设一帧实际上花费时间:<16.66ms,那么 dropFrame=0。表示没有掉帧,非常nice。
  • 假设一帧实际上花费时间:=16.66ms,那么 dropFrame=1。表示没有掉帧,非常nice。
  • 假设一帧实际上花费时间:16.66ms< <约32ms,那么 dropFrame=1。虽然没有达到理论值,但基本认为nice。
  • 假设一帧实际上花费时间:=32ms,那么 dropFrame=2。认为掉了一帧,但基本认为nice。

按照这种计算,matrix根据掉帧数的区间分布,定出了这样一个衡量流畅度的标准:

image.png

如果一个页面的掉帧数是0~3帧区间,认为是best级别。也就是每一帧的渲染时间基本维持在16ms ~ 48ms之间。转换一下更好理解:

image.png

这么定义有什么好处呢? 如果单纯的去看界面的平均帧率,看不出来哪里卡顿了。再有就算每一帧是32ms,只要保持连续帧都是32ms,那么对人眼来说也不认为是卡顿。只有突然出现一帧如 >300ms,那么用户就会觉得不流畅。

因此,我们通过收集掉帧数,来评判一个界面在一段时间内(matrix认为这段时间是200帧。也就是每200帧做一次分析)的掉帧数分布。官网图:

image.png

2.1.2 数据处理逻辑

如果开启了fps开关,那么就开始分析数据吧

//这个是开了fpsmonitor开关才会走这里,否则是默认是走else
listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
        intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
复制代码

2.1.2.1 IDoFrameListener.collect() 方法

FPSCollector 继承了 IDoFrameListener类,所以先看下collect方法:

@CallSuper
public void collect(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame,
                    long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    //FrameReplay 代表每一帧的信息
    FrameReplay replay = FrameReplay.create();
    replay.focusedActivity = focusedActivity;
    replay.startNs = startNs;
    replay.endNs = endNs;
    replay.dropFrame = dropFrame;
    replay.isVsyncFrame = isVsyncFrame;
    replay.intendedFrameTimeNs = intendedFrameTimeNs;
    replay.inputCostNs = inputCostNs;
    replay.animationCostNs = animationCostNs;
    replay.traversalCostNs = traversalCostNs;
    list.add(replay);
    
    //当收集到的帧数> 200的 且 有提供线程执行器的时候,开始分析收集到的帧数信息
    if (list.size() >= intervalFrame && getExecutor() != null) {
    
    // intervalFrame 由 FrameTrace 的内部类 FPSCollector 来决定。
    
        final List<FrameReplay> copy = new LinkedList<>(list);
        list.clear();
        //拷贝一份,并且根据备份开启一个分析的task。
        getExecutor().execute(new Runnable() {
            @Override
            public void run() {
                doReplay(copy);
                for (FrameReplay record : copy) {
                    //处理完成后,进行释放
                    record.recycle(); //循环利用
                }
            }
        });
    }
}
复制代码

很简单,注释里写的清楚了。 接着看 doReplay 方法:

2.1.2.2 FPSCollector.doReplay() 方法

@Override
public void doReplay(List<FrameReplay> list) {
    // 开始分析回放帧集合
    super.doReplay(list);

    for (FrameReplay replay : list) {
        doReplayInner(replay.focusedActivity, replay.startNs, replay.endNs, replay.dropFrame, replay.isVsyncFrame,
                replay.intendedFrameTimeNs, replay.inputCostNs, replay.animationCostNs, replay.traversalCostNs);
    }
}
复制代码

遍历帧集合,开始对每一帧进行分析~

    public void doReplayInner(String visibleScene, long startNs, long endNs, int droppedFrames,
                              boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs,
                              long animationCostNs, long traversalCostNs) {

        if (Utils.isEmpty(visibleScene)) return;
        // 1, 这里需要关注,回答了之前提出的问题
        if (!isVsyncFrame) return;

        // 2,从页面维度,把每一帧和当前的页面关联起来
        FrameCollectItem item = map.get(visibleScene);
        if (null == item) {
            item = new FrameCollectItem(visibleScene);
            map.put(visibleScene, item);
        }
        //3,开始对每个页面的所属帧信息进行收集
        item.collect(droppedFrames);

        
        //4,某个页面触发上报逻辑 timeSliceMs 默认是10s,可配置
        if (item.sumFrameCost >= timeSliceMs) { // report
            map.remove(visibleScene);
           
            // 5, 开始上报
            item.report();
        }
    }
}
复制代码

主要做了5个方面的事情:

  1. 排除无效帧
  2. 针对页面维度进行收集
  3. 具体收集逻辑
  4. 上报触发的逻辑
  5. 上报整合
  • 1、排除无效帧

这里过滤掉非UI绘制消息(无效帧)。比如,你在主线程通过handler发送一个普通的消息,但是并没有去刷新UI,虽然matrix会当做一帧来统计,但是这里不会进行分析。因为根本就没有走绘制流程,没有意义。

这也就回答之前提出的问题,所以不会影响到数据的统计!

还有就是matrix是通过looper的消息结束的时候进行监听的,而不是在choreographeronVsync方法中去监听。为什么?

因为 onVsync只能监听到有卡顿,而不知道哪里卡顿了。 而matrix通过自己在外部不断postcallback到choreographer的队列中,只要发生了UI绘制,那么就肯定会走looper的消息结束接口,然后在计算每个阶段的以及当前帧的耗时。非常的巧妙呀!

  • 2、针对页面维度进行收集

收集到的帧,肯定有多个帧属于同一个页面的。因此,基于页面的维度来进行统计。 而不是从帧的维度进行统计。这样更难全面的反映页面的卡顿情况分布。

  • 3、具体收集逻辑
void collect(int droppedFrames) {
    float frameIntervalCost = 1f * UIThreadMonitor.getMonitor().getFrameIntervalNanos()
            / Constants.TIME_MILLIS_TO_NANO;
    //统计的时间内,这个页面的总的绘制耗时。
    sumFrameCost += (droppedFrames + 1) * frameIntervalCost;
    
    //统计的时间内,这个页面的总的掉帧数 
    sumDroppedFrames += droppedFrames;
    
    //统计的时间内,这个页面总共绘制并统计了多少帧 
    sumFrame++;
    
    // 这相当于是一个页面的所有统计。
    //说白了,就是统计在某个页面,出现了好、坏、中、高等掉帧数的一个分布。看看占比。非常的nice呀!
    if (droppedFrames >= frozenThreshold) {
        //发生冻帧的次数,可以用来算占比情况
        dropLevel[DropStatus.DROPPED_FROZEN.index]++;
        // 发生冻帧的 掉帧数,可用来看分布情况 
        dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames;
    } else if (droppedFrames >= highThreshold) {
        dropLevel[DropStatus.DROPPED_HIGH.index]++;
        dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames;
    } else if (droppedFrames >= middleThreshold) {
        dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
        dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames;
    } else if (droppedFrames >= normalThreshold) {
        dropLevel[DropStatus.DROPPED_NORMAL.index]++;
        dropSum[DropStatus.DROPPED_NORMAL.index] += droppedFrames;
    } else {
        dropLevel[DropStatus.DROPPED_BEST.index]++;
        dropSum[DropStatus.DROPPED_BEST.index] += Math.max(droppedFrames, 0);
    }
}
复制代码

注释写的很清楚啦~

  • 4、上报触发的逻辑

当前某个页面的总绘制耗时,大于某个阈值才进行上报。 matrix 默认是10s,外部可以配置。

相当于某个页面存在卡顿的情况,但是如果不是经常的触发,导致累计达不到上报的条件,可以认为该页面卡顿情况还是可以接受的。如果触发了,则表示该卡顿大概率存在于线上场景且比较严重,我们需要引起重视了。

  • 5、上报整合
void report() {
    // 当前页面的时机fps: 每秒多少帧 
    float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost);
    MatrixLog.i(TAG, "[report] FPS:%s %s", fps, toString());

    try {
        TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
        if (null == plugin) {
            return;
        }
        // 上报每个区间的 掉帧次数,用来算占比
        JSONObject dropLevelObject = new JSONObject();
        dropLevelObject.put(DropStatus.DROPPED_FROZEN.name(), dropLevel[DropStatus.DROPPED_FROZEN.index]);
        dropLevelObject.put(DropStatus.DROPPED_HIGH.name(), dropLevel[DropStatus.DROPPED_HIGH.index]);
        dropLevelObject.put(DropStatus.DROPPED_MIDDLE.name(), dropLevel[DropStatus.DROPPED_MIDDLE.index]);
        dropLevelObject.put(DropStatus.DROPPED_NORMAL.name(), dropLevel[DropStatus.DROPPED_NORMAL.index]);
        dropLevelObject.put(DropStatus.DROPPED_BEST.name(), dropLevel[DropStatus.DROPPED_BEST.index]);

        //上报每个区间的 掉帧数和 用来看分布
        JSONObject dropSumObject = new JSONObject();
        dropSumObject.put(DropStatus.DROPPED_FROZEN.name(), dropSum[DropStatus.DROPPED_FROZEN.index]);
        dropSumObject.put(DropStatus.DROPPED_HIGH.name(), dropSum[DropStatus.DROPPED_HIGH.index]);
        dropSumObject.put(DropStatus.DROPPED_MIDDLE.name(), dropSum[DropStatus.DROPPED_MIDDLE.index]);
        dropSumObject.put(DropStatus.DROPPED_NORMAL.name(), dropSum[DropStatus.DROPPED_NORMAL.index]);
        dropSumObject.put(DropStatus.DROPPED_BEST.name(), dropSum[DropStatus.DROPPED_BEST.index]);

        JSONObject resultObject = new JSONObject();
        resultObject = DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication());
        //上报 当前面的名字+ 卡顿区间发生次数+卡顿区间掉帧总数+实际fps
        resultObject.put(SharePluginInfo.ISSUE_SCENE, visibleScene);
        resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);
        resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);
        resultObject.put(SharePluginInfo.ISSUE_FPS, fps);

        Issue issue = new Issue();
        issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS);
        issue.setContent(resultObject);
        plugin.onDetectIssue(issue);

    } catch (JSONException e) {
        MatrixLog.e(TAG, "json error", e);
    } finally {
        sumFrame = 0;
        sumDroppedFrames = 0;
        sumFrameCost = 0;
    }
}
复制代码

三、总结

FrameTrace 做的事情如下:

  1. 收集每一帧的信息:包含绘制开始和结束的时间、当前页面名字、input、animation、traversal阶段的耗时等等
  2. 从页面的维度触发,对当前页面发生的渲染帧进行了一个全面的评估:卡顿区间的占比是多少。该页面卡顿引起的原因是连续的掉帧? 还是某个帧严重卡顿?
  3. 当然,还提供了fps相关的页面展示类: FrameDecorator。在测试环境下,外部可以直接用。

好了,FrameTrace分析完了,后续的分析还在继续!

由于个人水平有限,若有问题之处还望评论指正,大家共同进步!如果觉得文章有帮助,还望帮忙点点赞,感谢~

Guess you like

Origin juejin.im/post/7032953373900947464