前言
ANR 是"卡到系统杀 App"的级别,普通卡顿(jank)用户能感知到但不至于崩溃。Choreographer 是 Android 渲染系统的核心,本篇围绕它拆解卡顿的本质、FPS 监测方案以及线上监控的实现。
一、Choreographer 是 Android 渲染的心脏
Android 显示屏每 16.6ms(60Hz)会发出一次 VSYNC 信号。Choreographer 在 VSYNC 到达时调度一次"渲染周期",把 Input、Animation、Traversal、Commit 这四种任务打包一次完成。
VSYNC 到达
↓
Choreographer.doSchedule(VSYNC)
↓
① 处理 InputEvent(按键、触摸)
② 处理 Animation(动画)
③ 处理 Traversal(measure/layout/draw)
④ 提交给 RenderThread
↓
SurfaceFlinger 合成图像送显示
只要任务 ③ 在 16.6ms 内没完成,就掉帧(drop frame / jank)。
二、为什么会出现掉帧
掉帧的根因是单帧任务 > 16ms。常见情况:
三、用 Choreographer 监测掉帧
3.1 FrameCallback 监测方法
class JankDetector {
private val choreographer = Choreographer.getInstance()
private var lastFrameTimeNanos = 0L
private val jankThreshold = 50L // 50ms 是用户能感知
fun start() {
choreographer.postFrameCallback(frameCallback)
}
private val frameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (lastFrameTimeNanos != 0L) {
val frameTime = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000L // ns → ms
if (frameTime >= jankThreshold) {
onJankDetected(frameTimeNanos, frameTime)
}
}
lastFrameTimeNanos = frameTimeNanos
choreographer.postFrameCallback(this)
}
}
private fun onJankDetected(frameTimeNanos: Long, frameTime: Long) {
val stackTrace = Looper.getMainLooper().thread.stackTrace
val sb = StringBuilder()
stackTrace.forEach { sb.append("\tat $it\n") }
JankReporter.report(stackTrace = sb.toString(),
frameTimeNanos = frameTimeNanos,
frameTimeMs = frameTime)
}
}
这是业界最经典的方案——一行 postFrameCallback 就能监控每一帧。
3.2 上采样与轻量判断
不需要每一帧都处理——可以只对"延后超阈值"做记录:
val isOverThreshold = (frameTimeNanos - lastTime) > EXPECTED_NS * 2
if (isOverThreshold) jankReport()
两倍正常时间即记录为 jank,性能开销可忽略。
四、FPS 计算与滑动监测
4.1 滑动 FPS
用户在滑动页面时,FPS 应稳定在 60Hz。Sliding FPS 计算:
class SlidingFpsMonitor {
private val frameTimes = LinkedList<Long>()
private val maxFrames = 60
fun onFrame(time: Long) {
frameTimes.addLast(time)
while (frameTimes.size > maxFrames) frameTimes.removeFirst()
}
fun currentFps(): Float {
if (frameTimes.size < 2) return 0f
val span = frameTimes.last - frameTimes.first
if (span == 0L) return 0f
return (frameTimes.size - 1) * 1_000_000_000f / span
}
}
4.2 直接的 fps 接口
Android 内部有 SurfaceFlinger 接口,但需要系统权限。更好的方法:
class FpsMeter {
fun getFps(): Float {
val window = (context.getSystemService(WINDOW_SERVICE) as WindowManager)
.defaultDisplay
return window.refreshRate
}
}
这是屏幕刷新率上限,实际渲染 fps 需要采样。
五、用 systrace / Perfetto 找具体栈
5.1 systrace 抓取
adb shell atrace --async_start -t 30 -a com.example.app view gfx sched
# 30s 后自动停
adb shell atrace --async_stop
adb pull /sdcard/trace.html .
5.2 Perfetto 抓取(更现代)
# /data/local/tmp/perfetto_config.txt
cat > /tmp/perfetto_config.txt << EOF
duration_ms: 30000
buffers:
size_kb: 65536
fill_policy: discard
data_sources:
- config:
name: "android.gpu.memory"
producer_config:
sampling_interval_ms: 100
- config:
name: "android.processes"
producer_config:
process_priority: high
- config:
name: "android.surfaceflinger"
EOF
adb push /tmp/perfetto_config.txt /data/local/tmp/
adb shell perfetto --config /data/local/tmp/perfetto_config.txt \
--out /data/local/tmp/perfetto.trace
adb pull /data/local/tmp/perfetto.trace .
用 Perfetto UI(https://ui.perfetto.dev/)打开[1] .trace 文件,能看到 Frame 视图:
Frame #123: 65.4ms
├─ UI Thread:
│ ├─ performMeasure: 22ms
│ └─ performDraw: 38ms
├─ RenderThread:
│ ├─ Issue draw commands: 10ms
直接看到是哪一步拖慢。
六、Profile GPU 渲染
开发者选项里有一个"Profile GPU Rendering"开关,开启后在屏幕上画出柱状图:
每根柱子代表一帧
高度就是渲染耗时
绿色线是 16ms 基准
越线就是掉帧
实际工程中:
这个工具对于现场验证(用户排查、QA 测试)非常有用。
七、FPS 在 Compose 中的处理
Compose 的渲染和 View 系统不同:
@Composable
fun HeavyScreen() {
var state by remember { mutableStateOf(0) }
// 错误:在 Composable 中执行耗时操作
val data = computeExpensive(state) // 主线程运算 100ms
Column { ... }
}
// 正确:使用 LaunchedEffect + State
@Composable
fun HeavyScreen() {
var state by remember { mutableStateOf(0) }
var data by remember { mutableStateOf(emptyList<Item>()) }
LaunchedEffect(state) {
data = withContext(Dispatchers.Default) {
computeExpensive(state)
}
}
Column { ... }
}
Compose 的 key 是"不要在重组中跑重活"。
八、线上监控方案
8.1 自建 FPS 监控(简化版)
class OnlineFpsMonitor {
private val choreographer = Choreographer.getInstance()
private var lastFrameTime = 0L
private var frameCount = 0
private var timerStart = 0L
private val handler = Handler(Looper.getMainLooper())
private val checkInterval = 5000L
fun start() {
timerStart = SystemClock.uptimeMillis()
handler.postDelayed(::report, checkInterval)
choreographer.postFrameCallback(frameCallback)
}
private val frameCallback = object : Choreographer.FrameCallback {
override fun doFrame(time: Long) {
if (lastFrameTime == 0L) lastFrameTime = time
frameCount++
choreographer.postFrameCallback(this)
}
}
private fun report() {
val duration = SystemClock.uptimeMillis() - timerStart
val avgFps = frameCount * 1000f / duration
// 上报 avgFps 到 APM
DeviceMetrics.fps(avgFps, duration)
// 重置
frameCount = 0
timerStart = SystemClock.uptimeMillis()
handler.postDelayed(::report, checkInterval)
}
}
线上每 5 秒采样一次,得到平均 FPS。
8.2 火焰图关联到栈
FPS 监控只能告诉你"慢了",但"为什么慢"还需要栈。增强版:
private fun onJankDetected(time: Long, duration: Long) {
val mainLooper = Looper.getMainLooper().thread
val stack = mainLooper.stackTrace
if (duration > jankThreshold) {
crashReporter.reportNonFatal(stack.toString(), duration = duration)
}
}
把 jank 当 non-fatal 异常上报,能拿到完整栈。
8.3 与 APM 平台集成
Sentry:
Sentry.metrics().distribution(
"frame.time_ms",
frameDurationMs.toDouble(),
Unit.MILLISECOND,
tags = mapOf("page" to currentPage)
)
火山 APM / 自建:
{
"metric": "jank.count",
"value": 1,
"timestamp": 1234567890,
"tags": {
"app_version": "1.0.0",
"device_model": "Xiaomi 14",
"page": "feed"
}
}
九、卡顿治理的工程流程
十、几个具体卡顿场景
10.1 RecyclerView 列表滑动卡顿
10.2 Compose 组合慢
10.3 Activity 转场卡顿
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setEnterSharedElementCallback(...)
postponeEnterTransition()
// 视图准备好了再调用
startPostponedEnterTransition()
}
共享元素动画要等 layout 完成再启动。
小结
卡顿监控 = Choreographer 监测 + Perfetto 栈 + 线上 FPS 指标 + 治理流程。重要的不是监控到不掉帧,而是从监控到修复再到回归的闭环。 治理 3 个月后,P95 FPS 应该能稳定在 55+。
下一篇进入网络优化专题的最后一块——海外网络适配、连接策略与协议选择,覆盖 HTTPS/QUIC/HTTP3 在 Android 上的工程实践。
参考阅读
Android Vitals:Slow Rendering
Choreographer 与 Frame Metrics API
Perfetto Tracing SDK 文档
作者:互联网开发站 / 2026-08-24
引用链接
[1]https://ui.perfetto.dev/)打开: https://ui.perfetto.dev/%EF%BC%89%E6%89%93%E5%BC%80