Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之帧率监控FrameTracer源码分析

633 阅读10分钟

这是性能优化系列之matrix框架的第10篇文章,我将在性能优化专栏中对matrix apm框架做一个全面的代码分析,性能优化是Android高级工程师必知必会的点,也是面试过程中的高频题目,对性能优化感兴趣的小伙伴可以去我主页查看所有关于matrix的分享。

前言

matrix卡顿监控,下面几篇属于基础,可先行查阅,否则今天的内容读起来可能有点困难。

有了上边几篇基础,我们分析其他类型的tracer会势如破竹。言归正传,今天我们要分析的是matrix卡顿监控中的另一种tracer-FrameTracer-帧率监控。为什么要监控帧率呢?根本原因是为了保证帧率的稳定。通常来讲,Android设备大多都是60fps的帧率(当然也有90fps、120fps的),也就是画面每秒更新60次,假如应用的帧率能稳定的维持在60fps的话,对用户来讲体验是最好的。而要保证帧率维持在60fps,那么就要求每次刷新在16.66毫秒内完成。我们知道Android的刷新是基于VSYNV信号的,Android系统每隔16.66毫秒发出一次VSYNC信号,触发对UI进行渲染,VSYNC机制保证了Android的刷新频率维持在一个固定的间隔内,有利于帧率的稳定。于是FrameTracer存在的价值就是监控帧率是否稳定这一问题,也是卡顿监控中的一环。

FrameTracer是在TracePlugin中被初始化和启动,我们从它的几个关键方法入手深入源码探索。

  • 构造方法
  • onStartTrace
  • onStopTrace

构造方法

传入的supportFrameMetrics是一个boolean变量,当系统版本大于等于Android O时(8.0)为true。

public FrameTracer(TraceConfig config, boolean supportFrameMetrics) {
    useFrameMetrics = supportFrameMetrics;
    this.config = config;
    //frameIntervalNs是从Choreographer上反射获取的值,通常是16.66毫秒(这里是纳秒为单位)
    //,不过与机型的刷新率有关,默认机型就是60Hz的刷新率,所以这里等于16666667L纳秒
    this.frameIntervalNs = UIThreadMonitor.getMonitor().getFrameIntervalNanos();
    this.timeSliceMs = config.getTimeSliceMs();
    this.isFPSEnable = config.isFPSEnable();
    //下边是对帧率降低程度定义的几种类型
    this.frozenThreshold = config.getFrozenThreshold();
    this.highThreshold = config.getHighThreshold();
    this.normalThreshold = config.getNormalThreshold();
    this.middleThreshold = config.getMiddleThreshold();
    if (isFPSEnable) {
        addListener(new FPSCollector());
    }
}

onStartTrace

onStartTrace会调用到onAlive, 可以看到isFPSEnable一定要为true,FrameTracer才会生效。

这里提供了两种监控帧率的方法,一种针对8.0以下的机型,通过UIThreadMonitor的机制来实现,UIThreadMonitor的源码实现请查阅Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之UIThreadMonitor源码分析

另一种8.0及以上通过给Application注册监听来拿到Activity生命周期的回调,这样一来,应用内每一个Activity的创建或销毁就都在监控之内。这里我们只需要关注onActivityResumed和onActivityDestroyed方法。

@Override
public void onAlive() {
    super.onAlive();
    if (isFPSEnable) {
        //8.0以下的机型,通过UIThreadMonitor的机制来实现帧率的监控
        if (!useFrameMetrics) {
            UIThreadMonitor.getMonitor().addObserver(this);
        }
        Matrix.with().getApplication().registerActivityLifecycleCallbacks(this);
    }
}

我们分别来看下这两种不同的实现方式,先从8.0以下的实现看起。

8.0以下UIThreadMonitor

UIThreadMonitor通过调用addObserver将当前类添加为监听者,在Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之UIThreadMonitor源码分析中对UIThreadMonitor有专项分析,这里主要用到了它的回调方法doFrame,doFrame中会通过对Choreographer一些操作可以直接拿到应用刷新相关的参数:

  • focusedActivity:当前页面activity
  • startNs:一帧刷新的开始时间
  • endNs:一帧刷新结束的时间
  • isVsyncFrame:是否是vsync
  • intendedFrameTimeNs:vsync回调的时间
  • inputCostNs:input事件处理的时间
  • animationCostNs:动画处理的时间
  • traversalCostNs:traversal事件处理的时间

UIThreadMonitor是怎么拿到这些数据的?请查阅Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之UIThreadMonitor源码分析

在doFrame方法中,应用处于前台时,会去汇总这些参数,然后调用notifyListener,notifyListener内部逻辑见后续分析,因为8.0以上也会用到这个方法。

@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);
    }
}

8.0以上registerActivityLifecycleCallbacks

8.0以上Android系统直接暴露了相关api,addOnFrameMetricsAvailableListener,可以直接调用这个方法注册监听,去获取到相关信息(其实7.0就已经有了这个api,只是为什么matrix8.0才使用?我们暂时不关注了)。

onActivityResumed

当useFrameMetrics为true时,表示当前系统为8.0及以。这里关键的代码是addOnFrameMetricsAvailableListener,通过调用window的addOnFrameMetricsAvailableListener方法注册一个监听,从而可以收到系统的回调。

@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onActivityResumed(Activity activity) {
    if (useFrameMetrics) {
        //拿到刷新率
        this.refreshRate = (int) activity.getWindowManager().getDefaultDisplay().getRefreshRate();
        //1s除以刷新率得到的就是每帧执行的时长,如前边提到的16.66毫秒
        this.frameIntervalNs = Constants.TIME_SECOND_TO_NANO / (long) refreshRate;
        Window.OnFrameMetricsAvailableListener onFrameMetricsAvailableListener = new Window.OnFrameMetricsAvailableListener() {
            @RequiresApi(api = Build.VERSION_CODES.O)
            @Override
            public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {
                FrameMetrics frameMetricsCopy = new FrameMetrics(frameMetrics);
                //实际的vsync到来时间
                long vsynTime = frameMetricsCopy.getMetric(FrameMetrics.VSYNC_TIMESTAMP);
                //预期的vsync到来时间, 如果此值与 VSYNC_TIMESTAMP 不同,则表示 UI 线程上发生了阻塞,阻止了 UI 线程及时响应vsync信号
                long intendedVsyncTime = frameMetricsCopy.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP);
                frameMetricsCopy.getMetric(FrameMetrics.DRAW_DURATION);
                notifyListener(ProcessUILifecycleOwner.INSTANCE.getVisibleScene(), intendedVsyncTime, vsynTime, true, intendedVsyncTime, 0, 0, 0);
            }
        };
        activity.getWindow().addOnFrameMetricsAvailableListener(onFrameMetricsAvailableListener, new Handler());
    }
}

这里是取了VSYNC_TIMESTAMP和INTENDED_VSYNC_TIMESTAMP两个值,INTENDED_VSYNC_TIMESTAMP表示该帧的 vsync 信号预期应该发出时间,VSYNC_TIMESTAMP是该帧的vsync信号实际发出时间的时间,二者的差值就可以看作中间卡顿发生的时间。

其实通过这个接口还可以获取更多信息,我们看下FrameMetrics中定义的几种信息类型:

//处理输入事件花费的时间, 单位纳秒
public static final int INPUT_HANDLING_DURATION = 1;
//处理动画执行花费的时间, 单位纳秒
public static final int ANIMATION_DURATION = 2;
//measure和layout一共花费的时间
public static final int LAYOUT_MEASURE_DURATION = 3;
//draw绘制花费的时间
public static final int DRAW_DURATION = 4;
//DisplayLists与显示线程同步花费的时间
public static final int SYNC_DURATION = 5;
//向 GPU 发送绘制命令花费的时间
public static final int COMMAND_ISSUE_DURATION = 6;
//帧缓冲区交换花费的时间
public static final int SWAP_BUFFERS_DURATION = 7;
//所有操作总共花费的时间
public static final int TOTAL_DURATION = 8;
//是否是第一帧
public static final int FIRST_DRAW_FRAME = 9;
//预期vsync信号发出的时间
public static final int INTENDED_VSYNC_TIMESTAMP = 10;
//实际的vsync信号发出的时间
public static final int VSYNC_TIMESTAMP = 11;
//GPU计算花费的时间
public static final int GPU_DURATION = 12;

所以8.0以上的处理逻辑就是通过拿到FrameMetrics对象,FrameMetrics对象中储存了各种时间信息,这里取出关键VSYNC_TIMESTAMP和INTENDED_VSYNC_TIMESTAMP两个关键信息之后,就进入了notifyListener方法,可见8.0及以上和8.0以下只是获取时间长度的方式不同,最终都是进入了notifyListener方法,殊途同归。

onActivityDestroyed

移除FrameMetricsAvailableListener

public void onActivityDestroyed(Activity activity) {
    if (useFrameMetrics) {
        activity.getWindow().removeOnFrameMetricsAvailableListener(frameListenerMap.remove(activity.hashCode()));
    }
}

后续

通过上边两项的分析之后我们已经拿到了帧绘制相关的时间信息,拿到信息后进行了什么处理呢,这里浏览一下后续的实现细节。

notifyListener

第一步,假如设置了掉帧监听接口,则根据两次时间间隔计算出掉帧的数量,当数量超过设定值时回调dropFrame。

//根据两次时间间隔计算出掉帧的数量
final long jitter = endNs - intendedFrameTimeNs;
final int dropFrame = (int) (jitter / frameIntervalNs);
if (dropFrameListener != null) {
    if (dropFrame > dropFrameListenerThreshold) {
        if (MatrixUtil.getTopActivityName() != null) {
            //当数量超过设定值时回调dropFrame
            dropFrameListener.dropFrame(dropFrame, jitter, MatrixUtil.getTopActivityName(), lastResumeTime);
        }
    }
}

第二步,执行这里

if (null != listener.getExecutor()) {
    //FrameTracer中默认值为300,所以一定是满足大于0的
    if (listener.getIntervalFrameReplay() > 0) {
        listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
    } 
}

collect

第三步,调用collect方法收集信息。

@CallSuper
public void collect(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame,
                    long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    FrameReplay replay = FrameReplay.create();
    //当前可见的Activity
    replay.focusedActivity = focusedActivity;
    replay.startNs = startNs;
    replay.endNs = endNs;
    //丢帧数
    replay.dropFrame = dropFrame;
    //是否是vsync
    replay.isVsyncFrame = isVsyncFrame;
    //vsync预期到达时间
    replay.intendedFrameTimeNs = intendedFrameTimeNs;
    //输入事件处理时间,这里为0
    replay.inputCostNs = inputCostNs;
    //动画事件处理时间,这里为0
    replay.animationCostNs = animationCostNs;
    //traversal事件处理时间,这里为0
    replay.traversalCostNs = traversalCostNs;
    list.add(replay);
    //intervalFrame默认为300,也就是默认收集300条后doReplay一次
    if (list.size() >= intervalFrame && getExecutor() != null) {
        final List<FrameReplay> copy = new LinkedList<>(list);
        list.clear();
        getExecutor().execute(new Runnable() {
            @Override
            public void run() {
                doReplay(copy);
                for (FrameReplay record : copy) {
                    record.recycle();
                }
            }
        });
    }
}

doReplay

第四步,遍历这300条数据,对每个数据执行doReplayInner。

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

doReplayInner

第五步,visibleScene表示当前可见页面,以visibleScene为key,将visibleScene相同的数据封装到到一个FrameCollectItem对象中,并存入map, 调用collect。

public void doReplayInner(String visibleScene, long startNs, long endNs, int droppedFrames,
                          boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs,
                          long animationCostNs, long traversalCostNs) {
    FrameCollectItem item = map.get(visibleScene);
    if (null == item) {
        item = new FrameCollectItem(visibleScene);
        map.put(visibleScene, item);
    }
    item.collect(droppedFrames);
    if (item.sumFrameCost >= timeSliceMs) { // report
        map.remove(visibleScene);
        item.report();
    }
}

collect

第六步,collect方法中将丢帧的情况按照丢帧数量等级做了划分,分别为:

  • frozen级别-丢帧大于42
  • high级别-丢帧介于24到42
  • middle级别-丢帧介于9到24
  • mormal级别-丢帧介于3到9
  • best级别-丢帧小于3
void collect(int droppedFrames) {
    //frameIntervalNs表示每帧之间的时间间隔,如在60hz刷新率的机型上为16.66毫秒,以纳秒
    //为单位表示则为16666666纳秒,TIME_MILLIS_TO_NANO是1毫秒用纳秒来表示的数字,则就是
    //1000000纳秒,所以最终计算得到frameIntervalCost为16.66
    float frameIntervalCost = 1f * FrameTracer.this.frameIntervalNs
            / Constants.TIME_MILLIS_TO_NANO;
    //根据丢掉的帧数乘以每帧的时长,得到的就是卡顿的总时长,以毫秒为单位
    sumFrameCost += (droppedFrames + 1) * frameIntervalCost;
    sumDroppedFrames += droppedFrames;
    sumFrame++;
    //如果丢帧数大于42,则认为丢帧度为“frozen”
    if (droppedFrames >= frozenThreshold) {
        dropLevel[DropStatus.DROPPED_FROZEN.index]++;
        dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames;
    } 
    //如果丢帧数大于24但是小于42,则认为丢帧度为“high”
    else if (droppedFrames >= highThreshold) {
        dropLevel[DropStatus.DROPPED_HIGH.index]++;
        dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames;
    } 
    //如果丢帧数大于9但是小于24,则认为丢帧度为“middle”
    else if (droppedFrames >= middleThreshold) {
        dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
        dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames;
    } 
    //如果丢帧数大于3但是小于9,则认为丢帧度为“normal”
    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);
    }
}

第七步,当丢帧导致的卡顿时长超过timeSliceMs(默认10s)时,报告卡顿问题。

if (item.sumFrameCost >= timeSliceMs) { 
    map.remove(visibleScene);
    item.report();
}

report

简单看下报告的参数

void report() {
    float fps = Math.min(refreshRate, 1000.f * sumFrame / sumFrameCost);
    resultObject = DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication());
    //丢帧场景
    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);
}

看一下打印的示例:

{
	"machine": "BAD",
	"cpu_app": 0,
	"mem": 1495580672,
	"mem_free": 654600,
	"scene": "sample.tencent.matrix.issue.IssuesListActivity",
	"dropLevel": {
		"DROPPED_FROZEN": 1,
		"DROPPED_HIGH": 0,
		"DROPPED_MIDDLE": 1,
		"DROPPED_NORMAL": 0,
		"DROPPED_BEST": 0
	},
	"dropSum": {
		"DROPPED_FROZEN": 2738,
		"DROPPED_HIGH": 0,
		"DROPPED_MIDDLE": 10,
		"DROPPED_NORMAL": 0,
		"DROPPED_BEST": 0
	},
	"fps": 0.04363667964935303
}

至此,帧率信息监听的代码就分析完了。

onStopTrace

主要是资源清理操作。

public void onDead() {
    super.onDead();
    removeDropFrameListener();
    if (isFPSEnable) {
        UIThreadMonitor.getMonitor().removeObserver(this);
        Matrix.with().getApplication().unregisterActivityLifecycleCallbacks(this);
    }
}

总结

通过今天的分析,我们了解到FrameTracer进行帧率监听分为两种方式。以Android 8.0为分界线,8.0以下通过UIThreadMonitor实现,通过对主线程消息队列中消息的执行前后进行监听,计算出一帧画面执行过程中不同类型消息的执行的时间,然后进行计算分析,达到监听帧率的效果;8.0及以上是通过Android系统提供的addOnFrameMetricsAvailableListener方法实现数据的获取,addOnFrameMetricsAvailableListener注册监听之后,可以拿到丰富的时间信息为我所用,从而使监听帧率的实现更加的方便快捷,可以想象一下,假如未来某天,Android开发适配的最低机型为7.0时,那么使用系统api就足够帮我们实现性能监控了,这是多么美好的一个期待。