2. doFrame 回调的时序与粘滞问题

doFrame 是 Choreographer.FrameCallback 中每帧执行的核心回调。其调用时序直接决定帧率是否稳定。当 CPU/GPU 负载过高、Vsync 调度异常或主线程消息堆积时,doFrame 会出现“粘滞”(stuck)现象:回调实际执行时间点严重偏离预期,甚至被跳过多帧。本章通过典型时序图、粘滞原因分析和帧率影响评估,帮助开发者定位此类性能问题。

2.1 典型时序图

下图对比了正常帧率与粘滞帧率下 doFrame 的执行时序。水平方向表示同一帧的生命周期,垂直方向为时间轴向下。Vsync 信号驱动 Choreographer 调度,doFrame 回调必须在下一次 Vsync 之前完成,否则将导致丢帧。

doFrame 回调时序对比 正常帧率 (60 fps) 粘滞帧率 (丢帧) VSYNC 触发 doFrame 开始 输入 → 动画 → 绘制 doFrame 结束 下一帧 VSYNC ✅ 每帧 16.6 ms 内完成,无丢帧 VSYNC 触发 doFrame 推迟(粘滞) 处理超时 → 丢帧 第二个 VSYNC 到达 doFrame 仍继续执行旧帧 ⚠️ 帧间隔 > 33 ms,jank 出现 图例: VSYNC 正常 doFrame 粘滞/丢帧

2.2 doFrame 调用时序详解

Choreographer 通过 FrameDisplayEventReceiver 接收 Vsync 事件,消息处理流程如下:

  1. 系统发送 Vsync:SurfaceFlinger 或硬件 Vsync 产生垂直同步信号。
  2. Choreographer.handleMessage(MSG_DO_FRAME):将 Vsync 封装为消息放入主线程消息队列。
  3. doFrame(long frameTimeNanos) 被调用:该回调在下一个主线程 Looper 循环中被执行。参数 frameTimeNanos 表示原始 Vsync 的纳秒时间戳。
  4. 内部三阶段处理:依次触发 InputCallbacks、AnimationCallbacks、TraversalRunnables(即 View 的 measure/layout/draw)。
  5. 注册下一帧监听:Choreographer 再次注册 FrameDisplayEventReceiver 等待下一个 Vsync。
关键参数:frameTimeNanos 是 Vsync 发生的理论时间,而非 doFrame 实际执行时间。系统通过该时间戳判断帧是否逾期。

2.3 粘滞 doFrame 的原因

粘滞是指 doFrame 回调未能按预期在 Vsync 后立即执行,而是被主线程的消息或 CPU/GPU 资源挤占,导致实际调用时间大大滞后。常见原因包括:

原因分类 具体场景 典型表现
主线程消息队列堵塞 Layout 频繁、Binder 调用超时、Bitmap 解码 doFrame 延迟 8~30 ms
Vsync 调度失配 SurfaceFlinger 过载、GPU 渲染末完成栅栏等待 Vsync 信号本身被延迟或丢弃
CPU 频率锁定与核心调度 低性能模式、温控降频、中断绑定 Choreographer 消息处理变慢
动画/绘制耗时过长 自定义 View 中过度 onDraw、GPU 过度绘制 doFrame 内部执行时间 > 16ms
同一线程上其他优先级高任务 AMS/WMS 回调、广播处理 MSG_DO_FRAME 消息被插队
注意:即使 doFrame 最终被执行,如果 frameTimeNanos 与当前时间偏差超过一个 Vsync 周期(>16.6ms),Choreographer 内部会自动跳过中间帧,导致帧率直接减半。

2.4 粘滞对帧率的影响

粘滞 doFrame 直接影响应用帧率 (FPS) 和用户感知的 jank:

  • 丢帧 (Frame drop):实际帧间隔 ≥ 33.3ms,屏幕上该帧内容实际显示时间被拉长,视觉效果卡顿。
  • 帧率阶梯下降:轻度粘滞导致 30 fps(每秒30帧),重度粘滞降至 15 fps 甚至更低。
  • VSync 同步丢失:SurfaceFlinger 可能切换至动态帧率模式或与 BufferQueue 不同步,产生撕裂。
  • 电池与发热恶性循环:粘滞导致渲染时间延长,GPU 持续高负载,热量累积又进一步触发降频。

2.5 调试与定位方法

利用系统工具和日志定位粘滞帧:

2.5.1 Systrace 抓取

通过 Systrace 观察 Choreographer#doFrame 的包络线与 Vsync 信号间距:

python systrace.py -a com.example.app -o trace.html
# 重点关注如下标记:
# -- "Choreographer#doFrame" 持续时间
# -- "Choreographer#scheduleVsync" 是否延迟
# -- "msg_dispatch" 轮到 MSG_DO_FRAME 的时间

2.5.2 命令行 dumpsys

查看 Choreographer 内部状态:

adb shell dumpsys choreographer
# 输出中包含:
# -- "Frame pending" 等待的帧数
# -- "Frame skipped" 被跳过的帧数
# -- "Last call time" 上次 doFrame 调用时间

2.5.3 代码日志埋点

在自定义 FrameCallback 中记录实际执行时间差:

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long lastFrameTimeNanos = 0;
    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNanos != 0) {
            long deltaMs = (frameTimeNanos - lastFrameTimeNanos) / 1000000;
            if (deltaMs > 20) {
                Log.w("Choreographer", "粘滞检测: 帧间隔 " + deltaMs + "ms");
            }
        }
        lastFrameTimeNanos = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

2.6 常见错误与避坑

常见错误 错误原因 正确做法
在 doFrame 中执行了同步 I/O 或网络请求 阻塞主线程,导致后续 doFrame 全部粘滞 将 I/O 移至子线程或使用异步回调
混淆 frameTimeNanos 与当前时间 使用 System.nanoTime() 计算差值导致负值或错误 始终使用 frameTimeNanos 做帧间比较
未正确处理 Choreographer 的注册/解绑 Activity 销毁后 callback 仍被调用,引发异常 在 onPause 中取消注册,onResume 中重新注册
连续多次 postFrameCallback 造成无效帧 在 doFrame 内部又多次 post 同一 callback 只 post 一次,且不要重复嵌套

2.7 性能优化建议

核心原则:保持 doFrame 内总耗时 ≤ 12 ms(留余量给系统调度),避免任何阻塞操作。
  • 异步化输入处理:将触摸事件的复杂逻辑(如手势识别)移到 InputEventReceiver 的后台线程。
  • 动画复用与降级:使用 ObjectAnimator 代替自定义动画循环,减少 doFrame 中 onAnimationUpdate 开销。
  • 渲染裁剪:在 doFrame 的 Traversal 阶段,只对脏区域进行 measure/layout/draw,避免全屏重绘。
  • 帧率监管:当检测到粘滞时,主动降低动画帧率或放弃非关键渲染(如模糊效果)。

通过上述手段,可显著缓解 doFrame 粘滞现象,保障应用帧率稳定接近 60 fps 或设备刷新率上限。