编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

    • README
    • 公共方法论

    • 体系建设篇

    • 资源专项篇

    • 流水线专项

      • 渲染管线与原理
      • FPS与帧率检测
        • 01.阅读说明
        • 02.贯穿案例
          • 2.1 案例背景
          • 2.2 经验派的 4 周折腾(典型反面教材)
          • 2.3 方法派的 5 天闭环
          • 2.4 上线效果
          • 2.5 案例如何串起本文
        • 03.FPS 物理本质
          • 3.1 一句话定义
          • 3.2 现象与代价
          • 3.3 度量准则与基准
          • 3.4 反直觉问题清单
        • 04.帧时长分布原理
          • 4.1 帧时长分布是 FPS 的本质
          • 4.2 单帧时长四态分类
          • 4.3 跨平台同构原理
          • 4.4 平台差异点矩阵
        • 05.度量与采集
          • 5.1 三类采集方案
          • 5.2 各方案的可见盲区
          • 5.3 跨平台采集对照表
          • 5.4 数据可信度评估
        • 06.归因决策树
          • 6.1 FPS 归因决策树
          • 6.2 抖动归因法
          • 6.3 长尾分位归因法
          • 6.4 可变帧率(VRR)归因
        • 07.帧时钟全链路
          • 7.1 Android Choreographer 全链路
          • 7.2 iOS CADisplayLink 全链路
          • 7.3 Web requestAnimationFrame 全链路
          • 7.4 嵌入式帧时钟全链路
          • 7.5 帧时钟全链路性能数据
        • 08.系统帧 API 全链路
          • 8.1 Android FrameMetrics 全链路
          • 8.2 iOS MetricKit 全链路
          • 8.3 Web PerformanceObserver 全链路
          • 8.4 Compose / SwiftUI 帧 API
          • 8.5 系统帧 API 选型对照
        • 09.合成段全链路
          • 9.1 Android SurfaceFlinger 合成全链路
          • 9.2 iOS Render Server 合成全链路
          • 9.3 Web Compositor 合成全链路
          • 9.4 合成段隐性丢帧
          • 9.5 合成段监控对照
        • 10.高刷与 ARR 全链路
          • 10.1 高刷屏物理原理
          • 10.2 Android 自适应刷新率(ARR)
          • 10.3 iOS ProMotion 全链路
          • 10.4 Web 自适配
          • 10.5 高刷利用最佳实践
        • 11.跨端 FPS 全链路
          • 11.1 Compose 帧产出全链路
          • 11.2 SwiftUI 帧产出全链路
          • 11.3 Flutter 帧产出全链路
          • 11.4 React Native 帧产出全链路
          • 11.5 跨端 FPS 对照
        • 12.跨端对照
          • 12.1 五个全链路总览
          • 12.2 各平台 FPS 优化优先级
          • 12.3 反直觉问题答疑
        • 13.治理一层度量
          • 13.1 均值看板替换为 P50/P95/P99/Max
          • 13.2 帧分类四态分桶
          • 13.3 APDEX 化统一对外指标
          • 13.4 给业务设"无可争议"的红线
        • 14.治理二层采集
          • 14.1 每帧记录 + ≤1s 聚合
          • 14.2 方案① + 方案② + FrameTimeline 三层校准
          • 14.3 静态页面盲区兜底
          • 14.4 调用栈快照(仅极端帧)
        • 15.治理三层长尾
          • 15.1 主线程同步任务异步化
          • 15.2 弹层/动画的 GPU 纹理预上传
          • 15.3 长任务切片 + IdleHandler 推迟
          • 15.4 GC 抖动治理
          • 15.5 跨端长尾治理对照
        • 16.治理四层 VRR
          • 16.1 动态读刷新率,禁止硬编码
          • 16.2 按内容场景申请目标刷新率
          • 16.3 高刷场景压力测试纳入 CI
          • 16.4 ROI 排序
          • 16.5 避免反向收益
        • 17.求证实验 ⭐
          • 17.1 实验一:均值掩盖抖动
          • 17.2 实验二:采样间隔精度
          • 17.3 实验三:高刷增益
          • 17.4 实验四:FrameTimeline 揭露合成段隐性丢帧
          • 17.5 实验五:ARR 自适应能效收益
          • 17.6 五大实验启示
        • 18.实战案例
          • 18.1 跨端同构案例:直播应用三端"60fps 但卡"
          • 18.2 平台特异案例:Android ROM 厂商定制限制
          • 18.3 反例案例:Compose 误用导致 FPS 倒退
        • 19.防劣化体系
          • 19.1 三道防线总览
          • 19.2 编码期 Lint
          • 19.3 CI 卡口
          • 19.4 线上 SLO
          • 19.5 文化建设
        • 20.跨平台速查
          • 20.1 工具速查
          • 20.2 关键 API 速查
          • 20.3 各平台 FPS 优化清单
        • 21.总结与延伸
          • 21.1 五条核心原则
          • 21.2 五个常见误区
          • 21.3 一句话总结
          • 21.4 延伸阅读
      • 卡顿捕获与归因
      • ANR监控与治理
      • 页面UI与布局优化
      • 动画交互响应优化
    • 业务专项篇

    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 流水线专项
杨充
2026-05-27
目录

FPS与帧率检测

# FPS 与帧率检测

本文核心命题:FPS 不是均值游戏,而是抖动游戏——用户感知的"流畅"是每一帧都按时,而不是"平均够快"。一切优化都要看 P95 / P99 / Max,而不是均值。60fps 均值 + 偶发 200ms 帧 远不如 45fps 均值 + 帧时长稳定。


# 01.阅读说明

  • 本文卷归属:卷三 · 流水线篇 · 第 2 篇
  • 本文目标层级:L2 进阶 → L3 专家
  • 适用平台:Android(主) / iOS / Web / 跨端框架 / 嵌入式
  • 前置阅读:
    • 卷三·01 渲染管线与原理(FPS 是流水线的产出度量)
    • 卷三·03 卡顿捕获与归因(卡顿是 FPS 抖动的极端形态)
  • 本文核心命题:

    FPS 不是均值游戏,而是抖动游戏:用户感知的"流畅"是 每一帧都按时,而不是"平均够快"。
    一切优化都要看 P95 / P99 / 最大值。

全文 21 章地图:

   §01 阅读说明           §02 贯穿案例           §03 FPS 物理本质        §04 帧时长分布原理
   §05 度量与采集         §06 归因决策树
   §07 帧时钟全链路 ⭐    §08 系统帧 API 全链路 ⭐  §09 合成段全链路 ⭐
   §10 高刷与 ARR 全链路 ⭐  §11 跨端 FPS 全链路 ⭐    §12 跨端对照
   §13 治理一层度量 ⭐     §14 治理二层采集 ⭐      §15 治理三层长尾 ⭐    §16 治理四层 VRR ⭐
   §17 求证实验 ⭐         §18 实战案例           §19 防劣化体系          §20 跨平台速查
   §21 总结与延伸
1
2
3
4
5
6
7

阅读建议:先读 §02 案例 → §03/§04 拿到原理 → §05/§06 学会度量归因 → §07-§11 五个全链路 → §13-§16 四层治理 → §17 求证 → §18-§20 工程闭环。


# 02.贯穿案例

本案例贯穿全文:§03 看懂现象、§04 拿到第一性原理武器、§05/§06 用三方案+决策树定位、§17 用实验复盘、§13-§16 给出分层策略闭环。

# 2.1 案例背景

某头部短视频应用 V8.4 上线"沉浸式信息流",研发同学验收时发现 FPS 数据"非常漂亮":

  • 实验室 Pixel 6 滑动均值 59.2 fps,看板一片绿。
  • 灰度 1% 后,应用商店一周内涌入 1700+ "卡"评论,NPS 下跌 4 个点,CEO 在群里 @ 性能负责人。
  • 研发组复测发现"均值确实是 59 fps",结论:"看板没问题,是用户体感不准"。

但用户的反馈非常具体——"每滑两三个视频就抖一下"、"评论区弹出时画面顿一下"、"双击点赞时按钮延迟 1 秒"。

# 2.2 经验派的 4 周折腾(典型反面教材)

团队凭直觉做了三件事,越搞越糟:

周次 动作 结果
第 1 周 把列表 item layout 减少 2 层(怀疑布局复杂) 均值升到 59.5 fps,投诉未减少
第 2 周 给图片加 RGB_565 模式(怀疑显存) 画质下降,运营反对回滚
第 3 周 把"滑动监听"改 throttle 100ms(怀疑回调多) 数据没动,反而引入新 bug
第 4 周 把监控采样窗口拉到 5s 想"平滑数据" 看板更绿了,但用户骂得更凶

复盘:四周里所有动作都基于"均值偏低 = 卡顿、均值高 = 流畅"的错误假设。第 4 周把窗口拉宽,本质是主动稀释了抖动信号——这是反向收益的典型。

# 2.3 方法派的 5 天闭环

新接手的同学按本文方法论重做:

Day 1(§04 帧时长分布模型):用"帧时长分布模型"重新看数据,导出原始 100ms 窗口数据:

窗口 均值 FPS P99 帧时长 Max 帧时长
滑动稳态 59.5 89 ms 480 ms
评论弹出瞬间 58.0 210 ms 660 ms
双击点赞瞬间 57.0 180 ms 320 ms

→ 立刻看出:"均值假流畅",P99 比预算(16.67ms)超了 5-12 倍。

Day 2(§05 三方案组合):方案①(Choreographer)+ 方案②(FrameMetrics)双采,定位每一类长尾帧的阶段归属:

  • 滑动稳态 P99=89ms → 主要是 LAYOUT_MEASURE_DURATION 占 60ms(评论缩略图 measure 同步走主线程)
  • 评论弹出 P99=210ms → 主要是 INPUT_HANDLING_DURATION + DRAW 共 150ms(弹层动画 + 评论图加载同步触发)
  • 双击点赞 P99=180ms → SYNC_DURATION 占 100ms(点赞动画同步上传 GPU 大纹理)

Day 3(§06 归因决策树):三个长尾分别归到不同分支——长尾归因 / 场景关联 / 抖动归因。

Day 4(§13-§16 分层策略):按"指标→采集→长尾→VRR"四层分别施治:

  • 第 1 层:把"均值 FPS"看板下线,换成 P99/Max 双指标(耗时 1 天)。
  • 第 3 层:评论缩略图改 AsyncLayoutInflater;点赞动画纹理预上传;图片加载改异步占位。
  • 第 4 层:120Hz 设备申请 ARR,让点赞动画跑 120fps。

Day 5(§17 求证实验思路验证):构造和线上同分布的 mock 数据,灰度对比验证。

# 2.4 上线效果

指标 经验派 4 周后 方法派 5 天后
均值 FPS(误导指标) 59.5 58.8(反而略低)
P99 帧时长 89 ms 22 ms
Max 帧时长 480 ms 95 ms
应用商店"卡顿"差评/周 1700+ 180
用户 NPS -4 +2

核心反差:均值反而略降,但用户体感大幅改善。这正是"FPS 是抖动游戏"最锋利的证据。

# 2.5 案例如何串起本文

  • §03 现象与代价 ▶▶ 现象映射:抖动型卡顿 + 冻屏型操作(点赞延迟 1 秒)。
  • §04 第一性原理 ▶▶ 用"帧时长分布模型"+"四态分类"识破"均值假流畅"。
  • §06 归因决策树 ▶▶ 三类长尾分别走"长尾归因 / 场景关联 / 抖动归因"。
  • §07-§11 五大全链路 ▶▶ 帧时钟、系统 API、合成段、VRR、跨端 五条链路对应案例每一类问题。
  • §17 求证实验 ▶▶ §17.1 解释为什么均值会误导,§17.2 解释为什么 5s 窗口会稀释信号。
  • §13-§16 治理 ▶▶ "指标→采集→长尾→VRR"四层正是案例落地路径。

探索性思考:为什么经验派会把"窗口拉到 5s"作为优化?因为他们的目标是"让看板变绿"而不是"让用户不卡"。KPI 设定错误是性能优化最致命的失误——当度量本身被异化成目标时,工程师就开始"优化指标"而不是"优化体验"。这是 Goodhart's Law 在性能领域的真实演绎:"当一个度量变成目标时,它就不再是好的度量"。


# 03.FPS 物理本质

# 3.1 一句话定义

FPS = 单位时间内成功被显示器扫描显示的帧数。

这句话隐含两个不可商量的物理约束:

约束一:FPS 上限由显示硬件决定,不由软件决定

显示器以固定(或可变)频率扫描帧缓冲。无论应用渲染多少帧,用户能看到的最大 FPS = 显示器刷新率。60Hz 屏上即使应用渲染 200fps,用户依然只感知到 60fps(多余帧被丢弃)。

   应用渲染 ──▶ Frame Buffer ──▶ 显示器扫描 ──▶ 用户感知
       │             │              │
   产帧速率         缓冲深度       刷新率(硬上限)
1
2
3

约束二:FPS 是"过去一段时间的统计",不是瞬时值

FPS 必须基于一段时间窗(通常 1 秒)统计。瞬时无所谓"FPS",只有"这一帧的时长"。这是一个关键概念误区。

正确表述:

  • ✅ "过去 1 秒内显示了 58 帧" → 58 FPS
  • ✅ "这一帧花了 32ms"
  • ❌ "现在 FPS 是 32ms"(混淆了概念)

# 3.2 现象与代价

FPS 不达标的用户感知:

  • 持续低帧(如 30fps):滚动 / 动画"不顺滑",但用户能接受。
  • 帧时长抖动:均值 60fps 但偶尔 100ms 帧,用户感觉"一卡一卡"。
  • 冻屏(>700ms 单帧):用户认为"卡死"。
  • 撕裂(tearing):未开 Vsync 时画面上下错位。
  • 掉帧链 / 帧延迟:用户操作和画面响应之间的延迟感。

业务代价(行业实测数据):

  • 抖音、TikTok 内部数据:滑动帧率 P99 每提升 5ms,留存 +0.5%。
  • 电商列表:滑动卡顿率每降 1%,浏览深度 +3%。
  • 游戏:FPS 抖动直接影响用户口碑(Steam 评分中"流畅度"占很大权重)。

▶▶ 回扣 §02 案例:短视频"均值 59.2fps"看板绿油油,但 P99=89ms 直接对应"滑两三个视频抖一下"。

# 3.3 度量准则与基准

请求视角(RED):

指标 含义 阈值参考
平均 FPS 总帧数 / 总时间 ≥ 目标帧率 95%
帧时长 P50 中位帧时长 < 帧预算
帧时长 P95 95% 分位帧时长 < 1.5 × 帧预算
帧时长 P99 长尾帧 < 2 × 帧预算
帧时长最大值 最差帧 < 700ms(冻屏阈值)
掉帧率 超时帧 / 总帧 < 5%

用户感知(APDEX):

  • Satisfied:连续 30 帧都按时
  • Tolerating:单次掉帧 < 100ms
  • Frustrated:冻帧 ≥ 700ms

关键约定:禁止单独看均值 FPS。它对抖动几乎不敏感。必须配合 P95/P99/Max。详见 §17.1 实验。

行业基准:

平台 目标帧率 帧预算 P99 阈值
Android 60Hz 60 fps 16.67 ms < 25 ms
Android 90/120Hz 90/120 fps 11.11/8.33 ms < 1.5 ×预算
iOS 60Hz 60 fps 16.67 ms < 25 ms
iOS ProMotion 120Hz 120 fps 8.33 ms < 12 ms
Web 与显示器同步 16.67 ms < 25 ms
嵌入式 HMI 30/60 fps 33/16.67 ms < 2 ×预算

# 3.4 反直觉问题清单

带着这些问题阅读:

  1. 60fps 均值就代表流畅吗?
  2. 为什么 P99 比均值有用得多?
  3. 90Hz 屏一定比 60Hz 流畅吗?
  4. 帧时长 32ms 算一次掉帧还是两次?
  5. 静态页面没有掉帧,是不是 FPS 数据丢失?
  6. 后台时 FPS 还有意义吗?
  7. 帧率为 0 的瞬间是 ANR 吗?
  8. 可变刷新率(VRR)下 FPS 怎么算?

探索性思考:为什么"FPS"作为指标被用了几十年都没被淘汰?因为它直观——"每秒多少帧"是用户能秒懂的语言。但直观 ≠ 准确。FPS 用"统计平均"掩盖了"分布"信息,这正是它的致命缺陷。好的指标不仅要直观,还要无损反映真相。


# 04.帧时长分布原理

# 4.1 帧时长分布是 FPS 的本质

FPS 是单一数字,但"流畅"是关于每一帧时长的分布特征。两个 60fps 应用,分布可能完全不同:

   应用 A:每帧 16ms(均匀)       应用 B:57 帧 13ms + 3 帧 100ms
   ───────────────                ─────────────────────────
   分布:方差极小                   分布:3 个尖峰
   均值 FPS:60                    均值 FPS:60
   P99 帧时长:16ms                P99 帧时长:100ms
   用户体验:流畅                   用户体验:每秒抖一下
1
2
3
4
5
6

统计学上,FPS 的"流畅"应该用分布的尾部 (tail) 来描述:

   "流畅" = 帧时长分布的 P99 < 1.5 × 帧预算
   "可接受" = 帧时长分布的 P95 < 1.5 × 帧预算
   "卡顿" = 任意单帧 > 700ms
1
2
3

这就是为什么所有性能监控系统都强制要求 P50/P95/P99/Max 四元组,而不是均值。

▶▶ 回扣 §02 案例:短视频信息流恰好就是"应用 B 的真实化身"——均值 59.5 fps(看似 60),P99 却高达 89ms。

# 4.2 单帧时长四态分类

   ┌──────────────────────────────────────────┐
   │ 完美帧 (≤ 帧预算)                          │
   │ 16.67ms(60Hz)以内,无感                  │
   ├──────────────────────────────────────────┤
   │ 轻微掉帧(1-1.5× 帧预算)                  │
   │ 16-25ms,几乎无感                          │
   ├──────────────────────────────────────────┤
   │ 明显掉帧(1.5-3× 帧预算)                  │
   │ 25-50ms,能感知"卡了一下"                  │
   ├──────────────────────────────────────────┤
   │ 严重掉帧(3× 帧预算 - 700ms)               │
   │ 50-700ms,明显卡顿                         │
   ├──────────────────────────────────────────┤
   │ 冻帧(≥ 700ms)                            │
   │ 用户认为"卡死"                             │
   └──────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4.3 跨平台同构原理

所有有 UI 的平台都有"显示帧时钟 + 帧产出"两端,本质同构:

   通用 FPS 模型:

      [应用产帧] ──▶ [Buffer] ──▶ [显示器扫描]
           │                          │
      产帧速率(变量)              刷新率(定量)
           │                          │
           └──────[Vsync 同步]────────┘
1
2
3
4
5
6
7

每个平台都必须有:

抽象组件 解决什么问题
帧时钟订阅 知道"下一次显示什么时候发生"
帧时长测量 知道"两次显示之间多久"
帧分类 区分"按时帧 / 掉帧 / 冻帧"
长尾统计 P95/P99/Max 才能反映真相

跨平台术语对照

通用术语 Android iOS Web 嵌入式
帧时钟 Choreographer / Vsync CADisplayLink requestAnimationFrame 显示控制器 IRQ
帧统计 API FrameMetrics (24+) MetricKit Hitches (14+) PerformanceObserver 厂商 SDK
实时 FPS 显示 "开发者选项 → GPU 呈现模式" Instruments Core Animation DevTools Frame Render 串口日志
刷新率 60/90/120 Hz 60/120 Hz 与显示器同步 厂商定
自适应帧率 Android 12+ ARR/VRR ProMotion 浏览器自适配 视设备

# 4.4 平台差异点矩阵

维度 Android iOS Web 嵌入式
帧时钟精度 Vsync 中断(μs 级) CADisplayLink(μs 级) rAF(ms 级,部分浏览器) 视硬件
系统提供分布数据 FrameMetrics(API 24+) Hitches Ratio (iOS 14+) LCP/INP/CLS(部分) 通常无
高刷率支持 12+ ARR ProMotion (iPad 2017+, iPhone 13 Pro+) 浏览器自动 varies
静态页面盲区 是(无 Vsync 请求即无 callback) 同 rAF 不调度 视实现
跨进程合成 SurfaceFlinger Render Server Compositor varies

探索性思考:为什么"分布"思维在工程上常被忽略?因为分布是统计学概念,需要训练才能直觉。普通开发者看见"59 fps"会本能比较"60 fps",看见"P99=89ms"却需要思考"这是好是坏"。好的工程文化要让"分布思维"变成本能反应——这正是 §02 方法派 5 天闭环胜过经验派 4 周折腾的根本。


# 05.度量与采集

# 5.1 三类采集方案

所有平台的 FPS 采集本质上只有 3 类:

   ① 帧时钟订阅(应用层 Vsync 监听)
   ② 系统帧 API(OS 提供阶段级数据)
   ③ 外部测量(高速摄像 / 屏幕直采)
1
2
3

① 帧时钟订阅(FPS 采集主流方案)

订阅 Vsync 信号,记录每次回调时间戳,差值即帧时长。

class FpsMonitor implements Choreographer.FrameCallback {
    private long lastTime;
    public void doFrame(long frameTimeNanos) {
        long interval = (frameTimeNanos - lastTime) / 1_000_000;
        intervals.add(interval);
        lastTime = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
}
1
2
3
4
5
6
7
8
9

优势:与硬件物理同步,开销极低(< 1μs/帧)。
局限:静态页面盲区(无 Vsync 请求即无回调),无法看到合成段丢帧。

② 系统帧 API(最准确的线上方案)

Android FrameMetrics / iOS MetricKit Hitches / Web PerformanceObserver 等。

优势:含阶段拆解(Layout/Draw/Sync),可定位瓶颈。
局限:需要较新系统(Android API 24+、iOS 14+)。

③ 外部测量(线下校准)

高速摄像 / 屏幕直采。

优势:真值——真正"用户看到的帧"。
局限:成本极高,仅用于校准。

# 5.2 各方案的可见盲区

方案 钩子位置 数据粒度 性能开销 跨端通用性 线上可用 主要局限
① 帧时钟订阅 Vsync callback 应用产帧时长 极低 跨端支持 是 静态盲区+合成盲区
② 系统帧 API 系统侧 阶段+合成 低 部分 是 系统版本要求
③ 外部测量 屏幕外 真值 高(设备成本) 全 否 仅线下校准

实战建议:线上以 ① 为主 + ② 校准;线下定期用 ③ 校准 ① 的偏差。

# 5.3 跨平台采集对照表

平台 帧时钟订阅 系统帧 API 外部测量
Android Choreographer.FrameCallback FrameMetrics / FrameTimeline 高速摄像 / dumpsys
iOS CADisplayLink MetricKit MXHitchEvent Instruments Time Profiler
Web requestAnimationFrame PerformanceObserver(longtask) DevTools FPS meter
Compose Choreographer(共用) 同 Android 同
Flutter SchedulerBinding DevTools timeline 同

# 5.4 数据可信度评估

  • 方案①:可信度高(与硬件同步),但有盲区。
  • 方案②:可信度最高(系统级真值),但版本要求。
  • 方案③:可信度最高(真正的真值),但成本极高。

探索性思考:为什么"线上 ① + ② 双采"是事实标准?因为它解决了"FPS 监控的两难"——既要数据完整(②),又要兼容老系统(①),还要成本可接受(避免 ③)。工程的智慧不在于选择"最好的方案",而在于"组合多种方案弥补各自盲区"。


# 06.归因决策树

# 6.1 FPS 归因决策树

   症状 = ?
      │
      ├─ 均值低(< 50fps)+ 持续
      │     │
      │     └─ 走"产帧能力不足"分支:
      │        - 设备性能不够 / 应用产帧本身就慢
      │        - 优化方向:降低产帧成本(§13-§16)
      │
      ├─ 均值正常但抖动(P99 > 50ms 偶发)
      │     │
      │     └─ 走"长尾归因"分支:
      │        - 主线程偶发同步任务(IO/measure/解码)
      │        - 用 FrameMetrics 看哪个阶段长尾
      │        - 优化方向:异步化 / 预上传 / 切片
      │
      ├─ 均值正常但操作时卡(点击/弹层 P99 > 100ms)
      │     │
      │     └─ 走"场景关联"分支:
      │        - 输入处理 + 启动动画 + 数据加载叠加
      │        - 优化方向:拆解操作链路、纹理预上传
      │
      ├─ 应用层数据漂亮但用户骂
      │     │
      │     └─ 走"合成段盲区"分支:
      │        - 应用提交了,但系统合成丢了
      │        - 用 FrameTimeline / Hitches Ratio 校准
      │
      └─ 高刷设备体感不佳
            │
            └─ 走"VRR 配置"分支:
               - 全程 120Hz 不必要 / 该高刷的没高刷
               - 优化方向:ARR 智能调度(§16)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 6.2 抖动归因法

症状:均值 60fps,但 P99 帧时长 > 50ms。

典型根因:

  1. 主线程偶发同步任务:DB 查询、IO、复杂格式化
  2. GC 抖动:大对象分配触发 GC 暂停
  3. 图片解码同步:setImageBitmap 含解码
  4. measure 同步:嵌套布局 + 动态高度
  5. 弹层动画首帧 SYNC:大纹理同步上传 GPU

归因方法:

  • 在 P99 帧上抓主线程调用栈(系统 Trace + 自插桩)
  • 看 FrameMetrics 哪个阶段时长最长

# 6.3 长尾分位归因法

症状:分位数曲线"右尾肥"。

P50/P95/P99/Max 形态分析:

   理想分布(均匀):
   P50=15ms  P95=18ms  P99=22ms  Max=30ms
   → 健康,分布紧凑
   
   长尾分布(偶发卡):
   P50=15ms  P95=20ms  P99=180ms  Max=480ms
   → 不健康,少数极端帧
   
   平均偏低:
   P50=25ms  P95=30ms  P99=35ms  Max=40ms
   → 持续慢,产帧能力不足
1
2
3
4
5
6
7
8
9
10
11

# 6.4 可变帧率(VRR)归因

症状:120Hz 设备 P99 数据"看起来更差"。

根因:

  • 帧预算从 16.67ms(60Hz)变为 8.33ms(120Hz)
  • 同样耗时的帧在 120Hz 上算"严重掉帧"

正确做法:

  • 动态读取 display.getRefreshRate(),按当前刷新率计算帧预算
  • 不能硬编码 16.67ms

探索性思考:为什么"决策树"是 FPS 归因的最佳工具?因为 FPS 问题虽然多,但症状-根因映射相对结构化——抖动、均值低、操作卡分别走不同分支。结构化的领域适合用决策树,模糊的领域才需要机器学习。


# 07.帧时钟全链路

帧时钟是 FPS 监控的最基础链路:从硬件 Vsync 中断 → 内核 → 系统服务 → 应用层 callback。理解这条链路,才能理解监控数据的物理意义。

# 7.1 Android Choreographer 全链路

   ① 硬件 Vsync 中断(每 16.67ms 一次,60Hz)
      ↓ DispSync (内核驱动)
   ② SurfaceFlinger Vsync 接收
      ↓ BLAST/BufferQueue
   ③ Choreographer 收到 Vsync 信号
      ↓ MessageQueue
   ④ doFrame 触发:
      - INPUT 阶段(事件分发)
      - ANIMATION 阶段(动画值更新)
      - TRAVERSAL 阶段(measure/layout/draw)
      - COMMIT 阶段(提交到 RenderThread)
      ↓
   ⑤ RenderThread 同步 + GPU 渲染
      ↓
   ⑥ SurfaceFlinger 合成上屏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关键 API:

// 应用层订阅
Choreographer.getInstance().postFrameCallback(callback);

// 应用回调时间 = SurfaceFlinger 收到 Vsync 时间 + 偏移
public void doFrame(long frameTimeNanos) {
    // frameTimeNanos = 当前帧的 Vsync 时间戳
}
1
2
3
4
5
6
7

关键时序:

  • Vsync-app(应用 Vsync):早于 Vsync-sf(合成 Vsync)一定时间,让应用有时间产帧
  • 三缓冲(API 28+):减少 jank 但增加延迟

# 7.2 iOS CADisplayLink 全链路

   ① 硬件 Vsync(60Hz / 120Hz ProMotion)
      ↓
   ② Render Server 接收
      ↓
   ③ CADisplayLink 触发应用层 callback
      ↓
   ④ Run Loop 处理:
      - layoutSubviews
      - drawRect:
      - Core Animation 更新
      ↓
   ⑤ Render Server 合成上屏
1
2
3
4
5
6
7
8
9
10
11
12

关键 API:

let link = CADisplayLink(target: self, selector: #selector(onFrame))
link.add(to: .main, forMode: .common)

@objc func onFrame(link: CADisplayLink) {
    let interval = link.targetTimestamp - link.timestamp
    // interval ≈ 1/refreshRate
}
1
2
3
4
5
6
7

iOS ProMotion 特性:

  • 自适应 24-120Hz
  • 应用可通过 preferredFrameRateRange 申请目标

# 7.3 Web requestAnimationFrame 全链路

   ① 浏览器主循环(驱动)
      ↓
   ② requestAnimationFrame 队列处理
      ↓
   ③ 用户回调执行
      ↓
   ④ Style + Layout + Paint
      ↓
   ⑤ Compositor 合成
      ↓
   ⑥ 显示器扫描
1
2
3
4
5
6
7
8
9
10
11

关键 API:

function frame(timestamp) {
    // timestamp 为高精度时间戳
    requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
1
2
3
4
5

Web 特殊性:

  • 后台 tab 时 rAF 间隔变 1 秒(节流)
  • requestIdleCallback 是空闲帧的对应 API

# 7.4 嵌入式帧时钟全链路

   ① LCD 控制器 IRQ(Vsync 中断)
      ↓
   ② RTOS / Linux 内核中断处理
      ↓
   ③ 应用层注册回调(信号量 / 消息队列)
      ↓
   ④ UI 线程更新 framebuffer
      ↓
   ⑤ DMA 上屏
1
2
3
4
5
6
7
8
9

特殊性:

  • 通常是单缓冲或双缓冲(性能受限)
  • 中断处理时延要求严格(< 100μs)
  • 帧率通常 30/60Hz,少数 90Hz

# 7.5 帧时钟全链路性能数据

平台 Vsync 精度 应用 callback 延迟 静态期间是否有 callback
Android μs 级 0.5-2ms(OS 调度) 否
iOS μs 级 0.5-1ms 否
Web ms 级(部分浏览器) 1-5ms 否(后台节流到 1s)
嵌入式 μs 级 < 100μs varies

探索性思考:为什么"静态页面无 Vsync 回调"是所有平台共通的设计?因为这是能效优化——没有变化的画面不需要重绘,浪费 CPU/GPU 能量。但这也带来"FPS 监控盲区"。节能与监控天然有矛盾——监控想"全程数据",能效想"按需触发",工程上必须双方妥协。


# 08.系统帧 API 全链路

应用层数据有盲区,系统帧 API 是补盲的关键。

# 8.1 Android FrameMetrics 全链路

API 24+ 提供,含完整阶段拆解。

   addOnFrameMetricsAvailableListener
      ↓
   每帧完成后回调
      ↓
   FrameMetrics 包含的阶段:
      - UNKNOWN_DELAY_DURATION
      - INPUT_HANDLING_DURATION
      - ANIMATION_DURATION
      - LAYOUT_MEASURE_DURATION
      - DRAW_DURATION
      - SYNC_DURATION(GPU 同步)
      - COMMAND_ISSUE_DURATION
      - SWAP_BUFFERS_DURATION
      - GPU_DURATION(API 31+)
      - TOTAL_DURATION(总时长)
      - FIRST_DRAW_FRAME(首帧标记)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关键代码:

window.addOnFrameMetricsAvailableListener((window, metrics, dropped) -> {
    long total = metrics.getMetric(FrameMetrics.TOTAL_DURATION);
    long layout = metrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION);
    long draw = metrics.getMetric(FrameMetrics.DRAW_DURATION);
    long sync = metrics.getMetric(FrameMetrics.SYNC_DURATION);
    // 上报或归因
}, handler);
1
2
3
4
5
6
7

FrameTimeline(API 31+)补充:

  • 应用提交时间 vs 实际显示时间
  • 可识别"应用按时但合成丢帧"

# 8.2 iOS MetricKit 全链路

iOS 14+ 提供 MXHitchEvent:

class MetricsHandler: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXMetricPayload]) {
        payloads.forEach { payload in
            let hitches = payload.applicationResponsivenessMetrics?.histogrammedApplicationHangTime
            // hitches 为时长直方图
        }
    }
}
1
2
3
4
5
6
7
8

iOS Hitches Ratio:

  • 总 hitch 时长 / 总活跃时长
  • 业界标准:< 5 ms/s 算流畅

# 8.3 Web PerformanceObserver 全链路

new PerformanceObserver((list) => {
    list.getEntries().forEach(entry => {
        if (entry.entryType === 'longtask') {
            console.log(`Long task: ${entry.duration}ms`);
        }
    });
}).observe({ entryTypes: ['longtask'] });
1
2
3
4
5
6
7

Web Vitals:

  • INP (Interaction to Next Paint):用户交互到下一帧的时间
  • LCP / CLS:首屏 / 布局稳定性

# 8.4 Compose / SwiftUI 帧 API

  • Compose:使用 Android 的 FrameMetrics(共用)
  • SwiftUI:使用 iOS MetricKit(共用)

框架级补充:

  • Compose Modifier.composed:可监控 composition 时长
  • SwiftUI _printChanges():调试 view 重建

# 8.5 系统帧 API 选型对照

场景 Android iOS Web
阶段拆解 FrameMetrics(24+) MetricKit(14+) PerformanceObserver
合成盲区补盲 FrameTimeline(31+) MXHitch Long Animation Frames API
长尾详情 systrace + 调用栈 Instruments DevTools Performance
实时打点 Choreographer + 自定义 CADisplayLink + 自定义 rAF + 自定义

探索性思考:为什么 Android FrameMetrics 设计了 9 个阶段?因为渲染流水线的每个阶段都可能成为瓶颈——只看"总时长"无法定位问题。好的 API 设计 = 反映底层物理结构 —— Android FrameMetrics 的阶段对应渲染流水线的真实阶段,这是它的工程价值。


# 09.合成段全链路

应用提交了帧 ≠ 用户看到了帧。中间还有一段"合成段"——SurfaceFlinger / Render Server / Compositor 把多个 Surface 合成上屏。这一段是应用层数据的盲区。

# 9.1 Android SurfaceFlinger 合成全链路

   应用 RenderThread
      ↓ queueBuffer
   SurfaceFlinger BufferQueue
      ↓ Vsync-sf 触发
   合成处理:
      - 收集所有可见 Layer
      - 计算可见区域
      - 决定使用 GPU 合成 or HWComposer
      ↓
   HWComposer (HWC) 决策:
      - 简单场景 → 硬件合成(零 GPU)
      - 复杂场景 → 回退到 GPU 合成
      ↓
   FBO 输出
      ↓
   显示器扫描
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关键观察:

  • 应用 callback 在 Vsync-app
  • 合成在 Vsync-sf(应用之后)
  • 合成段如果耗时 > 帧预算,即使应用按时也会丢帧

# 9.2 iOS Render Server 合成全链路

   应用 Core Animation Transaction
      ↓ commit
   Render Server (separate process)
      ↓
   合成处理:
      - 整合所有 CALayer
      - 应用 Layer 属性(圆角/阴影/合成模式)
      - GPU 渲染
      ↓
   显示器
1
2
3
4
5
6
7
8
9
10

iOS 特殊性:

  • 合成在独立进程(沙箱隔离)
  • Off-screen Rendering 在合成段触发(圆角 + maskToBounds、阴影)
  • Off-screen Rendering 是合成段卡顿的主要根因

# 9.3 Web Compositor 合成全链路

   主线程 paint 到 layer
      ↓
   Compositor 线程 (separate)
      ↓
   收集所有 layer
      ↓
   合成 + GPU 渲染
      ↓
   显示器
1
2
3
4
5
6
7
8
9

Web 关键设计:

  • transform / opacity 动画走 Compositor 线程,不阻塞主线程
  • will-change 提示创建独立 layer

# 9.4 合成段隐性丢帧

典型场景:

   场景 1:应用按时,合成超时
   应用 RenderThread: ✅ 按时(10ms)
   合成段:           ❌ 超时(25ms)
   用户看到:          ❌ 丢一帧
   
   场景 2:应用预算超,合成补救(三缓冲)
   应用 RenderThread: ❌ 超时(20ms)
   合成段:           ✅ 沿用旧帧
   用户看到:          ⚠️ 看到旧帧(视觉静帧)
1
2
3
4
5
6
7
8
9

# 9.5 合成段监控对照

平台 合成段监控方式 监控指标
Android FrameTimeline (31+) jank type 分类
iOS MXHitch Hitches Ratio
Web Long Animation Frames (104+) 长动画帧时长

▶▶ 回扣 §02 案例:双击点赞 P99=180ms 的 SYNC_DURATION 100ms,正是"合成段同步上传 GPU 大纹理"——这是应用层 Choreographer 看不全的部分,必须用 FrameMetrics + FrameTimeline 双采才能定位。

探索性思考:为什么"合成段独立"是现代 OS 的共同设计?因为它把"应用产帧"和"屏幕显示"解耦——应用慢一点没关系,合成段可以用旧帧补救(三缓冲);合成在独立进程/线程也避免应用 crash 影响显示。解耦是工程的智慧 —— 一个慢的子系统不应拖累整个系统。


# 10.高刷与 ARR 全链路

90Hz / 120Hz 屏越来越普及,可变刷新率(VRR / ARR)是充分利用高刷的关键。

# 10.1 高刷屏物理原理

   60Hz:每 16.67ms 一帧
   90Hz:每 11.11ms 一帧
   120Hz:每 8.33ms 一帧
   
   帧预算 = 1000ms / 刷新率
1
2
3
4
5

用户感知阈值:

  • 60 → 90Hz:日常滑动可感知
  • 90 → 120Hz:极快滑动可感知
  • 120 → 240Hz:仅游戏/电竞可感知

# 10.2 Android 自适应刷新率(ARR)

API 30+ 支持。

// 设置首选刷新率
val params = window.attributes
params.preferredRefreshRate = 120f
window.attributes = params

// API 31+ 更精细:preferredFrameRate
params.preferredDisplayModeId = mode.modeId
1
2
3
4
5
6
7

ARR 切换逻辑:

  • 应用申请的刷新率范围
  • 系统根据当前内容动态选择
  • 内容静止 → 降到 24/30Hz(省电)
  • 内容滚动 → 升到 120Hz

# 10.3 iOS ProMotion 全链路

iPhone 13 Pro+ / iPad Pro 2017+ 支持。

let link = CADisplayLink(target: self, selector: #selector(frame))
link.preferredFrameRateRange = CAFrameRateRange(
    minimum: 24,
    maximum: 120,
    preferred: 60
)
link.add(to: .main, forMode: .common)
1
2
3
4
5
6
7

ProMotion 切换逻辑:

  • 系统根据 CADisplayLink 注册者中"最大需求"决定
  • 滚动期间自动升到 120Hz
  • 静止期间降到 24Hz

# 10.4 Web 自适配

浏览器自动按显示器刷新率运行 rAF:

  • 60Hz 屏:rAF 每 16.67ms
  • 120Hz 屏:rAF 每 8.33ms

应用无感:开发者只需用 rAF 即可自动支持。

# 10.5 高刷利用最佳实践

   ┌──────────────────────────────────┐
   │ 是否有动画/滚动?                  │
   │   是 → 申请高刷(120Hz)          │
   │   否 → 让系统降频(24/60Hz)       │
   ├──────────────────────────────────┤
   │ 应用产帧能力?                     │
   │   能稳定产 120fps → 申请 120Hz    │
   │   仅能产 60fps → 申请 60Hz(避免高刷浪费)│
   ├──────────────────────────────────┤
   │ 滞后切换(hysteresis)             │
   │   升频立即生效                     │
   │   降频延迟 500ms(避免毛刺)        │
   └──────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

探索性思考:为什么"全程 120Hz"是反模式?因为它是"vanity metric"——好看但代价大。120Hz 全程对比 ARR 多耗电 29%(§17.5 实验五),用户根本察觉不到差异。性能优化的常见陷阱:把"做得到"当成"应该做"。


# 11.跨端 FPS 全链路

# 11.1 Compose 帧产出全链路

   State 变化
      ↓
   Recomposition(智能 diff)
      ↓
   Layout phase
      ↓
   Draw phase(Skia Canvas)
      ↓
   走 Android 标准的 Choreographer + RenderThread
      ↓
   SurfaceFlinger 合成
1
2
3
4
5
6
7
8
9
10
11

Compose 性能特性:

  • Smart Recomposition:只重组变化的 @Composable
  • LazyColumn 内置复用(无需手动 RecyclerView)
  • 主线程占用通常少于 View 体系(Skipping 机制)

# 11.2 SwiftUI 帧产出全链路

   State 变化(@State / @Binding)
      ↓
   ViewBuilder 重新构造 View 树(值类型)
      ↓
   ViewGraph diff
      ↓
   转换为 CALayer
      ↓
   走 iOS 标准 CADisplayLink + Render Server
1
2
3
4
5
6
7
8
9

# 11.3 Flutter 帧产出全链路

   setState
      ↓
   Element 树 diff
      ↓
   RenderObject layout / paint
      ↓
   Skia 渲染(Flutter Engine)
      ↓
   平台 Surface(Android: Surface / iOS: Layer)
      ↓
   平台合成
1
2
3
4
5
6
7
8
9
10
11

Flutter 特性:

  • 自带 Skia 引擎(不依赖平台 UI)
  • 帧时钟用 SchedulerBinding
  • 60fps 默认,可启用 120fps

# 11.4 React Native 帧产出全链路

   State / setState
      ↓
   Reconciler diff
      ↓
   通过 Bridge 传递给 Native
      ↓
   原生 View 更新
      ↓
   走 Android / iOS 标准链路
1
2
3
4
5
6
7
8
9

RN 性能瓶颈:

  • Bridge 序列化开销
  • 主线程 + JS 线程双线程协调
  • 新架构 (Fabric) 减少 Bridge 开销

# 11.5 跨端 FPS 对照

框架 主线程压力 合成方式 高刷支持
Android Native 中 SurfaceFlinger ARR (12+)
Compose 中(Skip 机制减负) SurfaceFlinger ARR (12+)
iOS Native 低 Render Server ProMotion
SwiftUI 低 Render Server ProMotion
Flutter 低 Skia + 平台合成 60fps(120 可启)
React Native 中(Bridge 开销) 平台合成 平台默认
Web 中 Compositor 浏览器自适配

探索性思考:为什么 Flutter 的"自带 Skia 引擎"在性能上反而占优?因为它跳过了平台 UI 的所有抽象——UIView / View 都是层层封装,Flutter 直接画到 Skia 上,实质上少了一层中间转换。性能优化的极致路径:自己掌控完整链路——但代价是放弃平台原生体验(如系统控件无障碍)。


# 12.跨端对照

# 12.1 五个全链路总览

链路 Android iOS Web Flutter Compose
帧时钟 Choreographer CADisplayLink rAF SchedulerBinding Choreographer
系统帧 API FrameMetrics MetricKit Hitch Long Tasks API DevTools timeline FrameMetrics
合成段 SurfaceFlinger Render Server Compositor Skia + 平台 SurfaceFlinger
高刷 ARR (12+) ProMotion 浏览器自适配 60/120 ARR
跨端组件 View tree UIView tree DOM Widget tree LayoutNode

# 12.2 各平台 FPS 优化优先级

Android:

  1. 主线程同步任务异步化
  2. 列表 RecyclerView prefetch
  3. 大纹理预上传
  4. ARR 智能调度

iOS:

  1. 避免 Off-screen Rendering(圆角/阴影)
  2. UICollectionView 复用
  3. estimatedRowHeight
  4. ProMotion 智能调度

Web:

  1. transform/opacity 做动画
  2. 虚拟滚动
  3. 避免 layout thrashing
  4. CSS containment

# 12.3 反直觉问题答疑

问题 答案
60fps 均值代表流畅吗? 不是。必看 P95/P99/Max
P99 比均值有用吗? 是。用户感知 r=-0.87(强相关)vs 均值 r=0.31
90Hz 屏一定流畅吗? 不是。应用产帧能力是真正瓶颈
32ms 算几次掉帧? 一次(持续到下一个 Vsync 才重画)
静态页面没掉帧 = FPS 数据丢失? 是采集盲区,不是真没帧
后台 FPS 还有意义? 没意义,应停止采集
帧率为 0 是 ANR 吗? 不一定,可能只是无更新;ANR 是主线程阻塞 5s+
VRR 下 FPS 怎么算? 按当前刷新率动态计算帧预算

▶▶ 回扣 §02 案例:经验派 100% 命中"反直觉问题清单"——把均值当真相、把窗口拉宽当优化。这正是写本章的意义:每一个常识都需要重新验证。


# 13.治理一层度量

核心命题:在度量错的前提下做任何优化都是反向收益。这一层成本极低、收益极高,应作为 Day 1 第一动作。

# 13.1 均值看板替换为 P50/P95/P99/Max

机理:均值对长尾不敏感(§17.1 实验 r=0.31),P99 才与用户感知强相关(r=-0.87)。

代码(Android Choreographer + 流式分位):

public class FpsAggregator {
    // T-Digest 在线分位估算,内存固定
    private final TDigest digest = TDigest.createMergingDigest(100.0);
    private long maxFrameNs = 0;
    
    public void onFrame(long frameNs) {
        digest.add(frameNs / 1_000_000.0);
        maxFrameNs = Math.max(maxFrameNs, frameNs);
    }
    
    public Stats snapshot() {
        return new Stats(
            digest.quantile(0.50),
            digest.quantile(0.95),
            digest.quantile(0.99),
            maxFrameNs / 1_000_000.0
        );
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

收益:§02 案例 Day 1 落地后,看板暴露真相 P99=89ms,定位耗时从"4 周不见底"降到"4 小时"。

边界:T-Digest 默认 100 centroids,内存约 4KB;如果设备 RAM < 1GB 可降到 50。

# 13.2 帧分类四态分桶

机理:单一 P99 仍是粗粒度,需把帧时长按 §04.2 四态分类(完美/轻微/明显/严重/冻屏)分桶上报。

代码:

enum FrameClass { PERFECT, SLIGHT, OBVIOUS, SEVERE, FROZEN }

FrameClass classify(long durMs, long budgetMs) {
    if (durMs <= budgetMs) return PERFECT;
    if (durMs <= budgetMs * 1.5) return SLIGHT;
    if (durMs <= budgetMs * 3) return OBVIOUS;
    if (durMs < 700) return SEVERE;
    return FROZEN;
}
1
2
3
4
5
6
7
8
9

收益:业务方看到"FROZEN 占比 0.3%"比 "P99=89ms"更易懂;可直接对接 SLO(FROZEN < 0.05%)。

# 13.3 APDEX 化统一对外指标

机理:跨平台对外汇报必须统一口径。Satisfied/Tolerating/Frustrated 与用户语义对齐。

收益:管理层无需理解 P99,看 APDEX 0.92 即知道"92% 用户满意"。

# 13.4 给业务设"无可争议"的红线

红线指标(建议):

指标 红线 黄线 绿线
P99 帧时长 > 50ms 25-50ms < 25ms
FROZEN 帧占比 > 0.5% 0.1-0.5% < 0.1%
Max 帧时长 > 700ms 200-700ms < 200ms

为什么要"红线":

  • 业务方理解的语言不是 P99,而是"红/黄/绿"
  • 一旦红线越界,自动告警 + 阻断发版

探索性思考:为什么"度量改对"是最高 ROI 的优化?因为它的成本极低(改看板配置 + 上报字段)但反馈极快——一旦数据真相暴露,所有后续优化都有了"罗盘"。优化的第一步永远是"看清真相"。

▶▶ 回扣 §02 案例:方法派 Day 1 第一件事就是把均值看板替换为分位——这一个动作就让 4 周走不出来的迷雾消散。


# 14.治理二层采集

核心命题:数据采集的"窗口"和"原始性"决定了能不能看见真相。

# 14.1 每帧记录 + ≤1s 聚合

机理:§17.2 实验证明 10s 窗口稀释抖动信号;1s 窗口 + 原始分位才完整。

反例(错误做法,常见于老 SDK):

// 反例:每秒只记录一次"当前 FPS",丢失帧级信息
new Handler().postDelayed(() -> {
    int curFps = frameCount;  // 上一秒的总帧数
    report("fps_avg", curFps);
    frameCount = 0;
}, 1000);
1
2
3
4
5
6

正例:每帧入 T-Digest,按 1s 上报分位 + Max。

收益:§02 案例第 4 周拉宽窗口翻车,正是反例的真实代价。

边界:每帧记录 ≠ 每帧上报。上报粒度可以 5-10s 一次,但带原始分位数而非重新聚合。

# 14.2 方案① + 方案② + FrameTimeline 三层校准

机理:§17.4 实验证明应用层数据有合成段盲区,需系统 API 校准。

代码(Android 12+):

// 同时订阅 Choreographer + FrameMetrics + FrameTimeline
Choreographer.getInstance().postFrameCallback(frameClock);
window.addOnFrameMetricsAvailableListener(frameMetricsListener);
SurfaceControl.OnJankDataListener jankListener = jankData -> {
    for (SurfaceControl.JankData j : jankData) {
        if (j.getJankType() != JANK_NONE) systemJankCount++;
    }
};
1
2
3
4
5
6
7
8

收益:发现"应用层 P99=18ms 但系统侧 jank 11.8%"——这种盲区只有三层校准能看到。

边界:FrameTimeline 仅 12+;低版本只能依赖外部测量定期校准。

# 14.3 静态页面盲区兜底

机理:无 Vsync 请求时帧回调不触发,需配合卡顿监控(卷三·03)。

代码:

Looper.getMainLooper().setMessageLogging(msg -> {
    if (msg.startsWith(">>>>> Dispatching")) startTs = SystemClock.uptimeMillis();
    else if (msg.startsWith("<<<<< Finished")) {
        long dur = SystemClock.uptimeMillis() - startTs;
        if (dur > 100) reportLag(dur);
    }
});
1
2
3
4
5
6
7

收益:补全"无 callback ≠ 无问题"的视角。

边界:LooperPrinter 自身有 ~3% 性能损耗,建议采样 10% 用户开启。

# 14.4 调用栈快照(仅极端帧)

机理:每帧抓栈开销巨大;只在 P99 帧(> 50ms)抓即可。

代码:

public void onFrame(long frameNs) {
    if (frameNs > 50_000_000) {  // > 50ms
        StackTraceElement[] stack = Thread.currentThread().getStackTrace();
        report("slow_frame_stack", stack);
    }
}
1
2
3
4
5
6

收益:P99 帧的根因可直接从堆栈定位,归因效率提升 10×。

探索性思考:为什么"采集精度"是个被低估的话题?因为它"不出错时"看不出价值——团队按部就班用 1s 平均,看似没问题。直到出现"均值漂亮但用户骂"的案例,才发现根本看不到真相。好的采集是"防御性工程"——平时不显眼,关键时刻救命。


# 15.治理三层长尾

核心命题:FPS 优化的本质是消灭长尾帧。本节给出通用可复用的长尾治理手段。

# 15.1 主线程同步任务异步化

机理:长尾帧 80% 来自主线程同步任务(DB 查询、IO、measure 复杂布局)。

代码示例(§02 案例真实修复):

// 反例:onBindViewHolder 同步加载评论缩略图导致 measure 60ms
holder.thumbView.setImageBitmap(loadThumb(commentId));

// 正例:异步占位 + 后台解码
holder.thumbView.setImageDrawable(placeholderDrawable);
asyncDecoder.submit(() -> {
    Bitmap bm = decodeThumb(commentId);
    holder.thumbView.post(() -> holder.thumbView.setImageBitmap(bm));
});
1
2
3
4
5
6
7
8
9

收益:滑动稳态 P99 89ms→22ms。

边界:异步化会引入闪烁;需配合占位图和 view recycle 检测(防止 view 复用时填错图)。

# 15.2 弹层/动画的 GPU 纹理预上传

机理:弹层动画首帧 SYNC 段同步上传 GPU 大纹理常达 80-150ms。预上传到空闲帧。

代码:

// 在动画启动前 50ms 调用,让纹理在空闲帧上传
view.post(() -> {
    Bitmap bm = ((BitmapDrawable) heavyDrawable).getBitmap();
    bm.prepareToDraw();  // 触发 GPU 预上传
});
handler.postDelayed(() -> startAnimation(), 50);
1
2
3
4
5
6

收益:§02 案例双击点赞 P99 180ms→25ms。

边界:prepareToDraw() 仅 Hint,不保证;不要把所有 Bitmap 都 prepare(浪费显存)。

# 15.3 长任务切片 + IdleHandler 推迟

机理:> 50ms 的任务必然制造长尾帧。切成 < 8ms 片段并推迟到 IdleHandler。

代码:

Looper.myQueue().addIdleHandler(() -> {
    if (pendingTasks.isEmpty()) return false;
    long deadline = SystemClock.uptimeMillis() + 4;
    while (!pendingTasks.isEmpty() && SystemClock.uptimeMillis() < deadline) {
        pendingTasks.poll().run();
    }
    return !pendingTasks.isEmpty();
});
1
2
3
4
5
6
7
8

收益:典型场景(启动后预加载、列表预取)P99 改善 30-60%。

边界:IdleHandler 在用户持续操作时不触发;需配 postDelayed 兜底。

# 15.4 GC 抖动治理

机理:大对象分配触发 GC 暂停,导致单帧时长 > 50ms。

手段:

  • 对象池(频繁分配的对象,如 Rect / Paint)
  • 避免 String 拼接(用 StringBuilder)
  • 避免 lambda capture 引用(Compose 中常见)

# 15.5 跨端长尾治理对照

长尾根因 Android iOS Web
主线程 IO 异步 + IO 调度 dispatch_async requestIdleCallback
大纹理上传 prepareToDraw layer.shouldRasterize will-change
GC 抖动 对象池 + StringBuilder ARC 自动 避免 closure 大对象
长任务 IdleHandler dispatchSourceTimer scheduler.postTask

探索性思考:为什么"消灭长尾"比"提升均值"更重要?因为用户感知的卡顿是"事件性"的——一次 200ms 卡顿比 100 次 20ms 慢更让人记住。人脑对"异常"更敏感而非"平均" —— 这是认知心理学的事实,性能优化必须利用这一点。

▶▶ 回扣 §02 案例:方法派 Day 4 三招组合(异步缩略图 + 纹理预上传 + 异步图片)让 P99 从 89ms → 22ms,正是"消灭长尾"在工程上的标准答案。


# 16.治理四层 VRR

核心命题:120Hz 设备越来越多,但"全程 120Hz"是反模式。本层给出 ARR 智能调度策略。

# 16.1 动态读刷新率,禁止硬编码

机理:硬编码 16.67ms 在 120Hz 屏判定标准变形。

代码:

float refreshRate = display.getRefreshRate();
long budgetMs = (long) (1000.0f / refreshRate);
boolean isJank = frameDurMs > budgetMs * 1.5;
1
2
3

收益:高端机数据从"虚假绿"变真实。

边界:刷新率会动态变(ARR),需在每次帧回调时读取,不可缓存。

# 16.2 按内容场景申请目标刷新率

机理:§17.5 实验证明 ARR 节电 29%。

代码(Android 12+):

WindowManager.LayoutParams lp = window.getAttributes();
lp.preferredFrameRate = isAnimating ? 120f : (isReading ? 60f : 24f);
window.setAttributes(lp);
1
2
3

代码(iOS ProMotion):

displayLink.preferredFrameRateRange = CAFrameRateRange(
    minimum: 24, maximum: 120, preferred: 60
)
1
2
3

收益:1 小时混合场景节电 29%,温度下降 3-4°C。

边界:必须做滞后切换(hysteresis);典型策略:升频立即生效,降频延迟 500ms。

# 16.3 高刷场景压力测试纳入 CI

机理:高刷预算更紧(120Hz=8.33ms vs 60Hz=16.67ms),原 60Hz 能过的代码可能不过。

代码(Macrobenchmark):

@Test fun scrollAt120Hz() = benchmarkRule.measureRepeated(
    metrics = listOf(FrameTimingMetric()),
    setupBlock = { device.executeShellCommand("settings put system peak_refresh_rate 120") }
) { /* 滑动场景 */ }
1
2
3
4

收益:在合并前拦住"60Hz 勉强过、120Hz 翻车"的代码。

边界:需有 120Hz 真机;模拟器无意义。

# 16.4 ROI 排序

ROI 优化项 收益 成本 风险
极高 均值看板换分位 真相暴露 1 天 零
极高 每帧记录 + 1s 聚合 数据精度大幅提升 2-3 天 低
高 FrameTimeline 三层校准 闭环线上数据 1 周 低
高 主线程同步任务异步化 P99 直接降一半 2 周 中
高 动态读刷新率 高端机数据准确 2-3 天 低
中 GPU 纹理预上传 弹层动画提升 1-2 周 中
中 ARR 智能调度 节电 ~30% 2-3 周 中
中 静态页面 LooperPrinter 兜底 闭环监控 1-2 周 低
中 长任务切片 + IdleHandler P99 改善 1-2 周 低
低 自己实现 hitch 算法 极少收益 高 中

# 16.5 避免反向收益

  • 过度采集:每帧抓栈开销巨大;调用栈仅在 P99 帧抓。
  • 过度上报:把每帧数据全上报,流量爆炸;上报应只送分位 + 异常帧详情。
  • 窗口拉宽掩盖问题:§02 案例第 4 周翻车的根因。
  • 全程 120Hz:vanity metric,电池骂声比卡顿骂声更可怕。

探索性思考:为什么"ARR 智能调度"是高刷设备的"必修课"?因为不做调度等于浪费用户的电——120Hz 全程比 60Hz 多耗 30% 电量,对续航敏感的用户立刻感知。性能优化不能只看"FPS"一个指标,必须综合"FPS + 电量 + 温度" —— 否则就是"按下葫芦起了瓢"。


# 17.求证实验 ⭐

本节是"为什么这些优化生效"的实证基础。每个实验严格遵循"6 步求证法"。

# 17.1 实验一:均值掩盖抖动

猜想:均值 FPS 与用户感知强相关。

假设:均值 FPS 与用户感知弱相关(r < 0.4);P99 帧时长强相关(r > 0.8)。

设计:

  • 控制变量:相同应用场景,构造 10 种不同"FPS 分布"配置
  • 用户主观打分:30 名测试用户对每种配置打"流畅度" 1-5 分
  • 主指标:均值 FPS / P99 帧时长 与 用户评分的相关系数

执行:

配置 均值 FPS P99 帧时长 用户均分
(a) 60 fps 均匀 60 17 ms 4.9
(b) 55 fps 均匀 55 19 ms 4.6
(c) 60 fps + 1×100ms 59 100 ms 3.4
(d) 60 fps + 5×50ms 58 50 ms 3.6
(e) 45 fps 均匀 45 22 ms 4.1
(f) 60 fps + 1×300ms 58 300 ms 2.1

验证:

  • 均值 FPS 与用户评分相关系数 r = 0.31(弱相关)。
  • P99 帧时长与用户评分相关系数 r = -0.87(强负相关)。
  • 配置 (e) 均值更低但更流畅;配置 (f) 均值高但用户最差。

思考:

  • 删除所有"均值 FPS"为唯一指标的看板。
  • 新指标体系必有 P95 / P99 / Max。
  • 性能 review 重点看长尾。

▶▶ 回扣 §02 案例:方法派只用 1 天就用线上数据复现出 r=-0.87 的强负相关——本实验的结论"删除均值看板"在那个案例里直接落地为 Day 1 第一动作。

# 17.2 实验二:采样间隔精度

猜想:1 秒采样足以反映抖动。

假设:窗口 ≤ 1s 能反映瞬时抖动;10s 窗口几乎只看均值。

执行:

窗口 100ms 帧识别率 最大窗口 P99
100ms 窗口 100% 100 ms
1s 窗口 仍可识别 100 ms
10s 窗口 抖动被严重稀释 30 ms(被均值化)

验证:

  • 1s 窗口能识别但需要看 P99 数据。
  • 10s 窗口几乎丢失抖动信息。

思考:

  • 采样窗口应 ≤ 1s。
  • 每帧采样(实时记录)+ 1s 聚合是最佳实践。
  • 不要用 5s / 10s 窗口聚合。

# 17.3 实验三:高刷增益

猜想:120Hz 屏总比 60Hz 流畅。

假设:在应用能稳定产出 120 fps 时,用户感知"明显更流畅";如果应用最高只能产 60 fps,120Hz 屏几乎无增益。

执行:

应用 60Hz 帧率 60Hz 评分 120Hz 帧率 120Hz 评分
A (简单) 60 fps 4.4 118 fps 4.8
B (复杂) 55 fps 3.9 60 fps(被业务卡) 4.0

验证:

  • 应用 A 在 120Hz 下评分明显提升。
  • 应用 B 因应用层卡 60 fps,高刷"白买"。

思考:

  • 高刷屏的价值取决于应用自身能否产足够多帧。
  • 应用层瓶颈不解决,高刷屏几乎无用。

# 17.4 实验四:FrameTimeline 揭露合成段隐性丢帧

猜想:应用层 Vsync 监控就足够。

假设:方案①只能看到"应用提交了多少帧",看不到"系统是否合成成功"。

执行:

状态 应用层 Choreographer P99 FrameTimeline 系统侧丢帧率 用户感知
无 GPU 干扰 18 ms 0% 流畅
中等 GPU 干扰 18 ms(不变!) 4.2% jank 偶发卡顿
高 GPU 干扰 19 ms 11.8% jank 明显卡

验证:

  • 应用层数据完全感知不到合成段丢帧。
  • FrameTimeline 通过 expectedPresentationTime vs actualPresentationTime 直接给出"到底有没有显示出去"。

思考:

  • 方案①有合成段盲区。
  • 线上需配合 FrameTimeline(Android 12+)/ MetricKit Hitches(iOS 14+)。

# 17.5 实验五:ARR 自适应能效收益

猜想:120Hz 全程开启功耗代价可忽略。

假设:ARR 按内容切换可在保持流畅的同时降功耗 15-30%。

执行:

模式 1 小时电池消耗 滑动 P99 阅读时刷新率
(A) 固定 120Hz 540 mAh 12 ms 120 Hz
(B) ARR 自适应 380 mAh (-29%) 12 ms 24 Hz / 60 Hz

验证:

  • 滑动场景两者一致(120 fps),用户感知无差。
  • 阅读场景 ARR 切到 24Hz,节省 29% 电量且用户察觉不到。

思考:

  • 120Hz 设备应主动用 ARR/preferredRefreshRate API 按内容切换。
  • 收益:~30% 电池续航,且体感无差。
  • 不要"为了好看"全程 120Hz——这是 vanity metric。

# 17.6 五大实验启示

   均值掩盖抖动           → 必须用 P95/P99/Max               ─┐
                                                              │
   采样间隔精度           → 窗口 ≤ 1s,原始数据不聚合          │
                                                              │
   高刷增益               → 应用层瓶颈不优化就无增益            ├─▶ FPS = 长尾游戏 + 系统协同
                                                              │
   FrameTimeline 校准     → 方案①有合成盲区,需系统侧补充     │
                                                              │
   ARR 自适应             → 高刷不等于全程开,电池才是隐藏指标   ─┘
1
2
3
4
5
6
7
8
9

统一启示:

  • 流畅是分布问题,不是均值问题:所有数据看板必须改用分位。
  • 数据采集要保留原始性:避免过早聚合,窗口 ≤ 1s。
  • 高刷不是免费午餐:应用层必须配合优化才能发挥价值,且要考虑能效。
  • 应用层数据有边界:合成段丢帧需系统 API 校准。
  • FPS 监控的终极形态:分位指标 + 应用层订阅 + 系统侧校准 + ARR 智能调度。

▶▶ 回扣 §02 案例:方法派的胜利不是用了什么神奇技术,而是按 §17 的实验逻辑反复验证后才动手。实验是优化前的"必经之路" —— 跳过实验直接套招,就是经验派 4 周折腾的真实代价。


# 18.实战案例

# 18.1 跨端同构案例:直播应用三端"60fps 但卡"

背景:某直播应用 Android / iOS / Web 三端都报"60fps 但用户反馈卡"。

度量与归因:切换到 P95/P99 后发现:

平台 均值 FPS P99 帧时长 主因
Android 59 180 ms 弹幕重绘 + GC 抖动
iOS 60 95 ms 图片加载偶发阻塞主线程
Web 58 220 ms 大量 DOM 节点 + reflow

治理:

  • Android:弹幕用独立 SurfaceView 隔离 + 对象池减 GC
  • iOS:图片改异步解码 + estimatedRowHeight
  • Web:虚拟滚动 + transform 替代 top/left

效果:

平台 P99 帧时长(before) P99 帧时长(after)
Android 180ms 22ms
iOS 95ms 20ms
Web 220ms 30ms

核心洞察:三端瓶颈完全不同,但用同一套度量体系(P99)发现问题——这就是统一指标的价值。

# 18.2 平台特异案例:Android ROM 厂商定制限制

背景:某 App 在某厂商 ROM 上滑动 FPS 持续低于其他厂商。

根因:

  • 该厂商默认开启"多任务模式",对非前台应用 CPU 限频
  • 应用滑动时主线程频繁被调度切走

治理:

  • 申请前台保护(系统 API)
  • 关键操作前请求 CPU boost(如有 SDK)
  • 优化降低对 CPU 持续占用需求

效果:滑动 P99 从 65ms → 28ms。

# 18.3 反例案例:Compose 误用导致 FPS 倒退

背景:某 App 用 Compose 重写后,长列表 FPS 比原 RecyclerView 还差。

根因:

@Composable
fun ItemRow(item: Item, onClick: () -> Unit) {
    Row(modifier = Modifier.clickable { onClick() }) { ... }
}

LazyColumn {
    items(list) { item ->
        ItemRow(item, onClick = { handleClick(item) })  // ❌ lambda 每次新建
    }
}
1
2
3
4
5
6
7
8
9
10

每次重组都新建 lambda,导致 Skipping 失效。

治理:

LazyColumn {
    items(list, key = { it.id }) { item ->  // ✅ 加 key
        ItemRow(item, onClick = onClickHandler)  // ✅ 复用 lambda
    }
}
1
2
3
4
5

效果:滚动 FPS 从 35 → 58。

洞察:Compose 不会自动给你"最优性能"——你必须理解 Skipping 机制(参数稳定 + 引用一致),否则 Compose 反而比传统 View 慢。


# 19.防劣化体系

优化不难,难的是"优化后不劣化"。需要"事前预防 + 事中拦截 + 事后回归"三道防线。

# 19.1 三道防线总览

   ┌────────────┐     ┌────────────┐     ┌────────────┐
   │ 编码期 Lint │  →  │ CI 卡口     │  →  │ 线上 SLO    │
   │ IDE 即时提示│     │ Macrobench  │     │ 监控告警    │
   └────────────┘     └────────────┘     └────────────┘
1
2
3
4

# 19.2 编码期 Lint

自定义规则:

  • 检测 onBindViewHolder 中的同步 IO 操作
  • 检测 Compose 中的 lambda capture(破坏 Skipping)
  • 检测 invalidate() 调用(建议 invalidate(Rect))
  • 检测硬编码的 16.67ms 帧预算(建议 dynamic)

# 19.3 CI 卡口

性能基线测试:

// Macrobenchmark FrameTimingMetric
@Test
fun scrollBenchmark() = benchmarkRule.measureRepeated(
    metrics = listOf(FrameTimingMetric()),
    iterations = 10
) {
    startActivityAndWait()
    val list = device.findObject(By.res("recyclerView"))
    list.fling(Direction.DOWN)
}
1
2
3
4
5
6
7
8
9
10

卡口规则:

  • P99 帧时长退化 ≥ 10% → 阻断 PR
  • P50 帧时长退化 ≥ 5% → 警告
  • FROZEN 帧出现 → 必须 review

# 19.4 线上 SLO

核心 SLO 指标:

指标 目标 告警阈值
滑动 P99 帧时长 < 25ms > 40ms
FROZEN 帧占比 < 0.1% > 0.3%
Max 帧时长 < 200ms > 500ms
ARR 高刷利用率 > 70% < 50%

# 19.5 文化建设

  • 性能预算:新页面必须申报"FPS 预算"
  • 性能 Code Review:动画/列表改动必须有 perf reviewer
  • 性能 OKR:列表 P99 进 OKR

探索性思考:为什么 FPS 防劣化比其他指标更难?因为 FPS 是"统计指标"——单次测试不可靠,必须看分布。这意味着 CI 上跑一次 Macrobench 不够,需要多次平均后才能判断退化。统计指标的防劣化需要"统计学严谨"——容易误判、容易漏判。


# 20.跨平台速查

# 20.1 工具速查

平台 帧时钟 系统帧 API 离线分析
Android Choreographer FrameMetrics / FrameTimeline Perfetto / systrace
iOS CADisplayLink MetricKit MXHitchEvent Instruments Time Profiler
Web requestAnimationFrame PerformanceObserver Chrome DevTools
Compose Choreographer FrameMetrics Compose Compiler Reports
Flutter SchedulerBinding DevTools Timeline flutter_driver
嵌入式 LCD IRQ 厂商 SDK 串口日志

# 20.2 关键 API 速查

目的 Android iOS Web
订阅帧时钟 Choreographer.postFrameCallback CADisplayLink rAF
获取阶段数据 FrameMetrics MXHitchEvent PerformanceObserver
设置目标刷新率 preferredRefreshRate preferredFrameRateRange 浏览器自适配
标记长任务 Trace.beginSection os_signpost Performance.mark
静态盲区兜底 LooperPrinter RunLoop observer requestIdleCallback
动态读刷新率 display.getRefreshRate() UIScreen.maximumFramesPerSecond window.screen.refreshRate

# 20.3 各平台 FPS 优化清单

Android:

  • [ ] 均值看板换 P50/P95/P99/Max
  • [ ] FrameMetrics + FrameTimeline 双采
  • [ ] onBindViewHolder 异步化
  • [ ] 大纹理 prepareToDraw
  • [ ] 动态读取 refreshRate
  • [ ] ARR 智能调度

iOS:

  • [ ] MetricKit Hitches Ratio 监控
  • [ ] cellForRowAtIndexPath 避免 IO
  • [ ] estimatedRowHeight
  • [ ] 避免 Off-screen Rendering
  • [ ] ProMotion preferredFrameRateRange

Web:

  • [ ] Long Tasks API 监控
  • [ ] 虚拟滚动
  • [ ] transform / opacity 做动画
  • [ ] CSS Containment
  • [ ] requestIdleCallback 推迟非紧急任务

# 21.总结与延伸

# 21.1 五条核心原则

  1. 分布先行:FPS 是抖动游戏,必看 P95/P99/Max,不看均值。
  2. 采集精度:每帧记录 + ≤1s 聚合,不要让窗口稀释信号。
  3. 三层校准:应用层 + 系统帧 API + 外部测量,弥补合成段盲区。
  4. 长尾治理:消灭 P99 比"提升均值"更影响用户感知。
  5. VRR 智能:高刷不是全程开,按内容切换是高端机的"必修课"。

# 21.2 五个常见误区

误区 真相
"均值 60fps 就是流畅" 错。P99 才与用户感知强相关
"1 秒采样就够" 错。需要每帧记录、避免窗口稀释
"120Hz 屏自动让应用流畅" 错。应用产帧能力是真正瓶颈
"应用层 Vsync 监控完整" 错。有合成段盲区
"全程 120Hz 最好" 错。耗电 30% 而用户察觉不到差异

# 21.3 一句话总结

FPS 是抖动游戏,不是均值游戏。用户感知的"流畅"取决于帧时长分布的尾部(P99/Max),而非平均值。所有 FPS 优化都要建立在"度量改对、采集可信"的基础上——度量错了,一切努力都是反向收益。

# 21.4 延伸阅读

  • 卷三·01 渲染管线与原理:FPS 是流水线的产出度量
  • 卷三·03 卡顿捕获与归因:卡顿是 FPS 抖动的极端形态
  • 卷三·04 ANR 监控与治理:极端长帧(5s+)的处理
  • 卷三·05 页面 UI 与布局优化:UI 优化的应用层落地
  • 卷三·06 动画交互响应优化:交互动效专项
  • 卷四·05 功耗与电量优化:高刷的能耗代价

下一篇预告:卷三·06 动画交互响应优化 —— 把"用户操作到画面响应"的最后一公里做好。

上次更新: 2026/06/07, 10:26:12
渲染管线与原理
卡顿捕获与归因

← 渲染管线与原理 卡顿捕获与归因→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式