设计性能监控 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 获取精确的方法耗时