这是性能优化系列之matrix框架的第10
篇文章,我将在性能优化专栏中对matrix apm框架做一个全面的代码分析,性能优化是Android高级工程师必知必会的点,也是面试过程中的高频题目,对性能优化感兴趣的小伙伴可以去我主页查看所有关于matrix的分享。
前言
matrix卡顿监控,下面几篇属于基础,可先行查阅,否则今天的内容读起来可能有点困难。
- Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之gradle插件- 字节码插桩代码分析
- Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之AppMethodBeat专项分析
- Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之LooperMonitor源码分析
- Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之UIThreadMonitor源码分析
有了上边几篇基础,我们分析其他类型的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就足够帮我们实现性能监控了,这是多么美好的一个期待。