Administrator
发布于 2026-08-26 / 33 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 88 篇:Android 卡顿监控:从 Choreographer 到线上 FPS 监测

前言

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。常见情况:

阶段

常见原因

Input

输入事件堆积、被其他线程占用

Animation

复杂属性动画、循环触发

Traversal

布局嵌套、measure 慢、Custom View draw 慢

RenderThread

GPU 命令排队过度

SurfaceFlinger

合成任务过多

三、用 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 基准

  • 越线就是掉帧

实际工程中:

柱子颜色

含义

黄色

Swap Buffers 慢

红色

Command Issue 慢

蓝色

Issue Draw

绿色

Swap

这个工具对于现场验证(用户排查、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"
  }
}

九、卡顿治理的工程流程

阶段

行动

发现

APM 监测到 P95 FPS < 50

复现

用 Choreographer 现场抓栈

定位

systrace / Perfetto 找具体阶段

修复

异步 / 缓存 / 扁平布局

验证

准出对照,目标恢复

监控

阈值报警 + 持续巡检

十、几个具体卡顿场景

10.1 RecyclerView 列表滑动卡顿

原因

修法

onBindViewHolder 里 IO

移到协程

多层 LinearLayout

改 ConstraintLayout

Bitmap decode 阻塞

Glide / Coil + 后台线程

DiffUtil 重算耗时

用 AsyncListDiffer

10.2 Compose 组合慢

原因

修法

Lambda 内引用

用 remember { } 包裹

Key 没设

提供稳定 key

derivedStateOf 内部计算

改为 computed property

频繁动画

用 withFrameNanos

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


评论