作者:天才木木木木
0 介绍
要监控应用界面是否发生卡顿,需要先了解一下Android应用主线程的渲染机制:
Android 系统提供一个稳定的帧率输出机制,让软件层和硬件层可以以共同的频率一起工作,使我们可以享受稳定帧率的画面。
大部分手机的屏幕都是60Hz的刷新率,系统为了配合屏幕的刷新频率,每过16.6ms就会发出Vsync信号来通知应用进行绘制。如果每个Vsync周期应用都能完成渲染逻辑,那么应用的FPS就是60,给用户的感觉就是非常流畅。
在应用层,实现上述机制的关键类就是Choreographer
。每隔16.6ms,Vsync 信号唤醒 Choreographer来做应用的绘制操作。
想要监控卡顿或者是监测App的流畅度,就必须通过代码手段来获取FPS或者每帧耗时,并转化成可以衡量应用卡顿程度的指标。而几乎所有的卡顿监控方案都离不开Choreographer
这个类。所以先简单说说Choreographer
:
Choreographer
在应用层就是通过Choreographer来接受VSync信号并执行每一帧的渲染逻辑。
每当Vsync到来时,会往主线程消息队列里添加一个Message,最终其doFrame函数将被调用:
//Choreographer.java
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
......
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
public void run() {
......
doFrame(mTimestampNanos, mFrame);
}
doFrame函数中执行了应用层的callback,基本上包含了一帧的渲染工作:
//Choreographer.java
void doFrame(long frameTimeNanos, int frame) {
//自带了掉帧计算
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
......
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
......
}
其中CALLBACK_ANIMATION是处理动画相关的逻辑,而CALLBACK_TRAVERSAL则会调用到ViewRootImpl的performTraversals() 函数,从而执行到我们所熟悉的View的measure、layout、draw三大流程。
所以可以说,doFrame() 函数包含了应用层绘制一帧的逻辑处理。
由于Choreographer处于如此重要的一个位置,基本上所有的卡顿监控都会围绕着Choreographer进行的,除了自带的掉帧计算,Choreographer 提供的 FrameCallback 和 FrameInfo都是应用层可以直接访问的接口。
下面介绍一下市面上开源方案的几种实现方式和简单对比。
1 Choreographer的FrameCallback
TinyDancer 就是通过这种方式计算出FPS。 核心代码:
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (lastFrameTimeNanos > 0) {
val frameTime = (frameTimeNanos - lastFrameTimeNanos) / NANOS_PER_MS
}
lastFrameTimeNanos = frameTimeNanos
Choreographer.getInstance().postFrameCallback(this)
}
})
通过记录doFrame回调的间隔时间作为每帧耗时frameTime,如此也很容易计算出FPS=1000/frameTime,掉帧数=frameTime/16.6。
2 Choreographer + Looper
Tencent/matrix 虽然也是基于Choreographer,但其监测FPS的机制和上面的FrameCallback方案不太一样。 由前面Choreographer的介绍可知,所谓每一帧其实指的就是input、animation、traversal三种事件对应的三个doCallback方法的执行结果,而matrix统计帧耗时就是通过监测这三个方法的执行总时间来表示。matrix监测FPS的主要实现在LooperMonitor
和UIThreadMoniter
两个类里。
LooperMonitor
为主线程Looper设置一个Printer来监听UI线程每个Message的开始、结束,从而得到Message的执行耗时。(方案同 BlockCanary )
class LooperPrinter implements Printer {
@Override
public void println(String x) {
......
dispatch(x.charAt(0) == '>', x);
}
private void dispatch(boolean isBegin, String log) {
for (LooperDispatchListener listener : listeners) {
if (isBegin) {
listener.onDispatchStart(log);
} else {
listener.onDispatchEnd(log);
}
}
}
}
UIThreadMoniter
通过java反射向Choreographer的CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL三个事件的callback队列的头部插入自定义callback。
```java
//UIThreadMonitor.java
@Override
public void run() {
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);
}
前面提到Choreographer收到VSync信号时,也是往主线程消息队列里放入一个Message最终触发doFrame。而LooperMonitor监控了每个Message执行的开始/结束,如果UIThreadMonitor的doFrameBegin
被执行,则说明当前在 Looper 中正在执行的消息就是渲染的消息。然后再在Message结束的时候作为当前帧绘制的结束。这个整个一帧的监控就闭环了。
所以在Matrix中,完整的一帧耗时是onDispatchStart -> doFrame -> onDispatchEnd
。
因为UIThreadMonitor是在Choreographer的callback队列的头部添加callback,所以可用于记录每种类型callback队列执行的开始时间,在队列里的callback都执行完毕后,就可以计算对应的事件的耗时(CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL),比如CALLBACK_INPUT耗时
= CALLBACK_ANIMATION开始时间
- CALLBACK_INPUT开始时间
。
所以Matrix不仅可以得到每一帧的耗时,还能进一步得到每一帧内触摸事件、动画、View渲染三种事件的耗时情况。
3 官方方案——JankStats
JankStats —— 是官方推出的Jetpack套件中的一个新库,最早发布于2022年2月,该库提供了在运行时获取界面的每帧性能的回调,可以让开发者监测到性能问题及其发生的原因。
项目中依赖:
dependencies {
implementation "androidx.metrics:metrics-performance:1.0.0-alpha03"
}
简单使用:
val listener = JankStats.OnFrameListener { frameData ->
// A real app could do something more interesting, like writing the info to local storage and later on report it.
Log.v("JankStatsSample", frameData.toString())
}
jankStats = JankStats.createAndTrack(window, listener)
jankStats.isTrackingEnabled = true
简单几行代码就可以在OnFrameListener回调中获取每一帧的性能数据FrameData, 同时JankStats库还提供了PerformanceMetricsState供开发者记录当前的界面状态,并通过FrameData回调出来,有助于了解用户在那一帧期间做了什么交互。
open class FrameData(
frameStartNanos: Long,//当前帧开始时间
frameDurationUiNanos: Long,//当前帧耗时
isJank: Boolean,//是否发生了卡顿
val states: List<StateInfo>//业务代码记录的UI状态,可以通过PerformanceMetricsState在关键业务代码处埋点
)
原理
JankStats是一个典型的Android X库:在不同的Android版本和不同的设备上,实现行为一致的框架。
class JankStats private constructor(window: Window, private val frameListener: OnFrameListener) {
init {
val decorView: View? = window.peekDecorView()
implementation =
when {
Build.VERSION.SDK_INT >= 31 -> {
JankStatsApi31Impl(this, decorView, window)
}
Build.VERSION.SDK_INT >= 26 -> {
JankStatsApi26Impl(this, decorView, window)
}
Build.VERSION.SDK_INT >= 24 -> {
JankStatsApi24Impl(this, decorView, window)
}
Build.VERSION.SDK_INT >= 22 -> {
JankStatsApi22Impl(this, decorView)
}
Build.VERSION.SDK_INT >= 16 -> {
JankStatsApi16Impl(this, decorView)
}
else -> {
JankStatsBaseImpl(this)
}
}
}
}
阅读源码可以发现监测机制在API 24(7.0)前后有差异:
Android7.0及其以上系统,直接通过 Window 的新方法addOnFrameMetricsAvailableListener
,监听回调每一帧的详细数据FrameMetrics:
internal open class JankStatsApi24Impl() {
@RequiresApi(24)
private fun Window.getOrCreateFrameMetricsListenerDelegator():
DelegatingFrameMetricsListener {
.....
val delegates = mutableListOf<Window.OnFrameMetricsAvailableListener>()
delegator = DelegatingFrameMetricsListener(delegates)
//Window的这个方法可以监听每一帧的详细数据
addOnFrameMetricsAvailableListener(delegator, frameMetricsHandler)
}
return delegator
}
}
Android7.0以下设备,还是需要基于Choreographer,通过反射 Choreographer 的 mLastFrameTimeNanos 来获取当前帧的起始时间,然后通过 ViewTreeObserver的 OnPreDrawListener来感知绘制的开始,并往主线程的消息队列头部插入runnable来获取当前帧的UI线程绘制任务结束时间。
//JankStatsApi16Impl.kt
internal open class DelegatingOnPreDrawListener() : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
val frameStart = getFrameStartTime()
with(decorView) {
//往主线程的消息队列头部插入runnable
handler.sendMessageAtFrontOfQueue(Message.obtain(handler) {
val now = System.nanoTime()
val frameTime = now - frameStart//获得每帧绘制的真实耗时
})
}
return true
}
//反射 Choreographer 的 mLastFrameTimeNanos
private fun getFrameStartTime(): Long {
return choreographerLastFrameTimeField.get(choreographer) as Long
}
}
获取到了当前帧的起始时间和绘制的Message结束时间,则可以计算出每帧绘制的真实耗时frameTime = now - frameStart
方案对比
- Choreographer的FrameCallback
- 优点:
- 简单可靠,维护成本低,使用稳定性高的系统开放API
- 缺点:
- 调用postFrameCallback()会不断地请求Vsync(
scheduleVsyncLocked()
),当界面静止时,UI线程也会不断请求VSync信号。主线程不干活的时候也会监测,可能会把有卡顿的场景的数据给稀释掉了。 - 获取到的帧耗时并不是真实的每帧绘制耗时,而获取到≥16ms的两次doFrame的时间间隔。
- 调用postFrameCallback()会不断地请求Vsync(
- 适用场景:
- 比较粗粒度的掉帧数、卡顿监控。
- 优点:
- Matrix(Choreographer + Looper)
- 优点:
- 可以对UI线程绘制的不同阶段耗时进行较细致的监控
- 不需要postFrameCallback,避免不断请求VSync信号
- 缺点:
- 维护成本高,对系统api侵入式较强,大量地通过反射来hook Choreographer和Looper,容易出现兼容问题,高版本系统可能会失效
- 只统计了Choreographer的callback队列执行的耗时,即UI操作相关的耗时,没有包含两个VSYNC之间产生的其它非UI操作相关的message的耗时,因此统计出来的帧耗时可能偏低。
- 适用场景:
- 比较精细化的卡顿监控
- 优点:
- JankStats
- 优点:
- 官方方案,兼容性好,使用简单,可靠稳定。是在系统源码里进行埋点监测并收集数据。
- 数据详尽,FrameMetrics提供了第三方框架难以采集的详细数据,包含总耗时,input、layout&measure、draw甚至是 sync bitmap到GPU的耗时等等(据说和adb shell dumpsys gfxinfo和GPU呈现模式分析的数据一致)
- 使用便捷,内置规则判断当前帧是否卡顿帧,并可以记录当前帧的应用状态。
- 界面停止绘制时,不再生产帧率数据,避免脏数据。
- 缺点:
- 不支持个性化的定制需求。
- 库还只是alpha版本,不是成熟的release版本,如果有坑则只能等官方更新。
- 集成到老项目时不友好,可能需要升级AndroidX核心库、kotlin插件甚至gradle插件版本,代价大。
- 适用场景:
- 新项目,需要快速实现卡顿监控
- 优点:
结论
上面三种方案都可以获得当前应用的FPS,要基于项目情况去考虑方案选型。大型项目一般更倾向于维护成本低、稳定性高的基于系统开放API的方案,所以Choreographer的FrameCallback应该是使用最广泛的方案了。
采集到FPS只是应用卡顿监控的第一步,还需要制定科学的卡顿率指标。因为FPS并不能直观的反映应用的卡顿情况,还需要考虑“视觉惯性”,比如电影帧率仅24FPS也不觉得卡顿,但是应用如果一会30FPS一会60FPS,视觉上就会觉得很不流畅。
为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的学习路线以及核心笔记(还该底层逻辑):https://qr18.cn/FVlo89
性能优化核心笔记:https://qr18.cn/FVlo89
启动优化
内存优化
UI优化
网络优化
Bitmap优化与图片压缩优化:https://qr18.cn/FVlo89
多线程并发优化与数据传输效率优化
体积包优化
《Android 性能监控框架》:https://qr18.cn/FVlo89
《Android Framework学习手册》:https://qr18.cn/AQpN4J
- 开机Init 进程
- 开机启动 Zygote 进程
- 开机启动 SystemServer 进程
- Binder 驱动
- AMS 的启动过程
- PMS 的启动过程
- Launcher 的启动过程
- Android 四大组件
- Android 系统服务 - Input 事件的分发过程
- Android 底层渲染 - 屏幕刷新机制源码分析
- Android 源码分析实战