Android卡顿如何监控?
前言
我们应该如何衡量卡顿程度?如何对 app 的卡顿程度建立数据指标? 如何定位卡顿代码,找到带来卡顿的堆栈?
卡顿率 = 卡顿的帧数 / 总帧数
具体实现
页面总帧数 卡顿的帧数 卡顿各帧的耗时
通过设置自定义android.util.Printer,监听Looper的dispatchMessage耗时 通过向Choreographer循环注册FrameCallback,统计两次Vsync事件时间间隔
class JankLoggingActivity : AppCompatActivity() {private lateinit var jankStats: JankStats
private val jankFrameListener = JankStats.OnFrameListener { frameData ->
// 在实际使用中可以将日志上传到远端统计
Log.v("JankStatsSample", frameData.toString())
}override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化 JankStats,传入 window 和卡顿回调
jankStats = JankStats.createAndTrack(window, jankFrameListener).apply {
// 支持设置卡顿阈值,默认为2
this.jankHeuristicMultiplier = 3f
}// 设置页面状态
val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
// ...
}override fun onResume() {
super.onResume()
// onResume后重新开始统计
jankStats.isTrackingEnabled = true
}override fun onPause() {
super.onPause()
// onPause后停止统计
jankStats.isTrackingEnabled = false
}
初始化 JankStats,需要传入关联的 window 和卡顿监听。 支持设置页面状态,比如当日志上报时需要了解卡顿是在哪个页面发生的,我们这里就传入了当前 Activity 的名字,在卡顿回调中可以读取。这个特点非常重要,我们可以通过这个 API 区分卡顿的场景,比如当页面发生滚动时和不滚动时设置不同的 state,就可以统计出滚动和非滚动时的卡顿率。 支持设置卡顿阈值,默认为 2,即本帧耗时大于一帧预期耗时的2倍就判定为卡顿,我们这里修改为 3。 支持开始与暂停统计,当 Activity 退到后台时可以暂时关闭统计。 JankStats 库会将每一帧的所有跟踪数据报告给已启用的 JankStats 对象的 OnFrameListener, 应用可以存储和聚合这些数据,以便日后上传。
internal class JankActivityLifecycleCallback : ActivityLifecycleCallbacks {
private val jankAggregatorMap = hashMapOf<String, JankStatsAggregator>()
// 聚合回调
private val jankReportListener = JankStatsAggregator.OnJankReportListener { reason, totalFrames, jankFrameData ->
jankFrameData.forEach { frameData ->
// 获取当前 Activity name
Log.v("Activity",frameData.states.firstOrNull { it.key == "Activity" }?.value ?: "")
// 获取掉帧数
val dropFrameCount = frameData.frameDurationUiNanos / singleFrameNanosDuration
if (dropFrameCount <= JankMonitor.SLIGHT_JANK_MULTIPIER) {
slightJankCount++
} else if (dropFrameCount <= JankMonitor.MIDDLE_JANK_MULTIPIER) {
middleJankCount++
} else if (dropFrameCount <= JankMonitor.CRITICAL_JANK_MULTIPIER) {
criticalJankCount++
} else {
frozenJankCount++
}
}
// 实际使用中可以上传到远端统计
Log.v("JankMonitor","*** Jank Report ($reason), " +
"totalFrames = $totalFrames, " + // 总帧数
"jankFrames = ${jankFrameData.size}, " + // 总卡顿数
"slightJankCount = $slightJankCount, " + // 轻微卡顿数
"middleJankCount = $middleJankCount, " + // 中等卡顿数
"criticalJankCount = $criticalJankCount, " + // 严重卡顿数
"frozenJankCount = $frozenJankCount" // 冻结帧数
)
}
}override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
// 为所有 Activity 添加卡顿监听
activity.window.callback = object : WindowCallbackWrapper(activity.window.callback) {
override fun onContentChanged() {
val activityName = activity.javaClass.simpleName
if (!jankAggregatorMap.containsKey(activityName)) {
val jankAggregator = JankStatsAggregator(activity.window, jankReportListener)
PerformanceMetricsState.getHolderForHierarchy(activity.window.decorView).state?.putState("Activity", activityName)
jankAggregatorMap[activityName] = jankAggregator
}
}
}
}// ...
}
为所有 Activity 添加了聚合的卡顿监听,当卡顿数达到阈值或者 Activity 退到后台时会触发聚合回调。 在聚合回调中可以获取这段时间的总帖数,与卡顿的帧的列表,通过计算卡顿帧的掉帧数,我们可以获取总卡顿数,轻微卡顿数,严重卡顿数等。将这些数据上传就可以计算出页面的卡顿率。 在聚合回调中我们同样可以获取页面的状态,比如我们这里设置的activityName,通过设置状态我们可以统计不同场景下的卡顿率,比如滚动与非滚动。
这里精简了部分代码,完整代码可见:android-performance/jank-optimize。
https://github.com/RicardoJiang/android-performance/tree/main/jank-optimize
堆栈抓取方案
字节码插桩方案
看起来非常优秀,可以直接用于线上,不过我也没有实践过,有使用过的同学可以在评论区交流下~
源码
本文所有源码可见: