腾讯性能监控框架Matrix源码分析(八)TracePlugin 之 帧率面板展示

606 阅读7分钟

前文分析了 TracePlugin 帧率分析的数据来源,本文将分析这些数据是如何计算和展示到 View 上的。

一、效果预览

先来看一下官方 Demo 里面的效果:

20210207151523544-2.gif

从上面的 Demo 中可以看出:

右上角展示帧率、统计柱状图。 其实展示的是一个自定义 View,将接收到的数据经过计算得出帧率。

上图绿色的 60.00 FPS 指的是过去 300ms 内的平均帧率

灰色的 sum: 3.0 是 总掉帧次数,下面的彩虹从左到右分别代表 Normal/Middle/High/Frozen 这四个级别的掉帧占总掉帧的比例,往下是当前页面的掉帧数和掉帧比例

最底下的图表是过去 10s 内平均帧率(200ms 时间段)的横向柱状图,每 5s 就会有 25 条记录,50 FPS 差不多是 Normal 的帧率下限,30 FPS 差不多是 Middle 的帧率下限

滑动一段时间之后,跳转到结果页面。 搜集够一定数量的数据,报告 Issus 告知开发者。

页面静止时帧率为 60,滑动时帧率发生变化。 当 View 没有发生变化时,不会请求刷新,展示的是系统帧率。 当 View 滑动时,请求接收垂直同步信号,再经过计算得出帧率。

Demo 中展示的是一个持有 ListView 的 Activity,为了模拟卡顿效果,在每次触摸 ListView 的时候主线程休眠一段时间。比如 TestTraceMainActivity

public class TestTraceMainActivity extends Activity implements IAppForeground {
    private static String TAG = "Matrix.TestTraceMainActivity";
    FrameDecorator decorator;
    private static final int PERMISSION_REQUEST_CODE = 0x02;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test_trace);
        IssueFilter.setCurrentFilter(IssueFilter.ISSUE_TRACE);

        Plugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
        if (!plugin.isPluginStarted()) {
            MatrixLog.i(TAG, "plugin-trace start");
            plugin.start();
        }
        //初始化帧率控制器FrameDecorator
        decorator = FrameDecorator.getInstance(this);
        //检测悬浮窗权限
        if (!canDrawOverlays()) {
            requestWindowPermission();
        } else {
            decorator.show();
        }

        AppActiveMatrixDelegate.INSTANCE.addListener(this);
    }

展示帧率 View,类 FrameDecorator 负责接收数据和展示帧率图,通过 FrameDecorator 展示右上角的 View 帧率统计图是一个自定义 View,并且由 WindowManager 添加,所以需要在 Android M(6.0) 以上的设备打开浮窗权限。

private boolean canDrawOverlays() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        return Settings.canDrawOverlays(this);
    } else {
        return true;
    }
}

下面看看FrameDecorator.getInstance(this); 因为帧率都是在UI线程,所以这里做了特别判断,为了避免线程并发问题,这里使用了lock来控制,视图的真正展示是自定义View FrameDecorator 自定义View技术原理暂时不展开讨论

public static FrameDecorator getInstance(final Context context) {
    if (instance == null) {
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            instance = new FrameDecorator(context, new FloatFrameView(context));
        } else {
            try {
                synchronized (lock) {
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            instance = new FrameDecorator(context, new FloatFrameView(context));
                            synchronized (lock) {
                                lock.notifyAll();
                            }
                        }
                    });
                    lock.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    return instance;
}

看下FrameDecorator构造函数

private FrameDecorator(Context context, final FloatFrameView view) {
    this.frameIntervalMs = 1f * UIThreadMonitor.getMonitor().getFrameIntervalNanos() / Constants.TIME_MILLIS_TO_NANO;
    this.maxFps = Math.round(1000f / frameIntervalMs);
    this.view = view;
    //初始化视图基础参数
    view.fpsView.setText(String.format("%.2f FPS", maxFps));
    this.bestColor = context.getResources().getColor(R.color.level_best_color);
    this.normalColor = context.getResources().getColor(R.color.level_normal_color);
    this.middleColor = context.getResources().getColor(R.color.level_middle_color);
    this.highColor = context.getResources().getColor(R.color.level_high_color);
    this.frozenColor = context.getResources().getColor(R.color.level_frozen_color);

    AppActiveMatrixDelegate.INSTANCE.addListener(this);
    //视图创建时增加帧回调,回调的数据来自前文讲的FrameTracer
    view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {
            MatrixLog.i(TAG, "onViewAttachedToWindow");
            if (Matrix.isInstalled()) {
                TracePlugin tracePlugin = Matrix.with().getPluginByClass(TracePlugin.class);
                if (null != tracePlugin) {
                    FrameTracer tracer = tracePlugin.getFrameTracer();
                    tracer.addListener(FrameDecorator.this);
                }
            }
        }

        @Override
        public void onViewDetachedFromWindow(View v) {
            MatrixLog.i(TAG, "onViewDetachedFromWindow");
            if (Matrix.isInstalled()) {
                TracePlugin tracePlugin = Matrix.with().getPluginByClass(TracePlugin.class);
                if (null != tracePlugin) {
                    FrameTracer tracer = tracePlugin.getFrameTracer();
                    tracer.removeListener(FrameDecorator.this);
                }
            }
        }
    });
    //初始化布局参数
    initLayoutParams(context);
    //设置拖拽功能,支持把视图面板划到屏幕任意地方
    view.setOnTouchListener(new View.OnTouchListener() {
        float downX = 0;
        float downY = 0;
        int downOffsetX = 0;
        int downOffsetY = 0;

        @Override
        public boolean onTouch(final View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    downX = event.getX();
                    downY = event.getY();
                    downOffsetX = layoutParam.x;
                    downOffsetY = layoutParam.y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    float moveX = event.getX();
                    float moveY = event.getY();
                    layoutParam.x += (moveX - downX) / 3;
                    layoutParam.y += (moveY - downY) / 3;
                    if (v != null) {
                        windowManager.updateViewLayout(v, layoutParam);
                    }
                    break;
                case MotionEvent.ACTION_UP:

                    PropertyValuesHolder holder = PropertyValuesHolder.ofInt("trans", layoutParam.x,
                            layoutParam.x > displayMetrics.widthPixels / 2 ? displayMetrics.widthPixels - view.getWidth() : 0);

                    Animator animator = ValueAnimator.ofPropertyValuesHolder(holder);
                    ((ValueAnimator) animator).addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            if (!isShowing) {
                                return;
                            }
                            int value = (int) animation.getAnimatedValue("trans");
                            layoutParam.x = value;
                            windowManager.updateViewLayout(v, layoutParam);
                        }
                    });
                    animator.setInterpolator(new AccelerateInterpolator());
                    animator.setDuration(180).start();

                    int upOffsetX = layoutParam.x;
                    int upOffsetY = layoutParam.y;
                    if (Math.abs(upOffsetX - downOffsetX) <= 20 && Math.abs(upOffsetY - downOffsetY) <= 20) {
                        if (null != clickListener) {
                            clickListener.onClick(v);
                        }
                    }
                    break;
            }
            return true;
        }

    });
}

代码已经写的比较清楚,我们这里关心的是注册了帧的回调tracer.addListener(FrameDecorator.this) 有了帧回调便可以实时刷新帧率数据

doFrameAsync是不是很熟悉的方法,来自于FrameTracer的回调

@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 (!Objects.equals(focusedActivity, lastVisibleScene)) {
        dropLevel = new int[FrameTracer.DropStatus.values().length];
        lastVisibleScene = focusedActivity;
        lastCost[0] = 0;
        lastFrames[0] = 0;
    }
    //时长 ,帧率计算
    sumFrameCost += (dropFrame + 1) * frameIntervalMs;
    sumFrames += 1;
    float duration = sumFrameCost - lastCost[0];
    //计算传入自定义view的参数
    if (dropFrame >= Constants.DEFAULT_DROPPED_FROZEN) {
        dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;
        belongColor = frozenColor;
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_HIGH) {
        dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;
        if (belongColor != frozenColor) {
            belongColor = highColor;
        }
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_MIDDLE) {
        dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;
        if (belongColor != frozenColor && belongColor != highColor) {
            belongColor = middleColor;
        }
    } else if (dropFrame >= Constants.DEFAULT_DROPPED_NORMAL) {
        dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;
        if (belongColor != frozenColor && belongColor != highColor && belongColor != middleColor) {
            belongColor = normalColor;
        }
    } else {
        dropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;
        sumDropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;
        if (belongColor != frozenColor && belongColor != highColor && belongColor != middleColor && belongColor != normalColor) {
            belongColor = bestColor;
        }
    }


    long collectFrame = sumFrames - lastFrames[0];
    //200ms刷新一次
    if (duration >= 200) {
        final float fps = Math.min(maxFps, 1000.f * collectFrame / duration);
        //更新视图
        updateView(view, fps, belongColor,
                dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index],
                dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index],
                dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index],
                dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index],
                sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index],
                sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index],
                sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index],
                sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]);
        belongColor = bestColor;
        lastCost[0] = sumFrameCost;
        lastFrames[0] = sumFrames;
        mainHandler.removeCallbacks(updateDefaultRunnable);
         //250ms刷新一次
        mainHandler.postDelayed(updateDefaultRunnable, 250);
    }
}

更新视图,注意,视图一定要在UI线程

private void updateView(final FloatFrameView view, final float fps, final int belongColor,
                        final int normal, final int middle, final int high, final int frozen,
                        final int sumNormal, final int sumMiddle, final int sumHigh, final int sumFrozen) {
    int all = normal + middle + high + frozen;
    float frozenValue = all <= 0 ? 0 : 1.f * frozen / all * 60;
    float highValue = all <= 0 ? 0 : 1.f * high / all * 25;
    float middleValue = all <= 0 ? 0 : 1.f * middle / all * 14;
    float normaValue = all <= 0 ? 0 : 1.f * normal / all * 1;
    float qiWang = frozenValue + highValue + middleValue + normaValue;

    int sumAll = sumNormal + sumMiddle + sumHigh + sumFrozen;
    float sumFrozenValue = sumAll <= 0 ? 0 : 1.f * sumFrozen / sumAll * 60;
    float sumHighValue = sumAll <= 0 ? 0 : 1.f * sumHigh / sumAll * 25;
    float sumMiddleValue = sumAll <= 0 ? 0 : 1.f * sumMiddle / sumAll * 14;
    float sumNormaValue = sumAll <= 0 ? 0 : 1.f * sumNormal / sumAll * 1;
    float sumQiWang = sumFrozenValue + sumHighValue + sumMiddleValue + sumNormaValue;

    final String radioFrozen = String.format("%.1f", frozenValue);
    final String radioHigh = String.format("%.1f", highValue);
    final String radioMiddle = String.format("%.1f", middleValue);
    final String radioNormal = String.format("%.1f", normaValue);
    final String qiWangStr = String.format("current: %.1f", qiWang);

    final String sumRadioFrozen = String.format("%.1f", sumFrozenValue);
    final String sumRadioHigh = String.format("%.1f", sumHighValue);
    final String sumRadioMiddle = String.format("%.1f", sumMiddleValue);
    final String sumRadioNormal = String.format("%.1f", sumNormaValue);
    final String sumQiWangStr = String.format("sum: %.1f", sumQiWang);

    final String fpsStr = String.format("%.2f FPS", fps);

    mainHandler.post(new Runnable() {
        @Override
        public void run() {
            view.chartView.addFps((int) fps, belongColor);
            view.fpsView.setText(fpsStr);
            view.fpsView.setTextColor(belongColor);

            view.qiWangView.setText(qiWangStr);
            view.levelFrozenView.setText(radioFrozen);
            view.levelHighView.setText(radioHigh);
            view.levelMiddleView.setText(radioMiddle);
            view.levelNormalView.setText(radioNormal);

            view.sumQiWangView.setText(sumQiWangStr);
            view.sumLevelFrozenView.setText(sumRadioFrozen);
            view.sumLevelHighView.setText(sumRadioHigh);
            view.sumLevelMiddleView.setText(sumRadioMiddle);
            view.sumLevelNormalView.setText(sumRadioNormal);

        }
    });
}

当搜集到的帧率数据超过设置的时间后(Demo 中设置的是 10s,开发者可自行设置),便进行上报,这样我们就可以通过某个时段的帧数来确定该页面是否需要进行优化。

20210126162000293.png

帧率数据从哪来?

先说结论,帧率数据从 UIThreadMonitor 来。翻看前文。

20210205160908176.png

设置数据给帧率 View

还是先来看一下数据是如何设置到帧率 View 的

20210207132155262.png

回过头来看插件报告捕捉到的一段时间内的数据:

20210126162000293.png

  • machine:设备名称,因为用的模拟器所以没能获取到;
  • scene:场景,也就是在哪个地方捕捉的数据,这里是一个 Activity;
  • dropLevel: 丢帧等级,Matrix 把丢帧分为四个等级:
  • DROPPED_FROZEN: 丢帧严重;
  • DROPPED_HIGH: 高度丢帧;
  • DROPPED_MIDDLE: 中度丢帧;
  • DROPPED_NORMAL: 普通丢帧;
  • DROPPED_BEST: 低丢帧,最佳状态;
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

  • dropLevel: 掉帧统计;

关于卡顿官方文档是这么解释的: FPS 低并不意味着卡顿发生,而卡顿发生 FPS 一定不高。  FPS 可以衡量一个界面的流程性,但往往不能很直观的衡量卡顿的发生,这里有另一个指标(掉帧程度)可以更直观地衡量卡顿。 所以 Matrix 使用 dropLevel 来统计一段时间内的丢帧程度。打个比方,如果这段时间丢帧等级基本在 DROPPED_BEST(发生了丢帧,但是丢的数量在 3 以下),那么属于比较完美的情况无需优化。

而 Demo 中 :

  • OPPED_MIDDLE(中度丢帧) 发生 24 次,所丢帧数 281; 按照每秒 60 帧来计算,中度丢帧发生了将近 3s。
  • OPPED_NORMAL(普通丢帧) 发生 31 次,所丢帧数 146; 普通丢帧发生了 2s 多。
  • OPPED_BEST(低丢帧)发生 237 次,所丢帧数 14。

所以在这 10s 中有将近 5s 发生了丢帧,说明当前页面存在问题需要优化,需要检查有没有在主线程或 View 的更新上面执行了复杂的逻辑。

fpx:帧率。计算出的平均帧数。

丢帧数量的计算

如何得知某一时间段丢帧的值呢?我们来看一下 Matrix 是怎么做的。

  1. 首先需要获取设备的刷新率,尝试反射获取系统的值。获取不到则使用默认值:
private long frameIntervalNanos = ReflectUtils.reflectObject(choreographer, "mFrameIntervalNanos", Constants.DEFAULT_FRAME_DURATION);
public static final long DEFAULT_FRAME_DURATION = 16666667L;

假设这台设备刷新率 60,那么每 16ms 刷新一次,也就是 166666… 纳秒刷新一次。

2.获取 VSync 垂直同步信号处理的时间

接收到信号记录当前时间: token = dispatchTimeMs[0] = System.nanoTime();

一次刷新处理完毕记录时间:long endNs = System.nanoTime();

  1. 计算所丢帧数:
// 一次刷新处理的时间
final long jiter = endNs - intendedFrameTimeNs;
// 除以刷新率
final int dropFrame = (int) (jiter / frameIntervalNs);

如果一次刷新耗时 16ms,这台设备 16ms 刷新一次,得出刚好丢失 1 帧。但是如果耗时不足 16ms,得出 0 说明不会丢帧。

最后简单总结下

帧率数据从 UIThreadMonitor 来,通过监听和回调的方式告知 FrameTracer; FrameDecorator 负责接收数据和管理帧率 View,通过设置监听给 FrameTracer 接收帧率信息; 丢帧分为五个等级,FrameTracer 会统计丢帧的次数和所丢的帧数; 丢帧信息由 FrameTracer 的内部类 FPSCollector 统计并报告给开发者。 到此本文结束,感谢阅读。