跳到主要内容

设计性能监控 SDK

问题

如何设计一个 Android 性能监控 SDK,实时采集 App 的性能指标?

答案

监控维度

卡顿检测

利用 Looper 的 Printer 机制,监控主线程每条消息的处理耗时:

object JankMonitor {
private const val THRESHOLD_MS = 100 // 超过 100ms 视为卡顿

fun start() {
Looper.getMainLooper().setMessageLogging { log ->
if (log.startsWith(">>>>> Dispatching")) {
// 消息开始处理
startTime = SystemClock.elapsedRealtime()
// 延迟采集主线程堆栈(如果超时了就能拿到卡在哪里)
watchDog.postDelayed(stackDumper, THRESHOLD_MS.toLong())
}
if (log.startsWith("<<<<< Finished")) {
// 消息处理完成
watchDog.removeCallbacks(stackDumper)
val cost = SystemClock.elapsedRealtime() - startTime
if (cost > THRESHOLD_MS) {
reportJank(cost, capturedStack)
}
}
}
}
}

帧率监控

object FpsMonitor : Choreographer.FrameCallback {
private var frameCount = 0
private var lastTimestamp = 0L

fun start() {
Choreographer.getInstance().postFrameCallback(this)
}

override fun doFrame(frameTimeNanos: Long) {
if (lastTimestamp == 0L) {
lastTimestamp = frameTimeNanos
}
frameCount++
val diffMs = (frameTimeNanos - lastTimestamp) / 1_000_000
if (diffMs >= 1000) {
// 每秒计算一次 FPS
val fps = frameCount * 1000.0 / diffMs
reportFps(fps)
frameCount = 0
lastTimestamp = frameTimeNanos
}
// 注册下一帧回调
Choreographer.getInstance().postFrameCallback(this)
}
}

数据上报策略

策略说明
聚合上报本地攒批(30s 或 50 条),一次批量上报
采样正常数据 10% 采样,异常数据 100% 上报
分级P0 立即上报,P1 攒批,P2 下次启动上报
弱网降级弱网只上报关键指标,丢弃低优先级数据
序列化Protobuf 序列化 + Gzip 压缩
性能开销控制

监控 SDK 本身不能成为性能瓶颈:

  • 数据采集在子线程进行
  • 文件写入用 mmap 避免 I/O 阻塞主线程
  • 单次采集耗时控制在 1ms 以内
  • 提供全局开关,线上可随时关闭

常见面试问题

Q1: 如何获取准确的冷启动耗时?

答案

冷启动的起点在 ContentProvider.onCreate(最先执行)或 Application.attachBaseContext,终点通常取首页 Activity 第一帧渲染完成:

// 起点:在自定义 ContentProvider 中记录
class StartupProvider : ContentProvider() {
override fun onCreate(): Boolean {
StartupTracker.markProcessStart()
return true
}
}

// 终点:首页第一帧绘制完成
class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
window.decorView.post {
// ViewRootImpl 的下一帧回调,此时第一帧已上屏
StartupTracker.markFirstFrame()
}
}
}

Q2: Looper Printer 方案检测卡顿有什么局限?

答案

  • 精度不够:只能知道某条 Message 耗时长,但堆栈是延迟抓取的,可能抓到的是耗时操作之后的堆栈
  • 无法检测 InputEvent 卡顿:触摸事件走 InputChannel,不经过 MessageQueue
  • 改进方案:配合 Choreographer.FrameCallback 检测掉帧,或使用 JVMTI Agent 获取精确的方法耗时

相关链接