Android性能优化
一、卡顿优化
前言:说到卡顿我们可能正常能想到是FPS刷新率,这是一个平均值,FPS高并不代表页面流畅,比如一个页面某一贞耗时了160毫秒,但是其他都是16毫秒,那么这个页面通过FPS的数据来看体现不出来卡顿,但是实际是用户明显的感觉到了卡顿的感觉。
那么我们可以通过卡顿的帧数跟总帧数的占比来判定页面卡顿情况
卡顿率 = 卡顿的帧数 / 总帧数
加入屏幕数心率是60/s,那么每帧耗时16ms,如果有的帧数超过了16毫秒就发生了掉帧,也就是卡顿。比如某一贞耗时160毫秒,那么我们就认为掉了9帧,根据掉帧的数量可以分级为下面的情况
1、获取各帧耗时一般有以下两种方案
- 通过设置自定义android.util.Printer,监听Looper的dispatchMessage耗时
- 通过向Choreographer循环注册FrameCallback,统计两次Vsync事件时间间隔
- 谷歌提供的AndroidX系列的组件JankStats 在Android7以上实现
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
}
这里我们可以先将卡顿数据存储在内存或者本地存储中,当卡顿数量达到一定程度或者页面切换时,再统一上传卡顿数据,减少上传次数,如下所示:
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,通过设置状态我们可以统计不同场景下的卡顿率,比如滚动与非滚动。
2.如何定位卡顿问题
- 堆栈抓取方案:思路其实很简单,在卡顿发生时 Dump 主线程堆栈,通过分析堆栈找到卡顿的原因
- 字节码插桩方案:堆栈抓取方案的最大缺陷是无法获取方法的执行耗时,而字节码插桩方式可以完美解决这一问题。Matrix 插桩对于好机器的性能影响可忽略,对差机器性能稍有损耗,但影响很小。对安装包大小影响,对于微信这种大体量的应用,实际插桩函数 16w+,对安装包增加了 800K 左右。