Android帧率监控原理:Choreographer vs SurfaceFlinger
做帧率监控这么多年,我一直觉得有两兄弟绕不开——Choreographer 和 SurfaceFlinger。一个是UI线程的节拍器,一个是合成显示的调度官。你要真想搞清楚帧率为什么掉,或者监控工具有没有漏报误报,就得先弄明白这俩家伙到底在干嘛。
我习惯把帧率监控拆成两个层面:应用侧和系统侧。Choreographer管应用侧,SurfaceFlinger管系统侧。今天咱们就把这俩掰开揉碎了讲。
Choreographer:UI线程的节拍器
Choreographer是Android在4.1引入的玩意,说白了就是给UI线程一个精准的节拍。它监听硬件VSync信号,然后回调你的doFrame方法。你想想看,如果没有这个节拍器,App想啥时候画就啥时候画,那画面撕裂、掉帧都算轻的。
它的工作流程大致是这样:
- 硬件VSync到来,Choreographer收到信号
- 它通知所有注册的
FrameCallback,也就是你的doFrame - App开始执行布局、绘制,然后把生成好的Buffer丢给SurfaceFlinger
doFrame 回调间隔。如果间隔不是16.6ms(60fps),那就是掉帧了。我经常用 Choreographer.getInstance().postFrameCallback() 在代码里手动打log看每次回调的时间差。
我在项目中遇到过一个问题:某个页面滑动时帧率突然从60fps掉到30fps。查半天以为是渲染太重,结果发现是某个第三方SDK在UI线程里做了大量JSON解析,把Choreographer的回调给堵住了。说白了,Choreographer再准,你线程没空处理也没用。
SurfaceFlinger:合成与显示的调度官
SurfaceFlinger工作在系统服务层,它管的是多个窗口(Surface)怎么合成一张图,然后送显。它不直接处理应用绘制,而是等App把Buffer提交过来后,统一做合成。
你想想看,系统里可能有十几个窗口:状态栏、导航栏、你的App、还有通知。每个窗口都有一块Buffer,SurfaceFlinger得决定什么时候把这些Buffer合成,什么时候送到显示屏。这个决策靠的是VSync信号和Fence机制。
SurfaceFlinger的工作周期一般也是16.6ms,但它和Choreographer并不是完全同步的。Choreographer走的是应用VSync,SurfaceFlinger走的是合成VSync。系统有两个VSync相位,就是为了让App渲染足够的时间,等SurfaceFlinger开始合成时Buffer已经就位。
两者对比
我把核心差异整理成了表格,你对比着看会更清晰:
| 对比项 | Choreographer | SurfaceFlinger |
|---|---|---|
| 工作层级 | 应用框架层 | 系统服务层 |
| 触发源 | 应用VSync | 合成VSync |
| 主要职责 | 协调绘制与布局 | 合成与显示调度 |
| 延迟来源 | UI线程卡顿 | Buffer提交延迟、合成超时 |
| 监控方法 | Choreographer.FrameCallback |
dumpsys SurfaceFlinger --latency |
| 常见问题 | 主线程死锁、过重绘制 | 多窗口竞争、Fence等待 |
你看,两者在时间轴上其实是流水线关系:Choreographer催App画,App画完丢Buffer,SurfaceFlinger拿到Buffer开始合,合成完送显。任何一个环节延迟,都会体现在最终帧率上。
实战中如何监控
平时做监控,我习惯双管齐下。在应用侧,我会插入Choreographer回调来记录每一帧的时间戳;在系统侧,我会定期拉取SurfaceFlinger的信息。
先给个写死的代码示例,你可以在自己的监控工具里用:
// 应用侧监控示例
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long lastFrameTimeNanos = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos != 0) {
long delta = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000L; // 转毫秒
if (delta > 20) { // 超过20ms算掉帧
Log.w("FrameMonitor", "掉帧: " + delta + "ms");
}
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
adb shell dumpsys SurfaceFlinger --latency + 你的WindowName。这条命令会输出近期的帧数据,每行是"时间戳 绘制耗时 帧周期"。我通常用它来排查是App渲染慢还是合成慢。
你想想看,光看Choreographer的回调时间,只能知道UI线程有没有卡。但如果帧率低是由SurfaceFlinger合成超时引起的,Choreographer那边是看不出来的。所以一定要两边数据对齐,才能准确定位。
嗯,最后提醒一句:监控工具本身也会消耗性能,千万别在线上App里高频轮询。我的经验是采样率控制在每秒一次,然后把数据先缓存在内存里,等帧率真掉的时候再详细记录。