Android 腾讯 Matrix 原理分析(四):TracePlugin 卡顿分析之丢帧展现

前言

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

一、效果预览

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


Demo[图片上传中...(2.png-4ee64a-1612682706410-0)]

1.1 注意的点

从上面的 Demo 中可以看出:

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

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

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

1.2 使用步骤

  1. 准备需要检测的帧率的 Activity 或者任何地方,通常是一些 View 比较复杂、涉及计算较多的展示页面。

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

mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        MatrixLog.i(TAG, "onTouch=" + motionEvent);
        SystemClock.sleep(80);
        return false;
    }
});

  1. 展示帧率 View,类 FrameDecorator 负责接收数据和展示帧率图,通过 FrameDecorator 展示右上角的 View。
FrameDecorator decorator = FrameDecorator.getInstance(this);
// 检测浮窗权限
if (!canDrawOverlays()) {
    requestWindowPermission();
} else {
    decorator.show();
}

请求浮窗权限: 帧率统计图是一个自定义 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;
    }
}
  1. 经过上面的步骤已经展示出帧率 View,但是目前只是展示出帧率并没有搜集数据,接下来可以在需要的地方开启帧率分析和报告。
Matrix.with().getPluginByClass(TracePlugin.class).getFrameTracer().onStartTrace();

开始收集数据需要先获取 TracePlugin 卡顿分析插件,接着获取 FrameTracer 并调用它的 onStartTrace() 方法开始统计。

  1. 帧率上报。

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

帧率上报

仔细观察帧率统计图,发现滑动时帧数大幅下降。说明该页面在主线程做了太多事情,需要考虑进行优化。

二、帧率数据从哪来?

先说结论,帧率数据从 UIThreadMonitor 来。

回调过程.png

整体逻辑也比较简单:UIThreadMonitor 会监听系统垂直同步信号并维护监听者列表 observers,FrameTracer 开始工作后添加监听到列表 observers,等 UIThreadMonitor 接收到信号后遍历回调列表告知即可。

接下来我们根据上图把该顺序的代码剖析一下。

2.1 UIThreadMonitor 监听垂直同步信号

UIThreadMonitor 的工作原理在上篇文章中已经详细分析过,在此简单记录:

  1. UIThreadMonitor 内部维护 Choreographer 实例,该对象用来接收系统 VSync 信号;
  2. UIThreadMonitor 向 Choreographer 添加帧率回调监听,这样 Choreographer 接收到系统信号时会通知 UIThreadMonitor;
  3. UIThreadMonitor 再遍历回调给自己内部维护的列表。

实际的逻辑要比这三步复杂的多,感兴趣的可以回去看之前的文章:

Android 腾讯 Matrix 原理分析(三):TracePlugin 卡顿分析之帧率监听

2.2 FrameTracer 启动和监听

  1. FrameTracer 属于 卡顿分析插件 TracePlugin 的一部分,所以也是由 TracePlugin 启动的:

TracePlugin # start()

@Override
public void start() {
    super.start();
    ...
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            ...
            frameTracer.onStartTrace();
            ...
        }
    };
    // 主线程启动
    if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
        runnable.run();
    } else {
        MatrixLog.w(TAG, "start TracePlugin in Thread[%s] but not in mainThread!", Thread.currentThread().getId());
        MatrixHandlerThread.getDefaultMainHandler().post(runnable);
    }

}
  1. FrameTracer 的 onStartTrace() 方法会调用自己的 onAlive() 方法,而后会添加监听到 UIThreadMonitor。

FrameTracer # onAlive()

@Override
public void onAlive() {
    super.onAlive();
    UIThreadMonitor.getMonitor().addObserver(this);
}

需要注意的是添加的监听类型是 LooperObserver,也就是说只有继承了 LooperObserver 类才能被添加到 UIThreadMonitor 维护的监听者列表

2.3 UIThreadMonitor 回调监听

UIThreadMonitor 在监听主线程帧率事件后遍历回调:

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() 方法,而 FrameTracer 的 doFrame() 方法会将数据设置给帧率 View。

三、设置数据给帧率 View

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

数据传递流程

整个过程与上一章节类似,无法就是添加监听等待回调,然后将数据给到 View 更新 UI。接下来逐步分析:

3.1 FrameTracer 监听列表

  1. FrameTracer 从 UIThreadMonitor 中得知帧率信息,这个过程不再赘述。
  2. FrameTracer 内部维护一个 IDoFrameListener 类型的列表,用来存储监听者列表:
private final HashSet<IDoFrameListener> listeners = new HashSet<>();

IDoFrameListener 虽然叫 Listener 但其实是一个类,内部用 LinkedList 添加每帧数据、使用 doFrameAsync() 方法执行监听回调。

3.2 FrameDecorator 创建和添加监听

  1. FrameDecorator 由开发者手动创建,它是一个单例:
FrameDecorator decorator = FrameDecorator.getInstance(this);
  1. FrameDecorator 的获取单例方法中会创建帧率 View FloatFrameView,并且在构造函数中添加监听到 FrameTracer。

FrameDecorator # getInstance()

public static FrameDecorator getInstance(final Context context) {
    if (instance == null) {
        // 主线程直接创建
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            // 这里创建的 FloatFrameView
            instance = new FrameDecorator(context, new FloatFrameView(context));
        } else {
            try {
                // 子线程同步锁创建
                synchronized (lock) {
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                             // 这里创建的 FloatFrameView
                            instance = new FrameDecorator(context, new FloatFrameView(context));
                            synchronized (lock) {
                                lock.notifyAll();
                            }
                        }
                    });
                    lock.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    return instance;
}
  • 单例为 null 且在主线程则直接创建;
  • 有意思的是子线程创建的逻辑,这里同时用到了同步锁 synchronized 和对象锁,鉴于篇幅在此不作分析,感兴趣的朋友可以解析并分享一下。
  1. FrameDecorator 构造器中,会在 FloatFrameView attach 到 Window 的时候将 FrameDecorator 添加到 FrameTracer 的监听列表中:
private FrameDecorator(Context context, final FloatFrameView view) {
    ...
    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);
                }
            }
        }
...
    });
    ...
}

3.3 FrameTracer 收到帧率回调

  1. FrameTracer 用 doFrame() 方法接收 UIThreadMonitor 发来的帧率数据,然后再遍历自己维护的 listeners 列表,上面步骤提到过 FrameDecorator 实例也在这个列表中。

FrameTracer # doFrame

private final HashSet<IDoFrameListener> listeners = new HashSet<>();

@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);
    }
}
  1. notifyListener() 中遍历通知所有监听。

FrameTracer # 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 {
          ...
          synchronized (listeners) {
              for (final IDoFrameListener listener : listeners) {
                  ...
                  listener.getExecutor().execute(new Runnable() {
                          @Override
                          public void run() {
                              // 执行这里
                              listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                      intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                          }
                      });
               ...
              }
          }
      }
      ...
  }

可以看到主要调用监听者们的 doFrameAsync() 方法,这样就又回到了 FrameDecorator 中。

3.4 FrameDecorator 更新 View

上一小节中来到了 FrameDecorator 的 doFrameAsync() 方法,该方法负责将数据传递给帧率 View。

  1. 经过一番计算,调用 updateView() 方法:

FrameDecorator # doFrameAsync()

@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);
    ...
    long collectFrame = sumFrames - lastFrames[0];
    if (duration >= 200) {
        final float fps = Math.min(maxFps, 1000.f * collectFrame / duration);
        // 使用该方法更新 View
        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]);
         ...
    }
}

dropLevelsumDropLevel 数组暂时先不看,把调用链搞清楚再去分析。

  1. 接下来到了 updateView() 方法:

FrameDecorator # updateView

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) {

      ...
      // 切换到主线程
      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.sumQiWangView.setText(sumQiWangStr);
              ...
          }
      });
  }

注意两条注释:

  • 切换到主线程:因为需要进行 ui 的更新,所以到主线程执行;
  • 展示数据:将传递来的数据设置给帧率 View FloatFrameView。

到这里数据传递的流程已经基本理清了,接下来分析帧率的数值是如何计算出来的。

3.5 丢帧报告

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

帧率报告

主要看报告 json 中的部分内容:

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

丢帧数量属于的等级:

Best Normal Middle High Frozen
[0:3) [3:9) [9:24) [24:42) [42:∞)
  • dropLevel: 掉帧统计;

关于卡顿官方文档是这么解释的:

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

所以 Matrix 使用 dropLevel 来统计一段时间内的丢帧程度。打个比方,如果这段时间丢帧等级基本在 DROPPED_BEST(发生了丢帧,但是丢的数量在 3 以下),那么属于比较完美的情况无需优化。

而 Demo 中 :

  • DROPPED_MIDDLE(中度丢帧) 发生 24 次,所丢帧数 281;
    按照每秒 60 帧来计算,中度丢帧发生了将近 3s。
  • DROPPED_NORMAL(普通丢帧) 发生 31 次,所丢帧数 146;
    普通丢帧发生了 2s 多。
  • DROPPED_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.... 纳秒刷新一次。

  1. 获取 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 说明不会丢帧。

总结

最后简单总结下:

  1. 帧率数据从 UIThreadMonitor 来,通过监听和回调的方式告知 FrameTracer;
  2. FrameDecorator 负责接收数据和管理帧率 View,通过设置监听给 FrameTracer 接收帧率信息;
  3. 丢帧分为五个等级,FrameTracer 会统计丢帧的次数和所丢的帧数;
  4. 丢帧信息由 FrameTracer 的内部类 FPSCollector 统计并报告给开发者。

到此本文结束,感谢阅读。

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