编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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与帧率检测
      • 卡顿捕获与归因
      • ANR监控与治理
      • 页面UI与布局优化
        • 01.阅读说明
        • 02.贯穿案例
          • 2.1 案例背景
          • 2.2 经验派的 4 周折腾(典型反面教材)
          • 2.3 方法派的 6 天闭环
          • 2.4 上线效果
          • 2.5 案例如何串起本文
        • 03.UI 性能本质
          • 3.1 一句话定义
          • 3.2 反直觉问题清单
          • 3.3 现象与代价
          • 3.4 度量准则与基准
        • 04.三阶段成本原理
          • 4.1 三阶段成本公式
          • 4.2 优化优先级公式
          • 4.3 跨平台同构原理
          • 4.4 平台差异点矩阵
        • 05.度量与采集
          • 5.1 三类采集方案
          • 5.2 三种方案的总览与可见盲区
          • 5.3 跨平台采集对照表
          • 5.4 数据可信度评估
        • 06.归因决策树
          • 6.1 UI 问题决策树
          • 6.2 布局复杂度归因
          • 6.3 重绘范围归因
          • 6.4 Inflate 性能归因
        • 07.Inflate 全链路
          • 7.1 Android Inflate 全链路
          • 7.2 iOS Inflate 全链路
          • 7.3 Web Inflate 全链路
          • 7.4 跨端框架 Inflate 全链路
          • 7.5 Inflate 全链路性能数据
        • 08.Layout 全链路
          • 8.1 Android Layout 全链路
          • 8.2 iOS Layout 全链路
          • 8.3 Web Layout 全链路
          • 8.4 跨端 Layout 性能差异
        • 09.Draw 全链路
          • 9.1 Android Draw 全链路
          • 9.2 iOS Draw 全链路
          • 9.3 Web Paint 全链路
          • 9.4 重绘范围最小化原则
        • 10.列表复用全链路
          • 10.1 Android RecyclerView 复用全链路
          • 10.2 iOS UITableView / UICollectionView 复用全链路
          • 10.3 Web 虚拟滚动全链路
          • 10.4 跨端列表复用对照
        • 11.声明式 UI 全链路
          • 11.1 Compose 渲染全链路
          • 11.2 SwiftUI 渲染全链路
          • 11.3 React 渲染全链路
          • 11.4 声明式 vs 命令式性能对照
        • 12.跨端 UI 对照
          • 12.1 五个全链路总览
          • 12.2 各平台优化优先级
          • 12.3 反直觉问题答疑
        • 13.治理一层减节点
          • 13.1 减节点的四种手法
          • 13.2 删除:从业务侧砍
          • 13.3 合并:减少节点的同时不损失效果
          • 13.4 扁平化:用 ConstraintLayout(注意场景边界)
          • 13.5 延迟:ViewStub 与懒加载
        • 14.治理二层延 Inflate
          • 14.1 异步 Inflate(适合复杂页面)
          • 14.2 编译期生成(彻底跳过反射)
          • 14.3 ViewStub 延迟(最便宜的优化)
          • 14.4 RecyclerView 预创建(适合列表)
          • 14.5 跨平台 Inflate 延迟对照
        • 15.治理三层缩重绘
          • 15.1 局部 invalidate(最朴素)
          • 15.2 拆分独立 View(高频/低频隔离)
          • 15.3 setLayerType(缓存重绘结果)
          • 15.4 Web 的 GPU 加速正确姿势
          • 15.5 跨平台缩重绘对照
        • 16.治理四层 ROI 排序
          • 16.1 ROI 排序公式
          • 16.2 决策路径建议
          • 16.3 不要做的"伪优化"
          • 16.4 优化前的"度量门槛"
        • 17.求证实验 ⭐
          • 17.1 实验一:扁平化收益(场景边界)
          • 17.2 实验二:异步 Inflate 收益(场景边界)
          • 17.3 实验三:局部重绘收益
          • 17.4 实验四:列表复用与 onBindViewHolder 减负
          • 17.5 实验五:声明式 UI vs 命令式 UI
          • 17.6 五大实验启示
        • 18.实战案例
          • 18.1 跨端同构案例:电商详情页
          • 18.2 平台特异案例:Android 折叠屏适配
          • 18.3 Compose 反例:Skipping 失效导致重复重组
        • 19.防劣化体系
          • 19.1 三道防线总览
          • 19.2 编码期 Lint
          • 19.3 CI 卡口
          • 19.4 线上 SLO
          • 19.5 文化建设
        • 20.跨平台速查
          • 20.1 工具速查
          • 20.2 关键 API 速查
          • 20.3 各平台首屏优化清单
        • 21.总结与延伸
          • 21.1 五条核心原则
          • 21.2 五个常见误区
          • 21.3 一句话总结
          • 21.4 延伸阅读
      • 动画交互响应优化
    • 业务专项篇

    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

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

页面UI与布局优化

# 页面 UI 与布局优化

本文核心命题:UI 优化的本质是减少"产帧成本"——把 Layout / Draw / Inflate 三大主线程开销压缩到帧预算之内。一切 UI 优化都是在层级 / 重绘 / Inflate 三个维度上"减"或"延"。


# 01.阅读说明

  • 本文卷归属:卷三 · 流水线篇 · 第 5 篇
  • 本文目标层级:L2 进阶 → L3 专家
  • 适用平台:Android(主) / iOS / Web / 跨端框架 / 嵌入式
  • 前置阅读:
    • 卷三·01 渲染管线与原理(UI 优化是渲染优化的应用层落地)
    • 卷三·03 卡顿捕获与归因(UI 卡顿的归因)
  • 本文核心命题:

    UI 优化的本质是减少"产帧成本" —— 把 Layout / Draw / Inflate 这三大主线程开销压缩到帧预算之内。
    一切 UI 优化都是在层级 / 重绘 / Inflate 三个维度上"减"或"延"。

全文 21 章地图:

   §01 阅读说明        §02 贯穿案例        §03 UI 性能本质     §04 三阶段成本原理
   §05 度量与采集      §06 归因决策树
   §07 Inflate 全链路 ⭐  §08 Layout 全链路 ⭐  §09 Draw 全链路 ⭐
   §10 列表复用全链路 ⭐  §11 声明式 UI 全链路 ⭐  §12 跨端 UI 对照
   §13 治理一层减节点 ⭐  §14 治理二层延 Inflate ⭐  §15 治理三层缩重绘 ⭐  §16 治理四层 ROI 排序 ⭐
   §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 用三方案+决策树定位、§07-§11 五个全链路对应每一类问题、§17 用实验复盘、§13-§16 给出分层策略闭环。

# 2.1 案例背景

某新闻 App V6.8 重构详情页(产品要求"功能更丰富"),上线后用户体验崩盘:

  • 详情页打开 P95 = 1.8s(之前 0.6s),用户大量"白屏"投诉。
  • 评论列表滑动 FPS 仅 22(之前 56),P99 帧时长 110ms。
  • 详情页 cell 总创建时间长达 580ms(10 个 cell)。
  • DAU 流失 6%、文章阅读完成率 -15%。
  • 公司高层强力施压"必须立刻恢复"。

研发组初步反应:"功能加多了肯定慢点,是产品需求。"——这是典型的"功能 vs 性能"误区。

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

周次 动作 结果
第 1 周 把 LinearLayout 都改 ConstraintLayout(认为扁平就快) 简单 cell 反而慢 10%(CL 自身 overhead),详情页快 5%
第 2 周 全量异步 Inflate(认为异步就好) 简单 cell 也异步,Attach 闪烁 + 总耗时反增
第 3 周 用 setLayerType(LAYER_TYPE_HARDWARE) 加速所有 View GPU 内存涨 80MB,低端机 OOM 率升 5×
第 4 周 把列表 cell 高度固定(怀疑动态高度) FPS 升到 26,但内容显示不全

复盘:四周折腾错在"听经验,没数据"。ConstraintLayout 不是万能(简单场景反而慢);异步 Inflate 不是万能(简单 cell 没收益还闪烁);setLayerType 不是万能(吃显存);固定高度不是万能(牺牲业务)。每个所谓"经验"都需场景化验证——这正是案例反面教材的核心。

# 2.3 方法派的 6 天闭环

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

Day 1(§04 三阶段成本模型 + §05 三方案):

  • 方案②(FrameMetrics):详情页首帧 INPUT=8ms / ANIM=12ms / LAYOUT=520ms / DRAW=80ms / SWAP=8ms。
  • 方案①(自定义基类计时):详情页 setContentView 耗时 380ms(XML 层级达 9 层);评论 cell 层级 6 层。
  • 方案③(FPS):列表稳态 P99=110ms,远超 16.67ms 帧预算 6.6×。

→ 三阶段都有问题:Inflate(380ms)+ Layout 每帧(45ms)+ Draw 每帧(22ms)。

Day 2(§06 决策树 + 分类):

  • 首屏慢 → Inflate 分支 → 9 层深 + 大量未必要 View(社交/评论/相关推荐都同步 inflate)。
  • 滚动卡 → Layout 分支 → 6 层 cell + RelativeLayout 双测量 + 动态高度。
  • 局部卡 → Draw 分支 → 评论头像 + 表情用 SVG 实时解析。

Day 3-4(§13-§16 分层策略):

  • 第 1 层(节点收敛):详情页根布局扁平化(9 层→4 层);评论 cell ConstraintLayout(6 层→2 层);相关推荐改 ViewStub 延迟。
  • 第 2 层(Inflate 时机):评论/广告/相关推荐改 AsyncLayoutInflater;详情页主体保持同步(用户感知)。
  • 第 3 层(重绘):固定背景区域 setLayerType;SVG 改 WebP;只对动画区域 invalidate(Rect)。
  • 第 4 层(列表):onBindViewHolder 减负(图加载/格式化全异步);prefetchDataSource 提前测量。

Day 5(§17 实验思路验证):构造 mock 数据用 Macrobenchmark 跑前后对比。

Day 6(上线灰度):

# 2.4 上线效果

指标 经验派 4 周后 方法派 6 天后
详情页打开 P95 1.7s 0.55s
评论列表稳态 FPS 26 58
列表 P99 帧时长 100 ms 18 ms
单 cell 创建时间 50 ms 8 ms
内存峰值 380 MB 220 MB(setLayerType 滥用解除)
详情页 DAU 流失 -6% +1%(恢复)
文章阅读完成率 -15% +3%

核心洞察:UI 优化不存在"银弹"——每个"经验招"都有场景边界。方法派的胜利不是用了什么神奇技术,而是用数据驱动按场景选择:复杂层级才扁平化,复杂页面才异步 Inflate,动画区才用 setLayerType。经验招 + 错误场景 = 反向收益。

# 2.5 案例如何串起本文

  • §03 现象与代价 ▶▶ 业务损失映射:DAU -6%、阅读完成率 -15%。
  • §04 三阶段模型 ▶▶ 案例 Inflate/Layout/Draw 三阶段同时有问题,正是模型存在的理由。
  • §05 三方案组合 ▶▶ 单一方案看不到全貌,必须 ①②③ 配合。
  • §06 决策树 ▶▶ 三个症状走三条不同分支,对应三种治法。
  • §07-§11 五大全链路 ▶▶ Inflate/Layout/Draw/列表复用/声明式 UI 五个链路对应案例每一类问题。
  • §17 求证实验 ▶▶ 扁平化(场景边界)+ 异步 Inflate(场景边界)+ 列表复用 + 声明式 UI 都在案例中变现。
  • §13-§16 分层策略 ▶▶ "节点→Inflate→重绘→列表"四层正是案例落地路径。

探索性思考:为什么"经验派"会失败?不是经验本身错,而是经验缺少场景边界。"ConstraintLayout 比 LinearLayout 快"是在"嵌套 4 层以上"才成立;"异步 Inflate"是在"创建 > 50ms"才成立;"setLayerType"是在"高频重绘 + 内容稳定"才成立。没有场景的经验等于偏见——这是 UI 优化最容易踩的坑。


# 03.UI 性能本质

# 3.1 一句话定义

UI 优化 = 减少"主线程产出一帧"所需的工作量,把 Layout / Draw / Inflate 三大开销压到帧预算之内。

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

约束一:UI 工作量与"节点数"直接相关

无论 View / UIView / DOM / LVGL Object,UI 都是一棵树。Measure / Layout / Draw / Compose 都是树形递归算法。节点数越多,单帧成本越高(详见 卷三·01 §5.1 实验)。

约束二:UI 工作分摊在三个时间窗口

   ┌────────────────┐    ┌────────────────┐    ┌────────────────┐
   │ ① Inflate 阶段  │    │ ② 帧产出阶段    │    │ ③ 重绘阶段      │
   │ 仅创建时一次    │    │ 每帧执行       │    │ 局部触发        │
   │ 影响首屏时长   │    │ 影响 FPS       │    │ 影响交互流畅     │
   └────────────────┘    └────────────────┘    └────────────────┘
1
2
3
4
5

这三个阶段的优化手段完全不同,不能混为一谈:

  • Inflate 慢 → 异步 inflate / 缓存 / ViewStub
  • 帧产出慢 → 减少层级 / 简化 onDraw
  • 重绘慢 → 局部 invalidate / 合理的更新粒度

约束三:UI 工作在主线程上执行

UI 操作必须在主线程(详见 卷三·01 §2.1)。这意味着:

  • UI 优化的每一毫秒都在和"帧预算"赛跑。
  • 无法靠增加 CPU 核数加速。
  • 必须通过"减"或"延",而不是"并"。

# 3.2 反直觉问题清单

带着这些问题阅读:

  1. 减少 View 层级一定能让渲染更快吗?
  2. ConstraintLayout 一定比 LinearLayout 嵌套快吗?
  3. 异步 inflate 是不是总比同步快?
  4. wrap_content 真的慢吗?
  5. RelativeLayout 真的会"测量两次"吗?
  6. View 复用一定能加速滚动吗?
  7. 软件渲染(disable hardware acceleration)什么时候比 GPU 快?
  8. 页面层级 5 vs 10 实测差多少?

探索性思考:为什么"主线程"是 UI 设计的根本约束?因为 UI 状态需要串行一致性——如果 ListView 一边滑动一边异步改高度,渲染线程就要做协调,开销远大于"全部主线程"的简单方案。主线程是工程师不愿放弃的"宇宙秩序"——所有跨平台框架(Flutter / React Native / Compose)都没敢挑战这一点。

# 3.3 现象与代价

UI 性能问题的用户感知很直接:

  • 页面打开慢:点击后白屏 / 等待,启动期 Inflate 慢。
  • 滚动不流畅:列表滑动卡顿,每个 cell 创建 / 测量耗时长。
  • 切换迟钝:Activity / ViewController 之间切换有卡顿。
  • 键盘弹出卡顿:弹键盘时整个布局重新测量。
  • 旋转 / 折叠后慢:屏幕配置变化触发整树重新 Layout。

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

  • 头部 App:列表 P99 帧时长降 20ms,滑动留存 +1%。
  • 启动期 inflate 优化能让首屏可见时间 -200-500ms。
  • UI 卡顿是用户主观投诉最直接的来源("卡"占投诉 30%+)。

▶▶ 回扣 §02 案例:新闻 App 详情页 P95 1.8s + 列表 22fps 直接导致 DAU -6% + 阅读完成率 -15%——UI 性能 = 业务 KPI 不是夸张说法。

# 3.4 度量准则与基准

按 卷零·02 §3 指标体系:

请求视角(RED):

指标 含义 阈值参考
首屏 inflate 时长 从 setContentView 到 measure 完成 < 100ms
单 cell 创建耗时 onCreateViewHolder < 5ms
单 cell 绑定耗时 onBindViewHolder < 8ms
Layout 单次耗时 measure + layout < 帧预算 60%
Draw 单次耗时 onDraw < 帧预算 30%

行业基准:

平台 首屏 inflate 单 cell 创建 Layout 复杂度
Android < 100ms < 5ms 树深 ≤ 8
iOS < 80ms < 5ms 嵌套 ≤ 10
Web < 200ms(含 CSS) < 5ms DOM 节点 ≤ 1500
嵌入式 视设备 varies 严格规划

# 04.三阶段成本原理

# 4.1 三阶段成本公式

   单帧主线程总成本 = Inflate(首次) + Layout × N + Draw × M
   
   N = 当前帧需要重测量的节点数
   M = 当前帧需要重绘的节点数
1
2
3
4

关键洞察:

  • Inflate 是一次性成本(创建时一次),但成本最高(每个节点要解析 XML / 反射 / 创建对象)。
  • Layout 是每帧成本,与树深度指数相关(详见渲染篇 §5.1)。
  • Draw 是每帧成本,与重绘范围 + Shader 复杂度相关。

# 4.2 优化优先级公式

按"成本 × 频次":

   优先级 = 单次成本 × 触发频次

   ① Layout per 帧:成本 5-50ms × 60 帧/秒 = 300-3000 ms/秒(最高)
   ② Draw per 帧:成本 1-20ms × 60 帧/秒 = 60-1200 ms/秒
   ③ Inflate:成本 50-500ms × 1 次 = 50-500 ms(一次性,但首屏关键)
1
2
3
4
5

三类问题的物理来源:

   ┌────────────────────────────────────────────┐
   │ A. 层级过深:树形递归成本指数级               │
   │    根因:嵌套布局 / RelativeLayout 误用       │
   ├────────────────────────────────────────────┤
   │ B. 重绘范围过大:onDraw 被调用太多 / 太大     │
   │    根因:父布局 invalidate 全量;Shader 复杂  │
   ├────────────────────────────────────────────┤
   │ C. Inflate 慢:XML 解析 / 反射 / 资源加载    │
   │    根因:层级深 + 复杂控件 + 大量资源          │
   └────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10

▶▶ 回扣 §02 案例:新闻 App 同时命中三类——A(详情页 9 层 + 评论 6 层 RelativeLayout)+ B(SVG 实时解析 + 大背景 invalidate)+ C(首屏全量同步 inflate)。经验派的失败正是因为不分类——只治一类问题不可能解决三类共存的真实场景。三阶段模型存在的价值正是"分而治之"。

# 4.3 跨平台同构原理

所有平台的 UI 都是"声明式 / 命令式 + 树 + 渲染管线":

   通用 UI 模型:

      [声明 UI 树]   →   [Inflate 创建实例]   →   [Layout]   →   [Draw]   →   [Compose]
       声明文件             实例化对象               位置确定        画到 canvas    上屏
       XML/JSX/SwiftUI    Java 对象/JS DOM/UIView   Measure+Layout  onDraw         GPU 合成
1
2
3
4
5

跨平台术语对照

通用术语 Android iOS Web 嵌入式
树根 View UIView DOM LVGL Obj
节点 View / ViewGroup UIView / Layer Element Obj
测量 onMeasure sizeThatFits / layoutSubviews reflow 自定义
布局 onLayout layoutSubviews reflow 自定义
绘制 onDraw drawRect / Layer.draw paint 自定义
重绘 invalidate setNeedsDisplay requestAnimationFrame + DOM 修改 invalidate
异步实例化 AsyncLayoutInflater dispatchqueue 创建 Web Worker 不常见

# 4.4 平台差异点矩阵

维度 Android iOS Web 嵌入式
声明方式 XML / Compose XIB / Storyboard / SwiftUI HTML / CSS / JSX C 代码 / LVGL
实例化成本 高(XML 解析 + 反射) 中(XIB 高,代码低) 中(DOM 创建) 低
树深限制 经验 ≤ 8 经验 ≤ 10 浏览器有上限(巨深会卡) 严格
重绘粒度 View 级 Layer 级 Element 级 Obj 级
异步 inflate AsyncLayoutInflater 不常用 自动(浏览器) 罕见
主流模式 RecyclerView 复用 UITableView 复用 虚拟滚动 列表组件

探索性思考:为什么"减层级"是跨平台共通的优化?因为递归遍历的成本本质上是 O(N) ~ O(N·D)(D 为深度)。在简单平台(嵌入式)这是常识;在复杂平台(Web)也成立——浏览器 reflow 一旦触发就要遍历相关 DOM 树。跨平台共通的优化通常都来自"算法第一性",平台特性带来的优化反而是"末梢"。


# 05.度量与采集

# 5.1 三类采集方案

所有平台的 UI 性能采集方案,本质上只有 3 类:

   ① 节点级采集(每个 View 的测量 / 布局耗时)
                ② 阶段级采集(FrameMetrics / DevTools 阶段拆解)
                            ③ 全局级采集(FPS、INP 等用户感知)
1
2
3

① 节点级采集(精细到单个 View)

核心原理:在 onMeasure / onLayout / onDraw 等关键回调上插桩,记录每个节点的耗时。

// 自定义 View 基类做计时
public class TimedView extends View {
    @Override
    protected void onMeasure(int wMs, int hMs) {
        long t = System.nanoTime();
        super.onMeasure(wMs, hMs);
        Tracer.record("Measure", getClass().getSimpleName(), System.nanoTime() - t);
    }
}
1
2
3
4
5
6
7
8
9

或者 ASM / Aspect 编译期注入。

适用边界:性能分析阶段、定向排查特定页面。线下用为主。

② 阶段级采集

使用系统提供的"阶段级"数据,看 Layout / Draw / Sync 各阶段耗时(详见 卷三·01 §3.1)。所有应用线上监控的核心方案。

③ 全局级采集

用 FPS / INP / TTI 等用户感知指标。监控仪表盘、SLO 跟踪。

# 5.2 三种方案的总览与可见盲区

方案 钩子位置 数据粒度 性能开销 跨端通用性 线上可用 主要局限
① 节点级 onMeasure/Layout/Draw 单 View 级 高(5-15%) 需各平台插桩 否 侵入式
② 阶段级 FrameMetrics/PerfHUD 阶段级 低(1-2%) 跨平台支持 是 不知具体 View
③ 全局级 FPS/INP/TTI 帧/页面级 极低 全平台 是 不知阶段

可见盲区:

  • 方案①看得到具体哪个 View 慢,但开销大;
  • 方案②看得到哪个阶段慢(Layout/Draw),但不知是哪个 View;
  • 方案③看得到"哪个页面"卡,但不知"哪个阶段"。

实战建议:线上用 ② + ③;线下问题排查用 ①。

# 5.3 跨平台采集对照表

平台 节点级 阶段级 全局级
Android 自定义基类 / ASM FrameMetrics / Perfetto Choreographer / FPS
iOS 重写 layoutSubviews 计时 Instruments Time Profiler CADisplayLink / FPS
Web Performance.measure() Performance Long Tasks API INP / FPS
Compose Layout Inspector composition trace Choreographer
SwiftUI _printChanges() Instruments CADisplayLink

# 5.4 数据可信度评估

  • 节点级采集:可信度最高,但开销影响数据本身(heisenberg 效应)。
  • 阶段级采集:可信度高(系统级),是线上首选。
  • 全局级采集:可信度高,但延迟感知(已经发生才能知道)。

探索性思考:为什么 UI 性能采集没有"完美方案"?因为采集本身就是 UI 工作——你想精确测量,就要插桩,插桩就增加开销,增加开销就改变测量值。Heisenberg 不确定性原理在工程上的应用:你只能在"颗粒度"和"开销"之间二选一。


# 06.归因决策树

# 6.1 UI 问题决策树

   症状 = ?
      │
      ├─ 首屏打开慢(白屏 200-1500ms)
      │     │
      │     └─ 走 Inflate 分支:检查层级深度、ViewStub 是否使用、
      │                          复杂控件是否同步初始化、资源解码
      │
      ├─ 滚动卡顿(FPS < 50 / 帧时长 > 20ms)
      │     │
      │     └─ 走 Layout 分支:检查 cell 层级、动态高度、
      │                       RelativeLayout 双测量、过度嵌套
      │
      ├─ 局部卡顿(点击/动画时卡)
      │     │
      │     └─ 走 Draw 分支:检查重绘范围、Shader 复杂度、
      │                     setLayerType、SVG 实时解析
      │
      └─ 内存暴涨(GPU/RAM)
            │
            └─ 走资源分支:检查 setLayerType 滥用、Bitmap 缓存策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 6.2 布局复杂度归因

症状:滚动卡顿、Layout 阶段长。

典型根因:

  1. 层级过深:树深 > 8 层(Android)或 > 10 层(iOS)
  2. RelativeLayout 双测量:含 layout_above / layout_below 时会触发两次 onMeasure
  3. 嵌套 wrap_content:每一层都需要先测量子节点再确定自己尺寸
  4. 动态高度列表:列表滚动时不断重新测量
  5. 过度使用 weight:LinearLayout weight 也会触发两次测量

归因方法:

# Android
adb shell dumpsys gfxinfo <package> framestats | grep -A 5 "Layout"
# 看 LAYOUT 列耗时占比
1
2
3

# 6.3 重绘范围归因

症状:动画/交互卡顿、Draw 阶段长。

典型根因:

  1. invalidate 范围过大:调用无参数 invalidate() 而非 invalidate(Rect)
  2. 父布局重绘传递:父 View 重绘导致整棵子树重绘
  3. 复杂 onDraw:自定义 View 中调用复杂 Path / Shader
  4. OffscreenBuffer:setLayerType(LAYER_TYPE_HARDWARE) 滥用
  5. 过度透明合成:多层半透明叠加导致 GPU overdraw

归因方法:

  • Android:开发者选项打开"GPU 过度绘制"
  • iOS:Instruments → Color Blended Layers / Color Off-screen Rendered
  • Web:Chrome DevTools → Rendering → Paint Flashing

# 6.4 Inflate 性能归因

症状:页面打开慢。

典型根因:

  1. XML 层级深:每一层都要解析 XML / 反射创建
  2. 同步 Inflate:所有 View 都在主线程创建
  3. 复杂资源:大图 / SVG 在 Inflate 时解析
  4. 未用 ViewStub:低频显示的 View 也立即创建
  5. 未用 RecyclerView 复用:列表 cell 全部新建

归因方法:

  • Android:Profile GPU Rendering 配合 Trace
  • iOS:Time Profiler 看 -[UINib instantiateWithOwner:options:]
  • Web:Performance Panel 看 Layout / Recalculate Style

探索性思考:为什么"决策树"是 UI 归因的最佳模式?因为 UI 问题的症状-病因映射通常是 1 对 1 而非多对多——首屏慢几乎等同于 Inflate 问题,滚动卡几乎等同于 Layout 问题。强分类的领域适合用决策树,弱分类的领域才需要机器学习。UI 优化领域是前者。


# 07.Inflate 全链路

Inflate 是 UI 创建的"启动开销":XML/XIB 解析 → 反射创建对象 → 资源加载 → attach 到 ViewTree。首屏慢的 70% 都来自 Inflate。

# 7.1 Android Inflate 全链路

   setContentView(R.layout.xxx)
      ↓
   ① LayoutInflater.inflate() → 解析 XML(从 R.layout 资源)
      - 资源解析(bytecode 形式的 XML)
      - 创建 XmlPullParser
      ↓
   ② createViewFromTag()
      - 反射 Class.forName("android.widget.LinearLayout")
      - 反射 Constructor.newInstance(context, attrs)
      ↓
   ③ generateLayoutParams()  → 解析 layout_width / layout_height
      ↓
   ④ rInflateChildren() 递归解析子节点
      ↓
   ⑤ attach 到 root ViewGroup → setContentView 完成
      ↓
   ⑥ ViewRootImpl.requestLayout() → 后续 measure / layout / draw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关键瓶颈:

  • 反射创建 View:每个 Tag 都要走 Class.forName + newInstance
  • 属性解析:每个 attr 都要做类型转换
  • 嵌套递归:深度 N 的树需要 N 次递归
  • 资源加载:drawable / dimen 立即加载

优化机会点:

// ① AsyncLayoutInflater:把 ① ② ③ ④ 都丢到子线程
AsyncLayoutInflater(context).inflate(R.layout.xxx, parent) { view, _, _ ->
    parent.addView(view)  // 主线程仅做 attach
}

// ② Compose / 编译期生成:跳过 ① 和 ②(无反射)

// ③ X2C / 注解处理:编译期生成 Java 代码替代 XML 反射

// ④ ViewStub:延迟 ①-④ 到真正显示时
1
2
3
4
5
6
7
8
9
10

# 7.2 iOS Inflate 全链路

   loadView() / loadFromNib()
      ↓
   ① UINib.instantiateWithOwner:
      - XIB 文件解析(已编译为 NIB 二进制)
      - Archive 反序列化
      ↓
   ② [view setValue: forKey:]  // KVC 设置属性
      ↓
   ③ awakeFromNib  // 子类回调
      ↓
   ④ viewDidLoad
      ↓
   ⑤ viewWillAppear / viewDidAppear
1
2
3
4
5
6
7
8
9
10
11
12
13

与 Android 的差异:

  • iOS 没有"反射"问题(NIB 已编译)
  • 但 KVC 设置属性也有开销
  • 代码创建 View 比 XIB 快 30-50%(实测)

优化建议:

  • 性能关键 View:用代码创建(避免 XIB)
  • 复用:UITableView / UICollectionView 的 dequeueReusableCell

# 7.3 Web Inflate 全链路

   document.createElement("div")
      ↓
   ① 创建 DOM 节点(C++ 内核中分配)
      ↓
   ② parent.appendChild(child)
      ↓
   ③ 触发 Style Recalculation(CSS 匹配)
      ↓
   ④ 触发 Layout(reflow)
      ↓
   ⑤ 触发 Paint
      ↓
   ⑥ 触发 Composite
1
2
3
4
5
6
7
8
9
10
11
12
13

关键瓶颈:

  • CSS 匹配:选择器越复杂越慢(嵌套选择器 O(N²))
  • layout thrashing:element.offsetHeight; element.style.width = "100px"; element.offsetTop 强制三次 layout
  • Shadow DOM 创建:Web Components 比原生 div 慢

优化机会:

// ① documentFragment 批量插入
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    frag.appendChild(createNode());
}
parent.appendChild(frag);  // 一次 reflow

// ② 虚拟 DOM(React/Vue):内存中构造完整树后一次性 mount
1
2
3
4
5
6
7
8

# 7.4 跨端框架 Inflate 全链路

Flutter:

   Widget tree(声明)→ Element tree(实例)→ RenderObject tree(渲染)
      build() 每次都创建新 Widget(cheap)
      Element 复用(mount/unmount 才动)
      RenderObject 极少重建
1
2
3
4

React Native:

   JSX → React Element → ReactShadowNode → 原生 View
   Bridge 传递(异步序列化)→ 原生侧创建 View
1
2

Compose / SwiftUI:

   @Composable 函数运行 → Slot table → LayoutNode → Skia Canvas
   声明式 UI 的"重建"是廉价的(diff + 复用)
1
2

探索性思考:为什么"声明式 UI"反而能更快?因为它把"实例化"从运行时移到了"框架内部" —— Compose 的 LayoutNode、Flutter 的 RenderObject 都是为复用而设计,运行时只是函数调用 + diff,没有反射、没有 XML 解析。声明式 UI 的胜利不是 API 优雅,而是把昂贵的工作做了一次架构性的"位移"。

# 7.5 Inflate 全链路性能数据

阶段 Android(实测) iOS(实测) Web(实测)
解析 + 反射创建 50-200ms(10 节点) 30-80ms 5-30ms
KVC 属性设置 包含在反射中 10-30ms CSS 匹配 5-50ms
Attach + 测量 20-100ms 10-50ms reflow 10-100ms

▶▶ 回扣 §02 案例:详情页 setContentView 380ms,正是这条链路的真实代价。优化路径:① 减节点数(9→4 层)② 异步 Inflate(评论/广告)③ ViewStub 延迟(相关推荐)。


# 08.Layout 全链路

# 8.1 Android Layout 全链路

   ViewRootImpl.performLayout()
      ↓
   ① performMeasure(widthSpec, heightSpec)
      → ViewGroup.measure → onMeasure → 子 View.measure → ...
      (递归测量)
      ↓
   ② performLayout(left, top, right, bottom)
      → ViewGroup.layout → onLayout → child.layout → ...
      (递归布局)
      ↓
   ③ measureChildWithMargins → resolveSizeAndState
      MeasureSpec 三种模式:EXACTLY / AT_MOST / UNSPECIFIED
1
2
3
4
5
6
7
8
9
10
11
12

双测量原因:

  • RelativeLayout:含 layout_above / layout_below / layout_alignBaseline 时
  • LinearLayout + weight:先一次测量得到剩余空间,再一次分配
  • wrap_content + match_parent 嵌套:子节点需要先确定,父节点需要等子节点

优化机会:

  • ConstraintLayout:扁平化 + 一次性测量(避免双测量)
  • 避免 wrap_content + 嵌套 weight
  • 固定尺寸 cell:如果业务允许

# 8.2 iOS Layout 全链路

   setNeedsLayout
      ↓
   ① layoutIfNeeded  // 立即布局
   ② layoutSubviews  // 子类重写
      ↓
   ③ Auto Layout:
      - 添加 NSLayoutConstraint
      - Cassowary 算法求解
      - 应用 frame
   ④ 手动布局:
      - 直接 view.frame = CGRect(...)
1
2
3
4
5
6
7
8
9
10
11

Auto Layout 的代价:

  • Cassowary 算法时间复杂度 O(N²) 至 O(N³)
  • 100 + 约束的 cell 测量 ~5ms
  • 手动 frame 计算 < 0.5ms

优化建议:

  • 列表 cell:手动 frame > Auto Layout(约 3-5×)
  • 静态页面:Auto Layout 可接受

# 8.3 Web Layout 全链路

   修改 DOM / Style
      ↓
   ① Invalidate Layout(标脏)
      ↓
   ② Recalculate Style(CSS 计算)
      ↓
   ③ Layout / Reflow
      - block / inline 流计算
      - flex / grid 求解
      - 100% / auto 尺寸求解
      ↓
   ④ Paint
      ↓
   ⑤ Composite
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Layout Thrashing 反模式:

// 错误:强制同步 layout 三次
for (let el of items) {
    el.style.width = el.offsetWidth + "px";  // 写后立即读 → 强制 layout
}

// 正确:批量读 + 批量写
const widths = items.map(el => el.offsetWidth);  // 批量读
items.forEach((el, i) => el.style.width = widths[i] + "px");  // 批量写
1
2
3
4
5
6
7
8

# 8.4 跨端 Layout 性能差异

场景 Android Layout iOS AutoLayout Web Reflow Compose / SwiftUI
静态 100 节点 5-15ms 5-30ms 10-30ms 3-10ms
动态 100 节点 5-50ms 10-100ms 10-100ms 5-30ms
1000 节点列表 不可接受(必复用) 不可接受 不可接受 不可接受

探索性思考:为什么"双测量"在 Android RelativeLayout 上是必要恶?因为相对布局需要"对齐其他 View",而被对齐的 View 必须先确定位置。这是数学上的依赖图问题 —— ConstraintLayout 通过求解器(不是双测量)解决了这个问题,代价是引入求解器的算法常数(简单场景反而更慢)。没有银弹。


# 09.Draw 全链路

# 9.1 Android Draw 全链路

   View.invalidate()
      ↓
   ① ViewRootImpl 标脏
      ↓
   ② doFrame → performTraversals → performDraw
      ↓
   ③ View.draw(canvas)
      → onDraw  // 自定义绘制
      → dispatchDraw  // 子 View 绘制
      ↓
   ④ DisplayList(HWUI)记录绘制命令
      ↓
   ⑤ RenderThread → OpenGL ES → GPU
      ↓
   ⑥ SurfaceFlinger Composite
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关键开销:

  • invalidate 范围:无参 invalidate 重绘整个 View
  • Shader 复杂度:复杂 Path / 渐变 / 模糊
  • OffscreenBuffer:setLayerType(HARDWARE) 创建独立缓冲区
  • OverDraw:多层叠加,相同像素被绘制多次

# 9.2 iOS Draw 全链路

   setNeedsDisplay
      ↓
   ① CALayer 标脏
      ↓
   ② Core Animation transaction
      ↓
   ③ drawRect: / Layer.draw  // 自定义
      ↓
   ④ CGContextRef 上的 Quartz 2D 绘制
      ↓
   ⑤ 上传到 GPU
      ↓
   ⑥ Compositor 合成(带圆角/阴影时)
1
2
3
4
5
6
7
8
9
10
11
12
13

iOS 关键性能点:

  • Off-screen Rendering:圆角 + maskToBounds、阴影、shouldRasterize 都触发
  • 每像素采样:模糊 / 阴影 cost 与像素数线性相关
  • Layer 合并:Core Animation 自动合并相邻 Layer

# 9.3 Web Paint 全链路

   修改样式(color/background/transform)
      ↓
   ① 标记 paint dirty
      ↓
   ② 浏览器内部 Display List
      ↓
   ③ 栅格化(Rasterization)
      ↓
   ④ Compositor 合成
1
2
3
4
5
6
7
8
9

优化路径:

  • transform / opacity 动画:跳过 layout & paint,直接 composite
  • will-change:提示浏览器为该元素创建独立合成层
  • 避免 box-shadow / filter 频繁变化:触发 paint

# 9.4 重绘范围最小化原则

   修改 → 标脏 → 重绘
      ↑                ↑
      最小化            最小化
1
2
3

最小化标脏:

  • Android:调用 invalidate(Rect rect) 而非 invalidate()
  • iOS:setNeedsDisplayInRect:
  • Web:避免操作影响 layout 的属性

最小化重绘:

  • 拆分独立 layer / 独立 View
  • 把"频繁变化"的部分独立成一个 View
  • 静态部分用 setLayerType(HARDWARE) 缓存(注意 GPU 内存代价)

探索性思考:为什么 Web 的 transform 动画"零重绘"?因为浏览器把它放在 Compositor 线程上执行——transform 矩阵只是改变 Layer 的合成参数,不需要重新栅格化。这是 GPU 加速的正确用法 —— 大多数移动端的"丝滑动画"都基于这一原理(iOS Core Animation、Android RenderThread 都同此设计)。


# 10.列表复用全链路

列表是 UI 性能的"重灾区"——一个 1000 项的列表如果没有复用,要创建 1000 个 cell,Inflate 成本极高且内存爆掉。复用是列表的"灵魂"。

# 10.1 Android RecyclerView 复用全链路

   RecyclerView.onLayout()
      ↓
   ① LayoutManager 决定可见范围
      ↓
   ② 对每个可见位置:tryGetViewHolderForPositionByDeadline()
      → ① Scrap(屏幕内已有的)
      → ② Cache(recyclePool 缓存的)
      → ③ ViewCacheExtension(自定义)
      → ④ RecycledViewPool(复用池)
      → ⑤ getViewForPosition → onCreateViewHolder(最贵)
      ↓
   ③ onBindViewHolder(绑定数据)
      ↓
   ④ 加入 ViewGroup
1
2
3
4
5
6
7
8
9
10
11
12
13
14

关键瓶颈:

  • onCreateViewHolder:每次未命中复用都要 inflate
  • onBindViewHolder:每滚动一行触发,里面绝不能 IO / 解码 / 复杂计算

优化机会:

// ① 预创建 ViewHolder(PrefetchPool)
recyclerView.recycledViewPool.setMaxRecycledViews(TYPE_NORMAL, 20)

// ② onBindViewHolder 减负:图片/格式化全异步
override fun onBindViewHolder(holder: VH, position: Int) {
    holder.title.text = data[position].title  // 简单赋值
    Glide.with(...).into(holder.image)  // 异步加载
    // ❌ 禁止 holder.text = formatComplexDate(data[position].time)
}

// ③ DiffUtil:只更新变化的 item
DiffUtil.calculateDiff(callback).dispatchUpdatesTo(adapter)

// ④ stableIds:避免重复创建
adapter.setHasStableIds(true)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10.2 iOS UITableView / UICollectionView 复用全链路

   tableView.reloadData() / 滚动
      ↓
   ① cellForRowAtIndexPath  // 询问 cell
      ↓
   ② dequeueReusableCellWithIdentifier
      → 命中:复用
      → 未命中:实例化 cell
      ↓
   ③ 配置 cell 属性
      ↓
   ④ heightForRowAtIndexPath(如果不是 estimatedRowHeight)
1
2
3
4
5
6
7
8
9
10
11

iOS 优化要点:

  • estimatedRowHeight:异步算高度,避免一开始就遍历所有 cell
  • prefetchDataSource (iOS 10+):预加载即将出现的 cell 数据
  • cell 用代码创建 > XIB(性能)
  • cellForRowAtIndexPath 减负:图片/格式化全异步

# 10.3 Web 虚拟滚动全链路

Web 没有原生复用机制,需要"虚拟滚动"实现:

   监听 scroll
      ↓
   ① 计算当前可见区域
      ↓
   ② 渲染可见的 N+buffer 个项目(DOM 中只存在 N 个节点)
      ↓
   ③ 滚出可视区的 DOM 移除(或复用)
1
2
3
4
5
6
7

主流方案:

  • React-Window / React-Virtualized
  • Vue: vue-virtual-scroller
  • Vanilla:手写或 IntersectionObserver

# 10.4 跨端列表复用对照

平台 复用机制 单 cell 创建 适用规模
Android RecyclerView RecycledViewPool 5-20ms 10000+
iOS UITableView dequeueReusableCell 5-15ms 10000+
iOS UICollectionView dequeueReusableCell 5-20ms 10000+
Web Virtual Scroll 自定义/库 1-5ms 5000+
Flutter ListView.builder 内置懒加载 3-10ms 10000+
Compose LazyColumn 内置懒加载 3-10ms 10000+

探索性思考:为什么"复用"是列表的唯一解?因为一个列表的真实可见数量永远是 ~10 个 item(屏幕高度限制),而总数据量可能是 10000 个。"创建 10000 个对象只为了滚动到第 9999 个"在数学上就是浪费。复用是把"列表大小"和"内存大小"解耦的工程范式 —— 这是所有平台共通的智慧。


# 11.声明式 UI 全链路

# 11.1 Compose 渲染全链路

   @Composable 函数 setContent {}
      ↓
   ① Composer:构建 Slot Table(中间表示)
      ↓
   ② Composition:把 @Composable 调用变为 LayoutNode 树
      ↓
   ③ Measure phase:measure() on each LayoutNode
      ↓
   ④ Layout phase:place() on each LayoutNode
      ↓
   ⑤ Draw phase:onDraw() on Skia Canvas
      ↓
   ⑥ 上屏(与 View 体系一样的 RenderThread)
1
2
3
4
5
6
7
8
9
10
11
12
13

核心机制:

  • Smart Recomposition:只重新执行依赖了变化 state 的 @Composable
  • Skipping:参数没变的 @Composable 直接跳过
  • Layout 模型:Compose 是"一次测量",比 View 的 RelativeLayout 双测量快

# 11.2 SwiftUI 渲染全链路

   var body: some View
      ↓
   ① ViewBuilder 构造 View 树(值类型,不是引用)
      ↓
   ② ViewGraph 构建(内部数据结构)
      ↓
   ③ Layout:top-down 询问"建议尺寸" + bottom-up 返回"实际尺寸"
      ↓
   ④ Render:转换为 CALayer / Metal 调用
1
2
3
4
5
6
7
8
9

SwiftUI 的 Layout 系统:

  • 父向子询问"我建议你这么大"
  • 子返回"我接受这么大"
  • 父决定最终位置

# 11.3 React 渲染全链路

   JSX → React Element(虚拟 DOM)
      ↓
   ① Reconciliation(diff):找出最小变更
      ↓
   ② Fiber 调度:可中断的渲染
      ↓
   ③ Commit phase:批量应用 DOM 操作
      ↓
   ④ 浏览器 Layout / Paint
1
2
3
4
5
6
7
8
9

# 11.4 声明式 vs 命令式性能对照

场景 命令式(View/UIView) 声明式(Compose/SwiftUI/React)
静态 100 节点首屏 50-200ms 30-100ms
动态状态变更 触发整 ViewGroup invalidate 智能 diff 只重渲变化部分
列表复用 手动 dequeue 框架自动
主线程占用 高 中(部分工作可异步)

Compose 在某些场景反而慢的原因:

  • 首屏多了 Slot Table 构建开销
  • 简单页面(< 20 节点)开销大于收益
  • Skipping 失效(用了 lambda capture)会触发不必要重组

探索性思考:为什么 Compose / SwiftUI 没有"完全替代"传统 View?因为它们的性能优势集中在"动态变化频繁"的场景 —— 静态简单页面,传统 View 反而更快。架构演进不是替代而是分化:复杂动态页面用声明式,简单静态页面继续命令式。最佳实践都是"混合架构"。


# 12.跨端 UI 对照

# 12.1 五个全链路总览

链路 Android iOS Web Flutter Compose
Inflate XML+反射 XIB / 代码 DOM+CSS Widget→Element Slot Table
Layout onMeasure/onLayout layoutSubviews / Auto Layout reflow RenderObject LayoutNode
Draw onDraw + HWUI drawRect + Core Animation paint + Compositor Skia Skia
列表复用 RecyclerView UITableView 虚拟滚动 ListView.builder LazyColumn
声明式 Compose SwiftUI React/Vue Widget Compose

# 12.2 各平台优化优先级

Android:

  1. 减层级(ConstraintLayout 用得对)
  2. AsyncLayoutInflater
  3. RecyclerView 复用配合 prefetch
  4. ViewStub 延迟 inflate

iOS:

  1. 代码创建 cell(vs XIB)
  2. estimatedRowHeight 异步算高
  3. dequeueReusableCell 配合 prefetchDataSource
  4. 避免 Off-screen Rendering(圆角/阴影)

Web:

  1. 减少 DOM 节点
  2. 用 transform/opacity 做动画(跳过 layout)
  3. 虚拟滚动
  4. CSS containment(contain: layout)

Compose / SwiftUI:

  1. remember 减少不必要重组
  2. 拆分小的 @Composable
  3. LazyColumn / LazyVStack 替代 Column
  4. derivedStateOf 派生状态

# 12.3 反直觉问题答疑

问题 答案
减少 View 层级一定能让渲染更快吗? 是(核心原理),但 ConstraintLayout 的 overhead 在简单场景反向
ConstraintLayout 一定比 LinearLayout 嵌套快吗? 嵌套 ≥ 4 层时是;2-3 层反而慢
异步 inflate 是不是总比同步快? 不是,复杂度 < 50ms 的 cell 异步更慢(attach 闪烁)
wrap_content 真的慢吗? 嵌套时慢(双测量),单层不慢
RelativeLayout 真的会"测量两次"吗? 是,但仅当含 layout_above/below/baseline 时
View 复用一定能加速滚动吗? 是,无复用 1000 cell 必 OOM
软件渲染什么时候比 GPU 快? 极少数低端机或简单 2D(GPU 上下文切换开销大于 CPU 绘制)
页面层级 5 vs 10 实测差多少? 5 vs 10 层差 30-80ms(Inflate)+ 5-15ms(每帧 Layout)

▶▶ 回扣 §02 案例:经验派的失败 100% 命中"反直觉问题"——不分场景滥用经验招。这正是写本节的意义:每个"经验"都有边界,没有边界的经验等于偏见。


# 13.治理一层减节点

核心命题:任何 UI 优化的第一步都是减少节点数。这是物理约束(节点数 = 工作量基数),任何上层优化都建立在这个基础上。

# 13.1 减节点的四种手法

   ① 删除:业务上确实不需要的节点直接删
   ② 合并:用一个节点表达多个节点的视觉效果
   ③ 扁平化:用 ConstraintLayout / Auto Layout 替代嵌套
   ④ 延迟:ViewStub / 懒加载,需要时才创建
1
2
3
4

# 13.2 删除:从业务侧砍

最被低估但最有效的优化:问产品经理"这个 View 真的需要吗"。

典型可砍场景:

  • 装饰性的 divider(用 background 替代)
  • 占位 View(FrameLayout 里嵌一层 LinearLayout)
  • 调试残留 View
  • "未来可能用到"的 View

# 13.3 合并:减少节点的同时不损失效果

Android merge / include:

<!-- 错误:嵌套 ViewGroup -->
<FrameLayout>
    <LinearLayout>
        <TextView />
        <TextView />
    </LinearLayout>
</FrameLayout>

<!-- 正确:用 merge 减少一层 -->
<merge>
    <TextView />
    <TextView />
</merge>
1
2
3
4
5
6
7
8
9
10
11
12
13

用单个 View 表达视觉效果:

<!-- 错误:3 个 View 表达"标题+小图标" -->
<LinearLayout>
    <ImageView />
    <TextView />
</LinearLayout>

<!-- 正确:用 drawableLeft 在 TextView 上 -->
<TextView android:drawableLeft="@drawable/icon" />
1
2
3
4
5
6
7
8

# 13.4 扁平化:用 ConstraintLayout(注意场景边界)

ConstraintLayout 适用场景:

  • 嵌套层级 ≥ 4 层
  • 包含相对定位需求
  • 需要 chain / barrier / guideline 等高级特性

ConstraintLayout 不适用场景:

  • 简单 LinearLayout(< 5 子 View)
  • 单层水平/垂直排列

实测数据(详见 §17 实验一):

场景 LinearLayout ConstraintLayout
嵌套 5 层 25ms 8ms(3.1× 提升)
单层 3 个 View 1.2ms 1.5ms(反而慢)

# 13.5 延迟:ViewStub 与懒加载

ViewStub:

<ViewStub
    android:id="@+id/stub_comment"
    android:layout="@layout/comment_section"
    android:visibility="gone" />
1
2
3
4
// 真正需要时才 inflate
findViewById<ViewStub>(R.id.stub_comment).inflate()
1
2

懒加载场景:

  • 错误页面(90% 不显示)
  • 评论区(用户可能不下滑)
  • 广告位(部分场景不展示)
  • 设置项的展开内容

探索性思考:为什么"减节点"是最难做的?因为减节点经常需要业务妥协——产品想加 5 个 View,开发要砍掉 2 个。这是"政治问题而非技术问题"。真正的高手不是会扁平化布局,而是能说服产品砍 View。技术 < 沟通。

▶▶ 回扣 §02 案例:详情页 9 层→4 层是减节点的主战场——Day 3 的"扁平化"不是简单技术问题,而是和产品 PK 后砍掉了非必要的相关推荐区(移到 ViewStub 延迟)。


# 14.治理二层延 Inflate

核心命题:把昂贵的 Inflate 工作"延后"或"移走"——让用户先看到关键内容,次要内容慢慢加载。

# 14.1 异步 Inflate(适合复杂页面)

Android AsyncLayoutInflater:

AsyncLayoutInflater(context).inflate(R.layout.complex_section, parent) { view, _, _ ->
    parent.addView(view)
}
1
2
3

注意事项:

  • 子线程不能用 ContextCompat.getColor 等需要主线程资源的 API
  • inflate 完成后必须主线程 attach
  • 简单 cell 不要异步(attach 闪烁 + 总耗时反增)

# 14.2 编译期生成(彻底跳过反射)

X2C / XmlToJava:编译期把 XML 转为 Java/Kotlin 代码,跳过运行时反射。

收益:

  • 跳过 XML 解析
  • 跳过反射 newInstance
  • 通常加速 30-50%

代价:

  • 编译时间增加
  • 调试复杂度增加

# 14.3 ViewStub 延迟(最便宜的优化)

如 §13.5 所述。首屏只 inflate "用户首次看到"的部分。

# 14.4 RecyclerView 预创建(适合列表)

// 子线程提前 inflate cell 放进 RecycledViewPool
val pool = RecyclerView.RecycledViewPool()
pool.setMaxRecycledViews(TYPE_NORMAL, 20)
recyclerView.setRecycledViewPool(pool)

// 提前预热
backgroundExecutor.execute {
    repeat(10) {
        AsyncLayoutInflater(context).inflate(R.layout.cell_normal, null) { view, _, _ ->
            // 把 view 包装成 ViewHolder 放进 pool
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 14.5 跨平台 Inflate 延迟对照

平台 异步 Inflate 方案
Android AsyncLayoutInflater / 子线程 LayoutInflater
iOS dispatch_async + alloc/init UIView
Web requestIdleCallback / Web Worker
Flutter 内置(Widget tree 构建廉价)
Compose 内置(@Composable 智能跳过)

探索性思考:为什么"异步 Inflate"在简单 cell 上反而更慢?因为 attach 操作必须回主线程,且 attach 是 ViewParent 的 measure 触发链——简单 cell 自身 inflate < 5ms,跨线程切换的开销(context switch + attach delay)就 > 5ms,加上 attach 时的视觉闪烁,净收益是负的。异步不是免费的。

▶▶ 回扣 §02 案例:评论/广告/相关推荐异步 Inflate 是关键——这些区域单个 cell ~30ms,跨线程切换 ~5ms,净收益 25ms × 3 区 = 75ms。但简单的标题/正文 cell 不异步(< 5ms 没收益)。


# 15.治理三层缩重绘

核心命题:减少"每帧产出"的工作量——既要让"重绘范围"最小,也要让"重绘频率"最少。

# 15.1 局部 invalidate(最朴素)

// 错误:整个 View 重绘
invalidate()

// 正确:只重绘变化区域
invalidate(dirtyRect)
1
2
3
4
5

适用:自定义 View 中只更新部分内容(如时钟的指针)。

# 15.2 拆分独立 View(高频/低频隔离)

// 错误:把"经常变化的 progress"和"几乎不变的标题"放一个 View
class ProgressBar : View {
    fun draw(canvas: Canvas) {
        canvas.drawText(title, ...)  // 每帧都画
        canvas.drawProgress(progress, ...)  // 每帧都画
    }
}

// 正确:拆分
<LinearLayout>
    <TextView id="@+id/title" />  // 只在数据变化时重绘
    <ProgressBar id="@+id/progress" />  // 高频重绘
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 15.3 setLayerType(缓存重绘结果)

// 把 View 渲染结果缓存到 GPU
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
1
2

适用场景:

  • 静态背景
  • 复杂 onDraw(如 SVG / Path)
  • 经常 invalidate 但内容不变

禁忌:

  • 给所有 View 加(GPU 内存爆掉)
  • 给"不断变化内容"加(缓存等于失效)

# 15.4 Web 的 GPU 加速正确姿势

/* 用 transform 做动画(跳过 layout & paint) */
.anim {
    transform: translateX(100px);
    transition: transform 0.3s;
}

/* 提示浏览器创建独立合成层 */
.layer {
    will-change: transform;
}

/* CSS Containment:限制 reflow 范围 */
.card {
    contain: layout style paint;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 15.5 跨平台缩重绘对照

平台 局部 invalidate 缓存渲染 高频/低频隔离
Android invalidate(Rect) setLayerType 拆 View
iOS setNeedsDisplayInRect shouldRasterize 拆 UIView
Web DOM 局部更新 will-change 拆 component
Compose invalidate scope drawWithCache 拆 @Composable

探索性思考:为什么"缩重绘"经常被忽视?因为它的代价反馈周期长——重绘代价是"每帧"的,1ms 看不出来,但 60 帧就是 60ms/秒。性能问题的"温水煮青蛙" —— 用户感知的卡顿是慢慢累积的,不是一次性的,所以工程师容易忽略每帧 1-2ms 的优化。

▶▶ 回扣 §02 案例:SVG 改 WebP(避免每帧实时解析)+ 大背景 setLayerType + 局部 invalidate(Rect) 三招组合,让 Draw 阶段从 22ms → 5ms,对帧时长贡献 -17ms。


# 16.治理四层 ROI 排序

核心命题:UI 优化的真正难点不是"会用什么",而是"先用什么"——按 ROI(投入产出比)排序。

# 16.1 ROI 排序公式

   ROI = (优化收益 × 影响范围) / (改造成本 × 风险)
1
优化项 单次收益 影响范围 改造成本 风险 ROI
减节点(删除冗余) -10-50ms 单页面 极低 低 ⭐⭐⭐⭐⭐
用 ViewStub 延迟 -50-200ms 首屏 极低 低 ⭐⭐⭐⭐⭐
局部 invalidate -5-20ms/帧 高频路径 低 极低 ⭐⭐⭐⭐⭐
异步 Inflate(复杂 cell) -50-300ms 列表/复杂页 中 中(attach 闪烁) ⭐⭐⭐⭐
ConstraintLayout 扁平化 -5-30ms 单页面 中 低 ⭐⭐⭐⭐
RecyclerView prefetch -20-50ms 列表 低 低 ⭐⭐⭐⭐
setLayerType(动画区) -5-15ms/帧 动画区 低 中(GPU 内存) ⭐⭐⭐
改用 Compose / SwiftUI varies 整 App 极高 高 ⭐⭐
自定义渲染管线 varies 整 App 极高 极高 ⭐

# 16.2 决策路径建议

首屏慢(白屏 > 200ms):

  1. 先减节点(§13.1)
  2. 再 ViewStub 延迟非关键(§13.5)
  3. 复杂 cell 异步 Inflate(§14.1)
  4. 实在不行才考虑 Compose 重构

滚动卡(FPS < 50):

  1. 先看 onBindViewHolder 是否有 IO/解码(§10.1)
  2. 再扁平化 cell 层级(§13.4)
  3. RecycledViewPool prefetch(§14.4)
  4. 实在不行才虚拟化或换列表组件

局部卡(动画/交互):

  1. 先看 invalidate 范围(§15.1)
  2. 拆 View 隔离高频/低频(§15.2)
  3. setLayerType 配合静态部分(§15.3)
  4. 实在不行才考虑 OpenGL 自绘

# 16.3 不要做的"伪优化"

伪优化 真相
把所有 LinearLayout 改 ConstraintLayout 简单场景反而慢
全量异步 Inflate 简单 cell 闪烁 + 总耗时增加
setLayerType 加在所有 View GPU 内存爆掉
移除 wrap_content 改 fixed size 业务受损
关闭硬件加速 一般不会比 GPU 快

# 16.4 优化前的"度量门槛"

不达到这些数据不要轻易优化:

  • 首屏 inflate > 100ms(小于此值优化收益有限)
  • 单 cell 创建 > 10ms(小于此值不值得异步)
  • 帧时长 P99 > 25ms(小于此值卡顿不显著)

探索性思考:为什么"ROI 排序"是工程师最容易忽视的能力?因为做技术容易陷入"我用了哪些技术"而非"我解决了什么问题"。真正的高手不是用过的优化最多,而是判断准确——只用那些真正解决问题的优化。

▶▶ 回扣 §02 案例:方法派的胜利不在于用了 ConstraintLayout、AsyncLayoutInflater、setLayerType——经验派也用了。胜利在于按 ROI 排序:先减节点(高 ROI 低成本)→ 再延迟 inflate(中 ROI 中成本)→ 最后才动 setLayerType(低 ROI 高风险)。


# 17.求证实验 ⭐

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

  1. 猜想(直觉假设)
  2. 假设(带场景边界的命题)
  3. 设计(控制变量、可重复)
  4. 执行(采集真实数据)
  5. 验证(数据是否支持假设)
  6. 思考(实验启示和扩展)

# 17.1 实验一:扁平化收益(场景边界)

猜想:减少层级总能让渲染变快。

假设:嵌套 ≥ 4 层时,ConstraintLayout 比 LinearLayout 嵌套快;< 4 层时反向。

设计:

  • 控制变量:相同视觉效果,相同 View 数量
  • 变量:层级深度(2/3/5/8 层),布局类型(LL 嵌套 vs CL 扁平)
  • 设备:Pixel 4,Android 11
  • 采集:FrameMetrics 看 Layout 阶段耗时(首帧)

执行:

视觉布局 LinearLayout 嵌套 ConstraintLayout 扁平
2 层(3 个 TextView 排列) 1.2ms 1.5ms
3 层(含图标 + 文本) 2.5ms 2.3ms
5 层(带 padding 嵌套) 8.5ms 4.5ms
8 层(深嵌套) 25ms 8ms

验证:

  • 假设成立。临界点在 3-4 层。
  • 简单场景(≤ 3 层)ConstraintLayout 自身 overhead 大于扁平化收益。
  • 复杂场景(≥ 5 层)扁平化收益线性放大。

思考:

  • ConstraintLayout 的求解器是常数代价(约 1ms);LinearLayout 嵌套是 O(N²) 代价。
  • 简单场景用 LinearLayout,复杂场景用 ConstraintLayout —— 这不是"哪个更好",而是"哪个适合"。

# 17.2 实验二:异步 Inflate 收益(场景边界)

猜想:异步 Inflate 总比同步快。

假设:当 cell inflate 成本 > 30ms 时,异步净收益为正;< 10ms 时为负。

设计:

  • 变量:cell 复杂度(5 / 30 / 100ms)、inflate 数量(5 / 20 / 50 个)
  • 测量:① 总耗时 ② 首屏可见时间 ③ 视觉闪烁

执行:

单 cell 成本 同步 5 个 异步 5 个 同步 20 个 异步 20 个
5ms 25ms(流畅) 30ms + 闪烁 100ms(顿) 80ms + 闪烁
30ms 150ms(白屏) 70ms(流畅) 600ms(白屏) 200ms(流畅)
100ms 500ms(崩盘) 150ms(流畅) 2000ms(崩盘) 400ms(流畅)

验证:

  • 假设成立。临界点在 30ms 单 cell 成本。
  • 简单 cell 异步反而总耗时增加(context switch + attach 开销)。

思考:

  • attach 必须主线程,无法纯异步。
  • 异步 Inflate 的真正价值在于"复杂 cell",不是"所有 cell"。

# 17.3 实验三:局部重绘收益

猜想:局部 invalidate(Rect) 总比 invalidate() 快。

假设:当 invalidate 范围占 View 面积 < 30% 时,局部 invalidate 收益显著(>2×)。

设计:

  • 1080×2400 屏幕的全屏自定义 View,每帧重绘
  • 变量:invalidate 范围占比(5% / 30% / 80% / 100%)

执行:

invalidate 范围 每帧 Draw 耗时 FPS
5%(小动画区) 0.8ms 60
30%(局部更新) 4ms 60
80%(大半屏更新) 12ms 50
100%(全屏) 16ms 35

验证:

  • 假设成立。5% 范围比 100% 范围快 20×。

思考:

  • 局部 invalidate 是"零成本"优化(只改一行代码)。
  • 但前提是知道"哪个区域真的变了"——这需要业务侧的拆分。

# 17.4 实验四:列表复用与 onBindViewHolder 减负

猜想:onBindViewHolder 越简单,滚动 FPS 越高。

假设:onBindViewHolder 中含 IO/解码/复杂格式化时,FPS 必降到 < 40。

设计:

  • 列表 1000 项,含图片 + 文本 + 时间格式化
  • 变量:onBindViewHolder 实现(同步加载 / 异步加载 / 极简)

执行:

实现 onBindViewHolder 平均耗时 滚动 FPS 帧时长 P99
同步加载图片 + 同步格式化 35ms 22 110ms
异步加载图片(Glide)+ 同步格式化 8ms 45 35ms
异步图 + 异步格式化(缓存) 1.5ms 60 18ms

验证:

  • 假设成立。onBindViewHolder 必须 < 8ms(帧预算 50%)。

思考:

  • onBindViewHolder 是"最高频"代码路径,每滚一行都执行。
  • 任何 IO / 复杂计算必须移出。

# 17.5 实验五:声明式 UI vs 命令式 UI

猜想:Compose 比传统 View 更快。

假设:动态状态变化频繁场景下 Compose 占优;静态简单场景 View 占优。

设计:

  • 同样视觉效果的页面(10 个 TextView + 5 个 ImageView)
  • 变量:是否频繁变更状态(每秒 1 次 / 不变)
  • 测量:首屏 inflate / 状态变更 / 内存

执行:

场景 View 体系 Compose
首屏 inflate(无状态变更) 25ms 50ms(劣)
状态变更(每秒 1 次,10 次) 累计 80ms 累计 30ms(优)
内存(idle) 8MB 12MB(劣)

验证:

  • 假设成立。Compose 不是"全面更快",而是"动态场景更快"。

思考:

  • 首屏 Compose 多了 Slot Table 构建开销。
  • 状态变更 Compose 智能 Skipping,传统 View 必须 invalidate 整 ViewGroup。

# 17.6 五大实验启示

启示 含义
没有银弹 每个优化都有场景边界
数据驱动 不实测就不要"经验"
临界点思维 找到临界点再决定用不用
高频路径优先 onBindViewHolder > onCreateViewHolder
度量先于优化 不知道现状就不要动手

▶▶ 回扣 §02 案例:经验派失败是因为没做实验直接套招;方法派胜利是因为每招都按本节实验逻辑验证后才落地。实验不是奢侈品,是优化前的必经之路。


# 18.实战案例

# 18.1 跨端同构案例:电商详情页

背景:某电商 App 详情页同构 Android / iOS / H5,三端 UI 性能各不相同。

症状:

  • Android 首屏 1.2s,iOS 0.8s,H5 1.8s
  • iOS 滚动稳定 60fps,Android 50fps,H5 30fps(移动 Safari)

归因:

  • Android:XML 层级 8 层(深嵌套)
  • iOS:用 XIB(NIB 反序列化慢于代码)
  • H5:1500+ DOM 节点、未做虚拟滚动

治理:

  • Android:扁平化(→ 4 层)+ AsyncLayoutInflater + RecyclerView prefetch
  • iOS:cell 改代码创建(避免 XIB)+ estimatedRowHeight
  • H5:DOM 减半 + 虚拟滚动

效果:

端 首屏(before) 首屏(after) 滚动 FPS(before) 滚动 FPS(after)
Android 1200ms 450ms 50 58
iOS 800ms 380ms 60 60
H5 1800ms 750ms 30 50

核心洞察:同构页面也要"差异化优化"——三端的瓶颈不同(Android Inflate / iOS XIB / H5 DOM 数量),所以治理手段也不同。

# 18.2 平台特异案例:Android 折叠屏适配

背景:某新闻 App 折叠屏(如 Galaxy Z Fold)展开时整页重新 Layout,FPS 降至 12。

根因:

  • Configuration 变化触发 Activity 重建(onCreate 重新 inflate)
  • Inflate 重新走一遍(500ms+)
  • 用户感知"折屏一下卡 1 秒"

治理:

// AndroidManifest.xml 处理 configChanges
<activity android:configChanges="screenSize|smallestScreenSize|screenLayout">
1
2
override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    // 自定义处理:仅重新计算布局,不 inflate
    rootView.requestLayout()
}
1
2
3
4
5

效果:折屏切换从 1000ms → 80ms(12.5× 提升)。

# 18.3 Compose 反例:Skipping 失效导致重复重组

背景:某 App 用 Compose 重写后,长列表反而比 RecyclerView 慢。

根因:

@Composable
fun ItemRow(item: Item, onClick: () -> Unit) {  // ❌ lambda 每次重组都新建
    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
11

治理:

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

// 父:
LazyColumn {
    items(list, key = { it.id }) { item ->  // ✅ 加 key
        ItemRow(item, onClick = onClickHandler)  // ✅ 复用 lambda
    }
}
1
2
3
4
5
6
7
8
9
10
11

效果:滚动 FPS 从 35 → 58(恢复到接近 RecyclerView 水平)。

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


# 19.防劣化体系

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

# 19.1 三道防线总览

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

# 19.2 编码期 Lint

Android Lint:

<lint>
    <issue id="UseConstraintLayout" severity="warning" />  <!-- 嵌套 ≥ 4 层提示 -->
    <issue id="UnusedAttribute" severity="warning" />
    <issue id="ContentDescription" severity="error" />
</lint>
1
2
3
4
5

自定义规则:

  • 检测 layout XML 深度(自定义 lint detector)
  • 检测 onBindViewHolder 中的 IO 操作
  • 检测 invalidate() 调用(建议 invalidate(Rect))

# 19.3 CI 卡口

性能基线测试:

// Macrobenchmark
@Test
fun startupBenchmark() = benchmarkRule.measureRepeated(
    packageName = "com.example.app",
    metrics = listOf(StartupTimingMetric()),
    iterations = 10,
    startupMode = StartupMode.COLD
) {
    pressHome()
    startActivityAndWait()
}
1
2
3
4
5
6
7
8
9
10
11

卡口规则:

  • 首屏耗时退化 ≥ 5% → 阻断 PR
  • 列表 P99 帧时长退化 ≥ 10% → 警告
  • 内存峰值退化 ≥ 10% → 阻断 PR

# 19.4 线上 SLO

核心 SLO 指标:

指标 目标 告警阈值
首屏 P95 < 1s > 1.5s
首屏 P99 < 2s > 3s
滚动 FPS P95 > 55 < 50
帧时长 P99 < 25ms > 50ms
ANR 率 < 0.05% > 0.1%

告警 + 自愈机制:

  • 自动归因:性能退化告警 → 关联最近 PR
  • 灰度回滚:发现问题自动回滚到上一版本
  • 报告:每周生成性能趋势报告

# 19.5 文化建设

  • 性能预算:新页面必须申报"渲染预算"(Inflate 时长、首帧时长)
  • 性能 Code Review:UI 改动必须有 perf reviewer
  • 性能 OKR:列表 FPS 进 OKR

探索性思考:为什么"防劣化"比"优化"更难?因为优化是"专项动作",防劣化是"日常文化"。专项可以由一两个高手完成,文化需要全员参与。这就是为什么大厂都有"性能委员会"——不是为了治标,是为了治本。


# 20.跨平台速查

# 20.1 工具速查

平台 节点级 阶段级 全局级 自动化测试
Android Layout Inspector / Profiler Perfetto / Systrace / FrameMetrics Choreographer / FPS Macrobenchmark
iOS Instruments View Hierarchy Time Profiler / Core Animation CADisplayLink XCTest Performance
Web Chrome DevTools Performance Long Tasks API INP Puppeteer / Playwright
Compose Layout Inspector composition trace Choreographer Macrobenchmark
Flutter Flutter Inspector DevTools Timeline DevTools FPS flutter_driver

# 20.2 关键 API 速查

目的 Android iOS Web
异步 Inflate AsyncLayoutInflater dispatch_async requestIdleCallback
延迟显示 ViewStub lazy var dynamic import
局部重绘 invalidate(Rect) setNeedsDisplayInRect DOM 局部更新
缓存渲染 setLayerType(HARDWARE) shouldRasterize will-change
列表复用 RecyclerView UITableView virtual scroll
跳过 Layout - - transform/opacity
限制 reflow 范围 - - contain: layout

# 20.3 各平台首屏优化清单

Android 首屏 < 800ms:

  • [ ] 关键路径 inflate 层级 ≤ 5
  • [ ] 非关键 inflate 用 ViewStub / 异步
  • [ ] 列表 ≥ 50 项必用 RecyclerView prefetch
  • [ ] 启动期不解码大图 / 不解析复杂 SVG
  • [ ] 减少冷启动期的 Bitmap 加载

iOS 首屏 < 600ms:

  • [ ] cell 用代码创建(非 XIB)
  • [ ] 用 estimatedRowHeight
  • [ ] 滚动期不在 cellForRow 中做 IO
  • [ ] 避免触发 Off-screen Rendering(圆角/阴影)
  • [ ] 用 dequeueReusableCell

Web 首屏 < 1500ms:

  • [ ] DOM 节点 ≤ 1500
  • [ ] 关键 CSS inline
  • [ ] 长列表用虚拟滚动
  • [ ] transform / opacity 做动画
  • [ ] CSS Containment

# 21.总结与延伸

# 21.1 五条核心原则

  1. 本质先行:UI 优化 = 减节点 + 缩重绘 + 延 inflate(每个手段对应一个物理约束)。
  2. 场景边界:每个"经验招"都有边界,没有边界的经验等于偏见。
  3. 数据驱动:不实测不要"经验",求证实验是优化的基础。
  4. ROI 排序:先做 ROI 高的(减节点 / ViewStub / 局部 invalidate),再做 ROI 低的(自定义渲染)。
  5. 防劣化文化:优化是专项,防劣化是日常 —— 优化 + 防劣化 = 性能可持续。

# 21.2 五个常见误区

误区 真相
"扁平化总比嵌套快" 简单场景反向(ConstraintLayout 自身 overhead)
"异步总比同步快" 简单 cell 反而更慢
"setLayerType 加上去就快" GPU 内存代价 + 高频内容反而失效
"Compose 全面替代 View" 静态简单页面 View 更快
"用了高级技术就是好优化" ROI 才是最终评判标准

# 21.3 一句话总结

UI 优化的本质是减少"产帧成本"——通过减节点、缩重绘、延 inflate 三种手段,把 Layout / Draw / Inflate 三大开销压到帧预算之内。所有手段都有场景边界,没有边界的经验等于偏见。数据驱动 + ROI 排序 + 防劣化文化,是 UI 性能可持续的保障。

# 21.4 延伸阅读

  • 卷三·01 渲染管线与原理:UI 优化的渲染基础
  • 卷三·02 FPS 与帧率检测:UI 性能的量化
  • 卷三·03 卡顿捕获与归因:UI 卡顿归因
  • 卷三·04 ANR 监控与治理:极端卡顿
  • 卷三·06 动画交互响应优化:交互动效专项
  • 卷四·04 列表与滚动性能:列表专项

下一篇预告:卷三·06 动画交互响应优化 —— 把"用户感知流畅"的最后一公里做好。

上次更新: 2026/06/07, 10:26:12
ANR监控与治理
动画交互响应优化

← ANR监控与治理 动画交互响应优化→

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