编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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
    • 公共方法论

    • 体系建设篇

    • 资源专项篇

      • CPU监控与分析
      • 内存监控与治理
      • OOM与低内存治理
      • 线程模型调度优化
      • 进程与多进程优化
      • IO与存储性能
        • 01.阅读说明
        • 02.贯穿案例
          • 2.1 案例背景
          • 2.2 经验派的 4 周折腾(典型反面教材)
          • 2.3 方法派的 6 天闭环
          • 2.4 上线效果
          • 2.5 案例如何串起本文
        • 03.IO 物理本质
          • 3.1 一句话定义
          • 3.2 现象与代价
          • 3.3 度量准则与基准
          • 3.4 反直觉问题清单
        • 04.落盘机制原理
          • 4.1 文件 IO 全栈架构
          • 4.2 落盘语义层级
          • 4.3 写放大与小文件惩罚
          • 4.4 跨平台同构原理
          • 4.5 平台差异点矩阵
        • 05.度量与采集
          • 5.1 三类采集方案
          • 5.2 各方案的可见盲区
          • 5.3 跨平台采集对照表
          • 5.4 数据可信度评估
        • 06.归因决策树
          • 6.1 IO 问题决策树
          • 6.2 主线程 IO 检测三板斧
          • 6.3 数据库慢查询定位
          • 6.4 IO 量过大归因
        • 07.文件 IO 全链路
          • 7.1 Android 文件 IO 全链路
          • 7.2 iOS 文件 IO 全链路
          • 7.3 Web 文件 IO 全链路
          • 7.4 嵌入式文件 IO 全链路
          • 7.5 跨平台文件 IO 性能对照
        • 08.SQLite 全链路
          • 8.1 Android SQLite 写入全链路
          • 8.2 SQLite WAL 全链路
          • 8.3 SQLite 索引全链路
          • 8.4 iOS Core Data 全链路
          • 8.5 跨平台 DB 性能对照
        • 09.KV 存储全链路
          • 9.1 Android SharedPreferences 全链路
          • 9.2 MMKV 全链路
          • 9.3 iOS UserDefaults 全链路
          • 9.4 Web localStorage / IndexedDB
          • 9.5 跨平台 KV 选型对照
        • 10.mmap 全链路
          • 10.1 mmap 物理原理
          • 10.2 mmap 适用场景
          • 10.3 Android mmap API
          • 10.4 iOS mmap API
          • 10.5 mmap vs fsync 对比
        • 11.跨端存储对照
          • 11.1 跨端存储栈对照
          • 11.2 文件系统对照
          • 11.3 KV 存储对照
          • 11.4 数据库对照
          • 11.5 跨端通用最佳实践
        • 12.跨端对照
          • 12.1 五个全链路总览
          • 12.2 各平台优化优先级
          • 12.3 反直觉问题答疑
        • 13.治理一层主线程零 IO
          • 13.1 StrictMode 全量拦截 + penaltyDeath
          • 13.2 SharedPreferences 全面迁移到 MMKV
          • 13.3 所有 SQLite 操作走子线程
          • 13.4 日志/统计走异步缓冲
          • 13.5 主线程零 IO 检查清单
        • 14.治理二层量级
          • 14.1 缓存命中即返回
          • 14.2 数据压缩(CPU 换 IO 永远划算)
          • 14.3 协议升级(PB / FlatBuffer 替代 JSON)
          • 14.4 小文件聚合存储
          • 14.5 IO 量级最小化检查清单
        • 15.治理三层批量
          • 15.1 SQLite 事务批量化
          • 15.2 SQLite 启用 WAL 模式
          • 15.3 合理设置 SQLite 索引(不是越多越好)
          • 15.4 异步刷盘(mmap 自动 / msync 手动)
          • 15.5 文件 IO 缓冲
        • 16.治理四层防退化
          • 16.1 主线程 IO 时长 SLO 监控
          • 16.2 CI 基线测试
          • 16.3 定期 wal_checkpoint 和 VACUUM
          • 16.4 ROI 排序
          • 16.5 避免反向收益
        • 17.求证实验 ⭐
          • 17.1 实验一:主线程 SP 的真实代价
          • 17.2 实验二:批量 vs 逐条写入
          • 17.3 实验三:小文件惩罚
          • 17.4 实验四:WAL vs DELETE
          • 17.5 实验五:mmap vs fsync
          • 17.6 五大实验启示
        • 18.实战案例
          • 18.1 跨端同构案例:日志写崩主线程
          • 18.2 平台特异案例:iOS Core Data 主线程查询
          • 18.3 反例案例:批量但不包事务
        • 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 延伸阅读
          • 21.5 给团队的建议
    • 流水线专项

    • 业务专项篇

    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 资源专项篇
杨充
2026-05-27
目录

IO与存储性能

# IO 与存储性能

本文核心命题:IO 是"看不见的杀手"——它不像 CPU/内存那样直观可见,但 90% 的卡顿、ANR、启动慢的背后都有 IO 在作祟。IO 性能问题的根因 90% 在"如何使用 API",不在"用什么 API"。


# 01.阅读说明

  • 本文卷归属:卷二 · 资源篇 · 第 6 篇
  • 本文目标层级:L2 进阶 → L3 专家
  • 适用平台:Android(主) / iOS / Web / 跨端框架 / 嵌入式
  • 前置阅读:
    • 卷二·02 内存监控与治理(mmap 是内存与 IO 的桥梁)
    • 卷二·03 OOM 异常与低内存治理(IO 与 mmap 共享地址空间)
  • 本文核心命题:

    IO 是"看不见的杀手":CPU profiler 看不见(fsync 期间 CPU 空闲)、CPU 占用低(IO 等待)、内存正常(脏页未刷)。
    IO 优化 = 主线程零 IO + IO 量级最小化 + 单次 IO 最快 + 持续防退化。

全文 21 章地图:

   §01 阅读说明           §02 贯穿案例           §03 IO 物理本质      §04 落盘机制原理
   §05 度量与采集         §06 归因决策树
   §07 文件 IO 全链路 ⭐  §08 SQLite 全链路 ⭐    §09 KV 存储全链路 ⭐
   §10 mmap 全链路 ⭐    §11 跨端存储对照 ⭐    §12 跨端对照
   §13 治理一层主线程零 IO ⭐  §14 治理二层量级 ⭐  §15 治理三层批量 ⭐  §16 治理四层防退化 ⭐
   §17 求证实验 ⭐         §18 实战案例           §19 防劣化体系          §20 跨平台速查
   §21 总结与延伸
1
2
3
4
5
6
7

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


# 02.贯穿案例

本案例贯穿全文:§03 看懂介质本质、§05/§06 用三方案+决策树定位、§17 用实验复盘、§13-§16 给出分层策略闭环。

# 2.1 案例背景

某头部阅读 App V7.3 上线"全量日志埋点"功能(产品同学希望"用户每个动作都有数据"),灰度后立刻爆雷:

  • ANR 率从 0.04% 飙升到 0.61%,被 Google Play 打"应用未响应"标签。
  • 冷启动 P95 从 1.4s 升到 4.1s(产品要求"秒开"目标完全失败)。
  • 低端机(Android 8,2GB RAM)SQLite 主线程写 P99 达 820ms,触发 ANR。
  • 客服收到大量"读到一半页面卡 1 秒"投诉。
  • 阅读时长(核心 KPI)下降 8%。

研发组初步反应:"日志写一下能慢成这样?SharedPreferences/SQLite 都是异步的吧?"——这是典型的"API 表面认知"。

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

周次 动作 结果
第 1 周 把日志改成 SharedPreferences.apply()(认为是异步) ANR 率反而升到 0.78%:apply 累积后 onPause/onStop 同步等所有未落盘
第 2 周 把 SQLite 加大量索引(怀疑查询慢) 写入更慢(每写一行更新所有索引);冷启动达 4.6s
第 3 周 把日志写入加 try-catch(怀疑异常拖慢) 无变化(IO 不是异常)
第 4 周 把日志改成"批量每 50 条写一次"(方向对但实现错) 仍未包事务,性能仅好 15%

复盘:四周折腾错在"对 IO API 的表面认知"——以为 apply 真异步、以为加索引能加速所有操作、以为加 try-catch 能治 IO。IO 性能的真相在介质和落盘机制层,不在 API 名字层。

# 2.3 方法派的 6 天闭环

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

Day 1(§03/§04 物理本质 + §05 三方案):

  • 方案 A(系统级 iostat):写 IOPS 在卡顿时段飙到 1200+(远超低端机能力)。
  • 方案 B(/proc/[pid]/io):阅读 App 进程 write_bytes 每分钟 47MB(异常高)。
  • 方案 C(应用层埋点):日志库每秒触发 80+ 次 SQLite INSERT + 每次单独 commit。

→ 每条日志 = 1 次 INSERT + 1 次 fsync + 1 次 SharedPreferences.apply——三件事都在主线程触发。

Day 2(§06 决策树):

  • CPU 占用低(仅 8-15%)→ 大概率 IO 阻塞 ✓
  • 主线程 ✓
  • API:SharedPreferences 用 apply 但累积;SQLite 无事务、每条单独写

Day 3(§17 实验思路验证):直接用 §17.1 数据说服团队:"apply 累积 100 次会让 onPause 卡 80-200ms"。

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

  • 第 1 层(少):日志先在内存批量到 1000 条,再异步刷盘。
  • 第 2 层(批):日志库内部 SQLite 改 BEGIN TRANSACTION ... COMMIT,1000 条一次事务。
  • 第 3 层(异):日志写入完全异步线程;SharedPreferences 改用 MMKV(mmap)。
  • 第 4 层(缩):日志体积用 protobuf 替代 JSON,每条 -50%。

Day 6(上线灰度对比):

# 2.4 上线效果

指标 经验派 4 周后 方法派 6 天后
ANR 率 0.78% 0.05%
冷启动 P95 4.6s 1.3s
低端机 SQLite 主线程写 P99 820 ms 8 ms(移离主线程)
日志写入吞吐 80 条/s 3500 条/s
阅读时长 -8% +2%(回升)

核心洞察:日志库的"每条 INSERT + fsync"是典型 IO 反模式——单看每次操作很轻量(< 5ms),高频累积直接 ANR。IO 性能问题的根因 90% 在"如何使用 API",不在"用什么 API"。MMKV、WAL、事务批量这些"老技术"在每个项目都值得被重新检查。

# 2.5 案例如何串起本文

  • §03 物理本质 ▶▶ 介质延迟差 7 个数量级 + fsync 是杀手——案例所有问题的根。
  • §04 落盘机制 ▶▶ apply 异步 ≠ 不阻塞——表面 API vs 物理本质。
  • §07-§11 五大全链路 ▶▶ 文件/SQLite/KV/mmap/跨端 五条链路对应案例每一类问题。
  • §17 求证实验 ▶▶ §17.1 SP 假异步、§17.2 批量、§17.3 小文件、§17.4 WAL、§17.5 mmap 都在案例中变现。
  • §13-§16 四层治理 ▶▶ "少→批→异→缩"四字诀正是案例落地路径。

探索性思考:为什么"API 表面认知"是工程师最容易陷入的误区?因为 API 文档写"apply() 是异步的"——大多数工程师就此打住。但 SP 的源码里 apply() 异步只是"返回快",生命周期切换时仍同步等。API 是抽象的承诺,物理是冷酷的事实 —— 真正的工程师必须穿透 API 看到物理。


# 03.IO 物理本质

# 3.1 一句话定义

IO = 数据在"易失存储(内存)"和"持久存储(磁盘)"之间的传输。

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

约束一:存储介质的延迟差距是 7 个数量级

   L1 cache:        1 ns       (类比:1 秒)
   内存:            100 ns     (类比:1.5 分钟)
   SSD 随机读:      100 μs     (类比:1 天)
   SSD 随机写:      1 ms       (类比:10 天)
   HDD 寻道:        10 ms      (类比:100 天)
   机械硬盘随机:    100 ms     (类比:3 年)
1
2
3
4
5
6

移动设备几乎都是 eMMC/UFS(类 SSD),但低端机的小颗粒寿命末期,写入延迟可达 50-200ms。

约束二:fsync 是同步阻塞

   write() 系统调用 → 数据进入 page cache(μs 级)
                         ↓
              内核异步刷盘(kernel 决定何时)
                         ↓
   fsync()  → 强制刷盘 + 等待硬件 ACK(ms 级)
              线程完全阻塞
1
2
3
4
5
6

fsync 一次可能 = 1ms(高端 SSD)= 50-200ms(低端 eMMC 末期)。

约束三:小文件惩罚

每个文件的 IO 都涉及:

  • inode 查找
  • 目录遍历
  • 元数据更新(atime/mtime)
  • 分配/释放数据块

1000 个 1KB 文件的总开销远比 1 个 1MB 文件大(§17.3 实验27×)。

# 3.2 现象与代价

IO 慢的工程本质:

  • 同步阻塞:fsync/fdatasync 会等数据真正落盘,期间线程完全阻塞
  • 小文件惩罚:每个文件都要走 inode 查找、目录遍历
  • 写放大:一次逻辑写可能触发多次物理写(日志、索引、元数据)
  • 碎片惩罚:长时间使用后文件碎片化,连续读变成随机读

业务代价:

  • 主线程 fsync 一次,60fps 直接掉到 6fps
  • 启动期 IO > 5MB 时冷启时间 +500ms
  • 日志高频写不优化时 ANR 率涨 10×

▶▶ 回扣 §02 案例:低端机 SQLite 主线程写 P99 达 820ms 直接对应"低端机小颗粒寿命末期写入 50-200ms"——日志库每秒触发 80+ 次写直接打爆设备 IO 能力。

# 3.3 度量准则与基准

资源视角(USE):

指标 含义 阈值参考
IOPS 每秒 IO 次数 视设备
IO 带宽 读/写吞吐 视设备
IO 等待时间 iowait < 10%

请求视角(RED):

指标 含义 阈值参考
主线程单次 IO 耗时 单次 IO 调用 P95 < 16ms
主线程总 IO 时长占比 IO 时长 / 总时长 < 5%
数据库主线程查询 单次 query P99 < 50ms
启动期 IO 总量 冷启读写字节 < 5MB

行业基准:

平台 主线程 IO 红线 KV 选型 DB 选型
Android 0 主线程 IO MMKV SQLite + WAL
iOS 0 主线程 IO UserDefaults(轻量)/ MMKV Core Data / GRDB
Web 0 主线程 IO localStorage(小)/ IndexedDB IndexedDB
嵌入式 严格规划 NVM 嵌入式 KV

# 3.4 反直觉问题清单

带着这些问题阅读:

  1. 为什么"日志写得多"会直接导致 ANR?
  2. 同样写 1MB,写 1 个文件 vs 写 1000 个小文件,差几个数量级?
  3. 为什么 SQLite 加了索引反而更慢?
  4. 主线程读 SharedPreferences 真的安全吗?
  5. 为什么 mmap 写入"没真正落盘"也能保证不丢?
  6. SSD 的"随机 IO 不比顺序 IO 慢"是真的吗?
  7. 为什么"批量写"比"逐条写"快 100 倍?
  8. 异步写一定比同步写好吗?

探索性思考:为什么 IO 是"看不见的杀手"?因为它的症状(卡顿)和现象(CPU 低)是相反的——CPU profiler 上看一切正常,但用户感受卡顿。正常的 profile 工具按"CPU 占用"找问题,但 IO 阻塞时 CPU 占用是 0 —— 这是工具的盲区。好的工程师要会"看缺席的数据" —— CPU 不忙但卡顿,那就是 IO。


# 04.落盘机制原理

# 4.1 文件 IO 全栈架构

   应用代码(Java/Kotlin/Swift/JS)
      ↓ FileOutputStream.write()
   语言运行时(JVM/ARC/V8)
      ↓ JNI / syscall
   libc / Bionic
      ↓ write() syscall
   内核 VFS(虚拟文件系统)
      ↓ ext4/F2FS/APFS/HFS+ 具体文件系统
   Page Cache(内核内存)
      ↓ 异步刷盘(kernel 决定)
   Block 层 + IO 调度器(CFQ/Deadline)
      ↓
   设备驱动(eMMC/UFS/NVMe)
      ↓
   存储介质(NAND Flash)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关键观察:

  • write() 默认只到 page cache(用户态返回)
  • fsync() 才真正落到介质(硬同步点)
  • 任何一层崩溃都会丢"page cache 中未落盘"的数据

# 4.2 落盘语义层级

   ① 用户态 buffer → ② page cache → ③ 介质
   
   write()       ─┐
                 │
   fflush()      ─┤  仅刷到 page cache
                 │
   fsync()       ─┘  强制刷到介质(含数据 + 元数据)
   fdatasync()   ── 仅刷数据,不刷元数据(更快)
1
2
3
4
5
6
7
8

关键认知:

  • 崩溃丢失:write() 后未 fflush,进程崩溃时丢
  • 断电丢失:fflush() 但未 fsync,断电时丢
  • 介质损坏:fsync() 后仍可能丢(硬件故障)

性能 vs 可靠性权衡:

  • 不 fsync:高性能,崩溃可丢最近数据
  • fsync:可靠,每次 1-200ms 阻塞
  • mmap:高性能 + 内核管理(详见 §10)

# 4.3 写放大与小文件惩罚

写放大:

   逻辑写 1KB →
      物理写 4KB(块对齐)+
      数据库 WAL 8KB +
      索引更新 4KB
   = 物理 16KB(16× 放大)
1
2
3
4
5

小文件元数据开销:

   写 1 个 100MB 文件:
      = 1 次 inode 创建 + ~25600 个数据块
   
   写 1 万个 10KB 文件:
      = 10000 次 inode 创建 + ~30000 个数据块(每文件至少 1 块对齐)
      + 10000 次目录遍历
      + 10000 次 mtime 更新
1
2
3
4
5
6
7

# 4.4 跨平台同构原理

底层都是 POSIX 文件 IO(Windows 是 win32 API,但语义相近)。

跨平台术语对照

通用术语 Android iOS Web 嵌入式
文件读写 FileInputStream NSFileHandle File API fopen/fread
KV 存储 MMKV / SP NSUserDefaults localStorage NVS
数据库 SQLite / Room Core Data / GRDB IndexedDB 嵌入式 SQL
内存映射 MemoryFile / mmap mmap SharedArrayBuffer mmap
强制落盘 fsync fsync (无) sync
文件系统 ext4 / F2FS APFS / HFS+ OPFS / IndexedDB LittleFS / FATFS

# 4.5 平台差异点矩阵

维度 Android iOS Web 嵌入式
主流文件系统 ext4(旧)/ F2FS(新) APFS OPFS(实验) LittleFS / FATFS
默认 KV SharedPreferences NSUserDefaults localStorage EEPROM
默认 DB SQLite Core Data IndexedDB 自定义
沙箱 应用专用 应用专用 域名专用 无
异步 API Coroutines DispatchQueue Promise 视设计

探索性思考:为什么"page cache"是 OS 设计的伟大发明?因为它把"应用写"和"介质写"解耦——应用可以高频小写,内核合并刷盘。但代价是"崩溃丢数据"的风险。所有抽象都是 trade-off —— 性能换可靠性,反之亦然。


# 05.度量与采集

# 5.1 三类采集方案

   ① 系统级 IO 监控(设备级)
   ② 进程级 IO 追踪(进程级)
   ③ 应用层埋点(业务级)
1
2
3

① 系统级 IO 监控

# Android
adb shell cat /proc/diskstats
adb shell iostat 1

# Linux
iostat -x 1
1
2
3
4
5
6

优势:开销极低,全设备视图。
局限:粒度到设备级,无法归因到具体进程。

② 进程级 IO 追踪

# Android
adb shell cat /proc/$(pidof com.example.app)/io

# 输出:
# rchar: 读字节总数
# wchar: 写字节总数
# read_bytes: 实际从介质读
# write_bytes: 实际写入介质
1
2
3
4
5
6
7
8

优势:进程级归因。
局限:strace 拖慢被测进程数倍;线上不可用。

③ 应用层埋点

class IOWrapper(private val target: FileOutputStream) {
    override fun write(buf: ByteArray) {
        val start = SystemClock.elapsedRealtimeNanos()
        try { target.write(buf) }
        finally {
            val durMs = (SystemClock.elapsedRealtimeNanos() - start) / 1_000_000
            if (durMs > 16) report(durMs, Throwable())
        }
    }
}
1
2
3
4
5
6
7
8
9
10

优势:业务级归因(哪个业务、哪行代码)。
局限:覆盖度有限(仅业务调用层);性能开销 5-10%。

# 5.2 各方案的可见盲区

方案 钩子位置 数据粒度 性能开销 跨端通用性 线上可用 主要局限
① 系统级 iostat 设备 极低 跨端有差异 服务端可 无归因
② 进程级 /proc/io 进程 高(strace) Linux 系 不可 不知调用栈
③ 应用层 API hook 业务调用 中(5-10%) 跨端通用 可 不覆盖系统

实战建议:服务端用 ①;端上排障用 ②;端上线上用 ③ + 抽样 ①。

# 5.3 跨平台采集对照表

平台 系统级 进程级 应用层
Android /proc/diskstats / iostat /proc/[pid]/io FileInputStream / SQLiteDatabase hook
iOS task_info task_info NSFileHandle hook
Web (沙盒) (沙盒) IndexedDB / localStorage timing
Linux iostat / blktrace /proc/[pid]/io strace / 自定义 hook

# 5.4 数据可信度评估

  • 系统级:可信度高,但只看总量。
  • 进程级:可信度高,但开销大。
  • 应用层:可信度中(受 hook 覆盖度限制),但归因强。

探索性思考:为什么 IO 监控难做?因为它"看似简单实则复杂"——单一指标(IOPS / 字节数)只看总量,定位需要"调用栈 + 时间",但抓栈本身就拖慢 IO。IO 是 Heisenberg 原理的真实应用 —— 测量改变被测物。


# 06.归因决策树

# 6.1 IO 问题决策树

   卡顿 / 慢
      │
      ├─ CPU 高吗?
      │     │
      │     ├─ 高 → CPU 问题(不在本章)
      │     └─ 低 → 大概率 IO 阻塞
      │           │
      │           ├─ 主线程吗?
      │           │     │
      │           │     ├─ 是 → 检查文件操作、数据库、SP/MMKV、日志
      │           │     └─ 否 → 跨线程锁等待,或主线程在等异步 IO 结果
      │           │
      │           └─ 用什么 API?
      │                 ├─ SharedPreferences → apply 还是 commit?
      │                 ├─ SQLite → 事务/索引/WAL 检查
      │                 ├─ 文件 → fsync/小文件/序列化开销
      │                 └─ 网络缓存 → 磁盘缓存路径检查
      │
      └─ IO 量大吗?
            │
            ├─ 量大 → 算法层优化(合并、分片、压缩)
            └─ 量小但慢 → 物理层问题(碎片、磁盘满、低端机颗粒劣化)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

▶▶ 回扣 §02 案例:阅读 App 的诊断路径精准命中——CPU 低(8-15%)→ IO 阻塞 → 主线程 → SQLite 无事务 + SharedPreferences 累积。

# 6.2 主线程 IO 检测三板斧

  • StrictMode(Android):开发期开启 detectDiskReads/detectDiskWrites,主线程 IO 直接红屏
  • 采样栈追踪:定时采样主线程栈,统计落在 IO 系统调用上的比例
  • AOP 拦截:编译期插桩 FileInputStream/SQLiteDatabase 的 open,主线程触发记录

# 6.3 数据库慢查询定位

SQLite 慢查询定位 4 步:

  1. 开启 EXPLAIN QUERY PLAN,看是否走索引
  2. 检查 PRAGMA journal_mode(WAL 比 DELETE 快 3-5 倍)
  3. 看是否在事务里(无事务的逐条写每条都 fsync)
  4. 检查 page_size 与 cache_size(默认值往往不合理)

# 6.4 IO 量过大归因

典型 IO 量过大场景:

  • 日志写入:每条日志一次 fsync(典型反模式)
  • 数据库批量写未事务:每条 INSERT 一次 fsync
  • 多个小文件写:每个文件多次 syscall
  • 未压缩数据传输:网络 -> 磁盘 -> 内存全链路放大
  • 缓存重复读:同一文件反复读

探索性思考:为什么"决策树"是 IO 归因最有效的工具?因为 IO 问题的症状-根因映射相对结构化 —— CPU 低就是 IO,主线程就是要异步,这些都是确定性的判断。IO 归因不需要 AI,决策树足够。


# 07.文件 IO 全链路

# 7.1 Android 文件 IO 全链路

   ① FileOutputStream(file).write(bytes)
      ↓ Java 层缓冲(默认无缓冲)
   ② libcore.io.IoBridge → Posix.write
      ↓ JNI 调用
   ③ Bionic libc → write() syscall
      ↓ 陷入内核
   ④ VFS → ext4/F2FS 文件系统
      ↓
   ⑤ Page Cache(写入内存)
      ↓ 用户态返回(μs 级)
   ⑥ Kernel 异步刷盘(30s 默认)
      ↓
   ⑦ Block IO 调度 → eMMC / UFS 驱动
      ↓
   ⑧ NAND Flash 物理写入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关键性能点:

  • write() 默认只到 page cache(~10μs)
  • 显式 fsync() 才真正落盘(1-200ms)
  • BufferedOutputStream 减少 syscall 次数(推荐 8KB-64KB 缓冲)

优化代码:

// 反例:每次 write 都 syscall
val out = FileOutputStream(file)
data.forEach { out.write(it.toByteArray()) }

// 正例:用缓冲
BufferedOutputStream(FileOutputStream(file), 64 * 1024).use { out ->
    data.forEach { out.write(it.toByteArray()) }
}  // close 时 flush 一次

// 关键节点强制落盘(金融场景)
out.fd.sync()  // 等价 fsync
1
2
3
4
5
6
7
8
9
10
11

# 7.2 iOS 文件 IO 全链路

   ① NSFileHandle / FileManager
      ↓
   ② Foundation 缓冲层
      ↓
   ③ libsystem_kernel.dylib → write() syscall
      ↓
   ④ XNU 内核 → APFS / HFS+
      ↓
   ⑤ Unified Buffer Cache
      ↓
   ⑥ AHCI / NVMe 驱动 → 介质
1
2
3
4
5
6
7
8
9
10
11

iOS 特殊性:

  • APFS 写时复制(CoW),元数据开销大
  • iCloud 同步会增加额外 IO
  • NSFileProtection 加密层带来 ~5% 性能损耗

# 7.3 Web 文件 IO 全链路

   ① File API / OPFS(Origin Private File System)
      ↓
   ② 浏览器内部 IndexedDB 实现
      ↓
   ③ 转换为 SQLite 操作(部分浏览器)
      ↓
   ④ 走 OS 文件 IO
1
2
3
4
5
6
7

Web 特殊性:

  • 沙盒限制:只能在浏览器分配的存储区
  • 没有 fsync 等价 API
  • IndexedDB 性能与浏览器实现强相关

# 7.4 嵌入式文件 IO 全链路

   ① 应用 API
      ↓
   ② RTOS 文件层(LittleFS / FATFS)
      ↓
   ③ Flash 驱动
      ↓
   ④ NOR / NAND Flash
1
2
3
4
5
6
7

嵌入式特殊性:

  • 无 page cache(直接落盘)
  • Flash 擦写次数限制(~10K-100K)
  • 必须 wear-leveling 算法

# 7.5 跨平台文件 IO 性能对照

平台 write() 延迟 fsync 延迟 推荐缓冲
Android(高端机) ~10μs 1-10ms 64KB
Android(低端机) ~20μs 50-200ms 32KB
iOS ~10μs 1-10ms 64KB
Web IndexedDB ~100μs (无) (transaction)
嵌入式 ~100μs 直接 1-4KB

▶▶ 回扣 §02 案例:低端机 SQLite 主线程写 P99 达 820ms 直接对应"低端机 fsync 50-200ms"。物理介质的延迟上限是优化的天花板,不能违反。

探索性思考:为什么"page cache"对应用透明却如此重要?因为它是性能与可靠性的"调节器" —— 没有它,每次 write 都要等介质(~ms);有它后,write 几乎免费(~μs)。好的系统设计往往是"看不见的优化" —— 用户感觉不到 page cache 的存在,但失去它整个 OS 都会慢 100×。


# 08.SQLite 全链路

# 8.1 Android SQLite 写入全链路

   ① db.execSQL("INSERT INTO t ...")
      ↓
   ② SQLiteDatabase 解析 SQL
      ↓
   ③ 准备 statement(编译)
      ↓
   ④ 在事务上下文中执行:
      - 默认无事务 → 自动 BEGIN + INSERT + COMMIT(每条都触发)
      - COMMIT 触发:
        - 写 WAL 文件(或 rollback journal)
        - fsync()
        - 写主库(WAL)/ 应用 journal(DELETE)
        - fsync()
      ↓
   ⑤ 释放锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

关键瓶颈:

  • 每条 INSERT 都触发 2 次 fsync(1-200ms × 2)
  • 没有事务时,1000 条 INSERT = 2000 次 fsync

事务优化:

db.beginTransaction()
try {
    items.forEach { 
        db.execSQL("INSERT INTO t VALUES (?, ?)", arrayOf(it.id, it.data)) 
    }
    db.setTransactionSuccessful()
} finally {
    db.endTransaction()  // 仅 1 次 fsync
}
1
2
3
4
5
6
7
8
9

收益:1000 条从 1000 次 fsync → 1 次 fsync(§17.2 实验70× 加速)。

# 8.2 SQLite WAL 全链路

   DELETE 模式(默认):
   ① BEGIN → 创建 rollback journal
      ↓
   ② INSERT → 主库写
      ↓
   ③ COMMIT → 删除 journal(fsync 主库)
   
   读阻塞写,写阻塞读
1
2
3
4
5
6
7
8
   WAL 模式:
   ① BEGIN → 写 WAL 文件
      ↓
   ② INSERT → 追加到 WAL
      ↓
   ③ COMMIT → fsync WAL(不动主库)
      ↓ 后台 checkpoint
   ④ 一次性把 WAL 合并到主库
   
   读不阻塞写,写不阻塞读
1
2
3
4
5
6
7
8
9
10

配置:

PRAGMA journal_mode=WAL;         -- 启用 WAL
PRAGMA synchronous=NORMAL;       -- WAL 下用 NORMAL(默认 FULL 性能差)
PRAGMA wal_autocheckpoint=1000;  -- 1000 页自动 checkpoint
1
2
3

§17.4 实验 数据:

模式 读吞吐 写吞吐 读 P99 写 P99
DELETE 1,200 op/s 350 op/s 28 ms 180 ms
WAL 8,500 op/s 1,800 op/s 5 ms 15 ms

# 8.3 SQLite 索引全链路

   ① CREATE INDEX → B-tree 创建
      ↓ 占额外空间
   ② SELECT WHERE indexed_col = ?
      → 走索引(O(log n))
   ③ INSERT / UPDATE / DELETE
      → 更新表 + 更新所有索引(每个索引都要写)
1
2
3
4
5
6

关键认知:

  • 索引加速查询,但拖慢写入
  • 每多一个索引,写入慢 3-5%
  • 复合索引按"高频先后"排列

反模式:

-- ❌ 给低基数字段加索引(如 gender 仅 M/F)
CREATE INDEX idx_gender ON users(gender);

-- ❌ 给所有字段都加索引(写入慢 3-5×)

-- ✅ 仅为高频查询字段加索引
CREATE INDEX idx_user_email ON users(email);

-- ✅ 复合索引按高频先后
CREATE INDEX idx_log_time_level ON logs(create_time, level);
1
2
3
4
5
6
7
8
9
10

# 8.4 iOS Core Data 全链路

Core Data 是 iOS 的 ORM 层,底层仍是 SQLite。

   ① NSManagedObjectContext.save()
      ↓
   ② NSPersistentStoreCoordinator 转换为 SQL
      ↓
   ③ 走 SQLite 标准链路
1
2
3
4
5

Core Data 注意点:

  • 比直接 SQLite 多一层 ORM(性能 -20%)
  • 主线程 context 慎用,应用 background context
  • 自动 fetch limit 防止全表扫

# 8.5 跨平台 DB 性能对照

数据库 平台 单条写延迟 批量优化 推荐场景
SQLite 全平台 1-10ms 事务 通用
Core Data iOS 2-15ms NSBatchInsertRequest iOS 业务对象
Room Android 1-10ms @Transaction Android
IndexedDB Web 1-50ms transaction Web 离线
Realm 跨端 < 1ms write 块 高性能场景

▶▶ 回扣 §02 案例:日志库的"每条 INSERT 单独 commit"是典型反模式——每条 = 2 次 fsync = 2-400ms(低端机)。Day 4 改事务批量后,1000 条只需 2 次 fsync,吞吐 80→3500 条/s。

探索性思考:为什么"事务"是 SQLite 性能的"关键"?因为事务是"fsync 合并的契约" —— 应用告诉 SQLite "这一批要么全成要么全败",SQLite 就只在 COMMIT 时 fsync 一次。事务的本质是"原子性 + 性能"的统一 —— 看似为正确性服务,实为性能服务。


# 09.KV 存储全链路

# 9.1 Android SharedPreferences 全链路

   ① sp.edit().putString(k, v).apply()
      ↓ 仅修改内存中 HashMap
   ② QueuedWork 异步队列入队
      ↓
   ③ 子线程写入 XML 文件
      ↓
   ④ fsync()
   
   关键:onPause / onStop 时会同步等所有 apply 完成
        (QueuedWork.waitToFinish)
1
2
3
4
5
6
7
8
9
10

SP 的"假异步":

  • apply() 返回快(μs 级)
  • 但生命周期切换时,主线程会等所有未落盘的 apply
  • 累积 100 次 apply,onPause 卡 80-200ms

§17.1 实验证明这是反直觉的陷阱。

SP 替代方案 MMKV:

MMKV.initialize(context)
val mmkv = MMKV.defaultMMKV()
mmkv.encode("key", value)  // 真异步,无生命周期等待
1
2
3

# 9.2 MMKV 全链路

   ① mmkv.encode("key", value)
      ↓
   ② 序列化为 protobuf 字节
      ↓
   ③ 直接写入 mmap 区域
      ↓ (内核管理刷盘)
   ④ 用户态返回(< 1μs)
   
   关键:mmap 数据由内核异步刷盘
        进程崩溃可能丢失最近未刷数据
1
2
3
4
5
6
7
8
9
10

MMKV 优势:

  • mmap 零 syscall
  • protobuf 紧凑序列化
  • 多进程支持
  • 跨平台(Android/iOS/Win/Mac)

MMKV 局限:

  • 进程崩溃可能丢数据(需 msync 或 sync mode)
  • 单 key value 不超过 buffer size

# 9.3 iOS UserDefaults 全链路

   ① UserDefaults.standard.set(value, forKey: key)
      ↓
   ② 修改内存中 dictionary
      ↓
   ③ 异步写入 plist 文件(cfprefsd 进程)
      ↓
   ④ 跨进程通知更新
1
2
3
4
5
6
7

iOS UserDefaults 注意点:

  • 适合小数据(< 1MB)
  • 跨进程同步开销大
  • 大数据用 Core Data 或文件

# 9.4 Web localStorage / IndexedDB

localStorage:

   localStorage.setItem(key, value)
      ↓ 同步阻塞主线程
   写入磁盘
1
2
3

limitations:

  • 同步阻塞(每次写都阻塞主线程)
  • 5-10MB 限制
  • 仅字符串

IndexedDB:

   indexedDB.open(...).onsuccess = (e) => {
     const db = e.target.result;
     const tx = db.transaction(["store"], "readwrite");
     tx.objectStore("store").put({key, value});
   }
1
2
3
4
5

IndexedDB 优势:

  • 异步
  • 支持复杂数据
  • 大容量(视浏览器,通常 50MB+)

# 9.5 跨平台 KV 选型对照

场景 Android iOS Web 跨端
高频小数据 MMKV MMKV localStorage MMKV
配置项 MMKV / SP UserDefaults localStorage MMKV
大数据 SQLite / Room Core Data IndexedDB Realm
跨进程 MMKV (multi mode) App Group (无标准) MMKV

§17.5 实验 数据(高频小写):

场景 普通 IO + fsync mmap (MMKV) 增益
100×100B/s 持续 60s 12,500 ms 125 ms 100×

▶▶ 回扣 §02 案例:日志库改 MMKV 后吞吐从 80 升到 3500 条/s,正是 mmap 在"高频小写"场景下的真实威力。

探索性思考:为什么 MMKV 能"在原 SP 接口下"提供 100× 性能?因为它打破了"接口决定性能"的迷信——同样的 KV 接口,底层一个走 fsync 一个走 mmap,性能差 100×。接口是契约,实现是性能 —— 工程师永远要看实现,不要被接口骗了。


# 10.mmap 全链路

# 10.1 mmap 物理原理

   ① mmap(addr, len, PROT_RW, MAP_SHARED, fd, offset)
      ↓ 内核创建 VMA(虚拟内存区域)
   ② 应用首次访问 addr → page fault
      ↓
   ③ 内核从文件读 page 到 page cache
      ↓
   ④ 映射到应用虚拟地址
      ↓
   ⑤ 应用读写就是普通内存操作
   
   ⑥ 内核异步刷脏页
      ↓
   ⑦ msync(addr, len, MS_SYNC) 强制刷
1
2
3
4
5
6
7
8
9
10
11
12
13

关键认知:

  • mmap 没有 read/write syscall
  • 数据在 page cache 中,多进程共享
  • 写后不主动 msync 可能丢数据(崩溃 / 断电)

# 10.2 mmap 适用场景

最适合:

  • 高频小写(KV / 日志)
  • 大文件随机访问(数据库 / 索引)
  • 跨进程共享(IPC)
  • 只读大文件(资源 / 模型)

不适合:

  • 一次性大文件读(开销大于普通 IO)
  • 严格落盘的金融场景(需配合 msync)
  • 嵌入式(内存有限)

# 10.3 Android mmap API

// API 27+
val sm = SharedMemory.create("name", size)
val buf = sm.mapReadWrite()
buf.putInt(0, 42)

// 旧版本:MemoryFile(ashmem)
val mf = MemoryFile("name", size)
mf.writeBytes(data, 0, 0, data.size)
1
2
3
4
5
6
7
8

# 10.4 iOS mmap API

let fd = open(path, O_RDWR | O_CREAT, 0666)
ftruncate(fd, size)
let addr = mmap(nil, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
// 使用 addr 作为普通内存
msync(addr, size, MS_SYNC)  // 强制刷盘
1
2
3
4
5

# 10.5 mmap vs fsync 对比

维度 mmap fsync IO
高频小写 100× 快 慢
大数据顺序 略快 接近
数据安全(崩溃) 可能丢 保证
复杂度 中(需理解 VMA) 低
跨进程 天然支持 复杂

探索性思考:为什么 mmap 在工程上"被低估"?因为它的接口看起来不像 IO(更像内存)。工程师本能用 read/write,不会想到把"写文件"变成"写内存"。好的工具往往违反直觉 —— mmap 的"反直觉"是它优势的源头。


# 11.跨端存储对照

# 11.1 跨端存储栈对照

   Android:FileSystem → SQLite / MMKV / SP
   iOS:    FileSystem → SQLite / Core Data / UserDefaults / MMKV
   Web:    OPFS → IndexedDB / localStorage / SessionStorage
   嵌入式:  Flash → 嵌入式 KV / 自定义
1
2
3
4

# 11.2 文件系统对照

平台 文件系统 写时复制 加密 性能特点
Android(旧) ext4 否 视设备 性能稳定
Android(新) F2FS 是(部分) 设备级 优化 SSD
iOS APFS 是 默认 元数据开销大
macOS APFS / HFS+ 是 / 否 视配置 同上
Linux ext4 / btrfs 否 / 是 视配置 通用

# 11.3 KV 存储对照

平台 默认方案 推荐方案 性能
Android SharedPreferences(慢) MMKV 100×
iOS UserDefaults(中) MMKV / 自定义 mmap 5-10×
Web localStorage(同步) IndexedDB varies
跨端 (无) MMKV / Realm 高

# 11.4 数据库对照

平台 默认方案 推荐配置
Android SQLite(Room) WAL + 事务 + 合理索引
iOS Core Data 后台 context + WAL
Web IndexedDB transaction 批量
跨端 Realm / SQLite 与平台一致

# 11.5 跨端通用最佳实践

  • 所有平台:主线程零 IO
  • 所有平台:高频小写用 mmap(MMKV)
  • 所有平台:DB 必启用 WAL(或等价)
  • 所有平台:批量必用事务
  • 所有平台:小文件聚合存储

探索性思考:为什么"跨平台 IO 优化"思路高度统一?因为底层物理(NAND Flash)和 OS 设计(POSIX)是统一的。应用层 API 看起来千差万别,但物理本质是同一个。理解物理层的工程师,看任何平台都是"换 API 名字而已"。


# 12.跨端对照

# 12.1 五个全链路总览

链路 Android iOS Web 嵌入式
文件 IO FileOutputStream → ext4/F2FS NSFileHandle → APFS IndexedDB → 浏览器实现 LittleFS / FATFS
数据库 SQLite + Room Core Data IndexedDB 嵌入式 SQL
KV 存储 SP → MMKV UserDefaults localStorage NVS
mmap SharedMemory(API 27+) mmap syscall SharedArrayBuffer mmap
跨端方案 MMKV / Realm MMKV / Realm IndexedDB 自定义

# 12.2 各平台优化优先级

Android:

  1. StrictMode 全开
  2. SP 全面迁移 MMKV
  3. SQLite 启用 WAL + 事务
  4. 日志/统计走异步缓冲
  5. 小文件聚合存储

iOS:

  1. Core Data 必用 background context
  2. UserDefaults 仅小数据,大数据用 MMKV / Core Data
  3. SQLite/GRDB 配合 WAL
  4. 文件系统操作主线程禁

Web:

  1. localStorage 仅极小数据
  2. 大数据用 IndexedDB
  3. transaction 批量写入
  4. OPFS 用于高性能场景

# 12.3 反直觉问题答疑

问题 答案
"日志写得多" 怎么 ANR? 每条 fsync 50-200ms,累积阻塞主线程
1MB vs 1000×1KB 差几个数量级? 27×
加索引必加速 SQLite 吗? 错。写入慢 3-5×
主线程读 SP 安全吗? 不。首次读会同步加载整个 XML
mmap 数据真"零拷贝"? 是。但需 msync 才保证落盘
SSD 随机不慢吗? 读基本无差,写仍 2-5× 写放大
批量写比逐条快多少? 70-100×
异步写一定好吗? 不一定,进程崩溃可能丢

▶▶ 回扣 §02 案例:经验派 100% 命中"反直觉问题"——把 SP apply 当真异步,把加索引当万能。真相是:物理介质的延迟、fsync 的阻塞、page cache 的存在 才是真正决定性能的因素。


# 13.治理一层主线程零 IO

核心命题:主线程 IO 是性能问题的最大单点。这一层"0 容忍"是基础。

# 13.1 StrictMode 全量拦截 + penaltyDeath

机理:把"约定"变成"强制约束"。

代码:

if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
        .detectDiskReads()
        .detectDiskWrites()
        .detectNetwork()
        .penaltyDeath()  // Debug 直接 crash 暴露
        .build());
}
1
2
3
4
5
6
7
8

收益:开发期 100% 拦截;线上版用 penaltyLog 收集。

边界:Release 不能 penaltyDeath(用户场景);某些库(Glide 等)可能误报需白名单。

# 13.2 SharedPreferences 全面迁移到 MMKV

机理:§17.5 实验高频小写 100×;§17.1 实验SP apply 仍会在生命周期切换时同步等。

代码:

// 反例
val sp = context.getSharedPreferences("config", MODE_PRIVATE)
sp.edit().putString("key", value).apply()

// 正例
MMKV.initialize(context)
val mmkv = MMKV.defaultMMKV()
mmkv.encode("key", value)  // 真异步,无生命周期等待
1
2
3
4
5
6
7
8

收益:§02 案例日志库切 MMKV 后吞吐从 80→3500 条/s。

边界:MMKV 数据兼容性需测试;多进程场景用 MMKV.mmkvWithID(..., MMKV.MULTI_PROCESS_MODE)。

# 13.3 所有 SQLite 操作走子线程

机理:即使是 SELECT id FROM users LIMIT 1 在 SD 卡有压力时也可能 100ms+。

代码:

viewModelScope.launch(Dispatchers.IO) {
    val users = db.userDao().getAll()
    withContext(Dispatchers.Main) { 
        adapter.submitList(users) 
    }
}
1
2
3
4
5
6

收益:主线程永不被 SQLite 阻塞。

边界:Room 默认在主线程会 throw(除非 allowMainThreadQueries() 显式开启);可用此特性强制约束。

# 13.4 日志/统计走异步缓冲

机理:§02 案例日志库就是反面教材;正例是 Logan/Xlog 设计。

代码:

class AsyncLogger {
    private val buffer = ConcurrentLinkedQueue<LogEntry>()
    private val executor = Executors.newSingleThreadExecutor()
    
    fun log(entry: LogEntry) {
        buffer.offer(entry)  // 主线程仅入队(μs 级)
        if (buffer.size >= 1000) flushAsync()
    }
    
    private fun flushAsync() {
        executor.submit {
            val batch = mutableListOf<LogEntry>()
            while (batch.size < 1000 && buffer.isNotEmpty()) {
                batch += buffer.poll()
            }
            writeBatchToFile(batch)  // 子线程批量写
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

收益:§02 案例日志主线程耗时从 ms 级降到 μs 级。

边界:进程崩溃时缓冲区数据丢;关键日志用 mmap 兜底。

# 13.5 主线程零 IO 检查清单

  • [ ] StrictMode 已开启(Debug)
  • [ ] 所有 SP 已迁移 MMKV
  • [ ] 所有 SQLite 操作在子线程
  • [ ] 日志/统计走异步缓冲
  • [ ] 网络缓存读取异步
  • [ ] 资源加载异步
  • [ ] 启动期 IO ≤ 5MB

探索性思考:为什么"主线程零 IO"是看似简单实则最难的优化?因为开发者本能写"看起来简单"的同步代码——saveSettings() 比 lifecycleScope.launch { saveSettings() } 短。简洁的代码 ≠ 正确的代码 —— 工程上要培养"异步优先"的思维。

▶▶ 回扣 §02 案例:方法派 Day 4 第 1 层"主线程零 IO"是治理的基础——没有这一层,后续所有优化都没意义。


# 14.治理二层量级

核心命题:能不写就不写;必须写就压缩。

# 14.1 缓存命中即返回

机理:内存缓存命中 = 0 IO;磁盘缓存命中 = 1 次读;网络 = 重新拉。

代码:

class TieredCache<K, V> {
    private val memCache = LruCache<K, V>(100)
    
    fun get(key: K, loader: () -> V): V {
        memCache.get(key)?.let { return it }  // 0 IO
        val v = loadFromDisk(key) ?: loader().also { saveToDisk(key, it) }
        memCache.put(key, v)
        return v
    }
}
1
2
3
4
5
6
7
8
9
10

收益:热点数据 IO 频次 -90%。

边界:缓存一致性需通过版本号 / push 失效。

# 14.2 数据压缩(CPU 换 IO 永远划算)

机理:CPU 解压 10MB 数据 ~10ms;IO 读 10MB ~100ms。压缩到 3MB 只需 ~30ms IO + ~10ms 解压 = -60%。

代码:

// 写入压缩
val compressed = gzip(jsonBytes)
file.writeBytes(compressed)

// 读取解压
val bytes = gunzip(file.readBytes())
1
2
3
4
5
6

收益:磁盘空间 -60%,IO 时间 -50%。

边界:小文件(< 4KB)压缩收益小;CPU 紧张设备需评估。

# 14.3 协议升级(PB / FlatBuffer 替代 JSON)

机理:PB 比 JSON 体积 -50%、解析 -70%;FlatBuffer 零拷贝直接访问。

对照:

格式 体积 解析速度 易用
JSON 100% 1× 高
Protobuf 50% 3× 中
FlatBuffer 50% 10×(零拷贝) 低

收益:见 卷四·02 §6.1 策略 1.4。

边界:PB 需双端协议同步;schema 变更需兼容设计。

# 14.4 小文件聚合存储

机理:§17.3 实验1 万个 10KB 比 1 个 100MB 慢 27×。

代码:用 LevelDB / SQLite 聚合存 KV,避免一 key 一文件:

// 反例:每个图片缩略图一个文件
File("$cacheDir/thumb_$id.png").writeBytes(bytes)

// 正例:聚合到 SQLite 或 LevelDB
db.execSQL("INSERT INTO thumbs (id, data) VALUES (?, ?)", arrayOf(id, bytes))
1
2
3
4
5

收益:千级以上小文件场景 IO 时间 -90%。

边界:聚合后单文件损坏影响范围大,需备份策略。

# 14.5 IO 量级最小化检查清单

  • [ ] 热点数据有内存缓存
  • [ ] 大文件压缩(gzip / brotli)
  • [ ] 协议用 PB / FlatBuffer 替代 JSON
  • [ ] 小文件聚合到 DB / LevelDB
  • [ ] 启动期 IO 加载延迟到首屏后

探索性思考:为什么"CPU 换 IO 永远划算"?因为 CPU 是 ns 级、IO 是 ms 级,6 个数量级差距。即使解压用 10ms CPU,也只是 IO 节省 100ms 的 10%。性能优化是数量级的游戏 —— 用快的资源换慢的资源永远赚。

▶▶ 回扣 §02 案例:Day 5 日志体积用 protobuf 替代 JSON,每条 -50%——直接让磁盘空间和 IO 时间同步缩减。


# 15.治理三层批量

核心命题:§17.2 + §17.4证明这一层是 IO 优化的"基础设施"。

# 15.1 SQLite 事务批量化

机理:§17.2 实验事务比逐条快 70×。

代码:

db.beginTransaction()
try {
    items.forEach { 
        db.execSQL("INSERT INTO t VALUES (?, ?)", arrayOf(it.id, it.data)) 
    }
    db.setTransactionSuccessful()
} finally {
    db.endTransaction()
}
1
2
3
4
5
6
7
8
9

收益:§02 案例日志批量化是关键动作。

边界:事务内不能做长任务;事务大小过大(> 10MB)会触发 WAL 文件膨胀。

# 15.2 SQLite 启用 WAL 模式

机理:§17.4 实验吞吐 3-7×。

代码:

db.execSQL("PRAGMA journal_mode=WAL")
db.execSQL("PRAGMA synchronous=NORMAL")  // 配合 WAL 用 NORMAL 而非 FULL
1
2

收益:读写并发;写吞吐 +400%。

边界:WAL 文件需定期 checkpoint(PRAGMA wal_checkpoint(TRUNCATE));隔离级别变 SNAPSHOT。

# 15.3 合理设置 SQLite 索引(不是越多越好)

机理:索引加速查询但拖慢写入(每写要更新所有索引)。

代码:

-- 仅为高频查询字段加索引
CREATE INDEX idx_user_email ON users(email);

-- 不要给低基数字段加索引(如 gender 仅 M/F)

-- 复合索引按"高频先后"排列
CREATE INDEX idx_log_time_level ON logs(create_time, level);
1
2
3
4
5
6
7

收益:查询 100× 加速;过度加索引则写入慢 3-5×。

边界:EXPLAIN QUERY PLAN 验证;线上监控写延迟。

# 15.4 异步刷盘(mmap 自动 / msync 手动)

机理:mmap 让内核自动刷脏页;关键节点 msync 兜底。

代码:

// mmap 写入后异步刷盘(内核自动)
char* addr = mmap(nullptr, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(addr, data, len);
// 关键节点(如交易完成)显式 msync
msync(addr, len, MS_SYNC);
1
2
3
4
5

收益:吞吐 100×,关键节点保证一致性。

边界:mmap 数据可能丢(最近未 msync 的);金融类必须 msync。

# 15.5 文件 IO 缓冲

机理:BufferedOutputStream 减少 syscall 次数。

代码:

// 反例:每次 write 都 syscall
val out = FileOutputStream(file)
data.forEach { out.write(it.toByteArray()) }

// 正例:用缓冲
BufferedOutputStream(FileOutputStream(file), 64 * 1024).use { out ->
    data.forEach { out.write(it.toByteArray()) }
}
1
2
3
4
5
6
7
8

收益:syscall 次数 -100×(10000 次 → 100 次)。

边界:缓冲大小要合理(推荐 64KB);崩溃时缓冲数据丢。

探索性思考:为什么"批量"是 IO 优化的"圣杯"?因为它把"频次开销"摊到"批量大小"——10 次 fsync 100ms vs 1 次 fsync 100ms。批量化的本质是"摊薄固定成本" —— 这是工程上的通用智慧(不仅 IO,网络/数据库/线程都适用)。

▶▶ 回扣 §02 案例:方法派 Day 4 第 3 层"批量 + 事务 + WAL"组合是 SQLite 性能的"三件套"——单步收益 60×,案例日志库吞吐 80→3500 条/s。


# 16.治理四层防退化

核心命题:IO 性能容易在迭代中退化(新功能加日志/缓存/查询),必须建立长效机制。

# 16.1 主线程 IO 时长 SLO 监控

机理:上报每次主线程 IO 调用时长,P95 超阈值告警。

代码:

class IOTimingAspect {
    fun aroundIO(call: () -> Unit) {
        val start = SystemClock.elapsedRealtimeNanos()
        try { call() }
        finally {
            val durMs = (SystemClock.elapsedRealtimeNanos() - start) / 1_000_000
            if (Looper.myLooper() == Looper.getMainLooper() && durMs > 16) {
                report("main_io", durMs, Throwable().stackTraceToString())
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

收益:退化在 24h 内被发现。

边界:上报采样防数据爆炸。

# 16.2 CI 基线测试

机理:每次 PR 跑标准 IO 场景,超基线 20% 阻断合入。

代码:

// Macrobenchmark
@Test
fun ioBenchmark() = benchmarkRule.measureRepeated(
    metrics = listOf(TraceSectionMetric("file_io")),
    iterations = 5
) {
    startActivityAndWait()
    repeat(100) { writeLog("test entry") }
}
1
2
3
4
5
6
7
8
9

收益:退化在合入前被拦截。

边界:基线需周期性更新(设备/数据规模变化)。

# 16.3 定期 wal_checkpoint 和 VACUUM

机理:长期运行 SQLite 会产生空闲页 + WAL 膨胀。

代码:

// 定期(如启动空闲期)
db.execSQL("PRAGMA wal_checkpoint(TRUNCATE)")
// 每月或必要时
db.execSQL("VACUUM")
1
2
3
4

收益:磁盘空间稳定;查询性能不退化。

边界:VACUUM 全表重建,耗时较长,应在后台/空闲时段。

# 16.4 ROI 排序

ROI 优化项 收益 成本 风险 对应章节
极高 StrictMode 全开 + 主线程拦截 杜绝主线程 IO 事故 1 天 零 §13.1
极高 SP 全面迁移 MMKV 高频写 100× 1-2 周 中 §13.2
极高 SQLite 事务批量化 写入 70× 1 周 低 §15.1
极高 SQLite 启用 WAL 吞吐 3-7× 几天 中 §15.2
极高 日志/统计异步缓冲 主线程零 IO 1-2 周 低 §13.4
高 SQLite 操作全走子线程 主线程不被阻塞 1-2 周 中 §13.3
高 缓存命中即返回 IO 频次 -90% 2-3 周 中 §14.1
高 数据压缩 IO 时间 -50% 1 周 低 §14.2
高 小文件聚合 千级以上 -90% 2-3 周 中 §14.4
中 主线程 IO SLO 监控 退化早发现 1 周 低 §16.1
中 PB/FlatBuffer 协议 体积/解析双优化 2-4 周 中 §14.3
中 合理索引(去冗余) 写入 -40%、查询不变 1 周 中 §15.3
中 mmap + msync 高频小写 100× 1-2 周 中 §15.4
中 CI 基线测试 退化拦截 1-2 周 低 §16.2
中 wal_checkpoint + VACUUM 长期性能稳定 几天 低 §16.3
低 自实现存储引擎 极少收益 极高 极高 -

# 16.5 避免反向收益

  • 改 apply 以为治了主线程 IO:§02 案例第 1 周翻车。
  • 加大量索引以为加速 SQLite:写入反而慢 3-5×。
  • try-catch 包住 IO 异常:异常被吞但 IO 慢依旧。
  • 批量但不包事务:仍然每条 fsync。
  • mmap 后从不 msync:进程崩溃丢数据。

探索性思考:为什么"防退化"是工程文化问题?因为新功能开发者本能加日志、加缓存、加查询——每个单独看都"很轻量",但累积起来必然退化。长效机制 = 自动化拦截 + 文化共识 —— 缺一不可。


# 17.求证实验 ⭐

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

# 17.1 实验一:主线程 SP 的真实代价

猜想:SharedPreferences.apply() 真的"异步",主线程不阻塞。

假设:apply() 调用本身 < 1ms;但 onResume/onPause 会等所有未落盘的 apply 完成(QueuedWork.waitToFinish);累积 100 次 apply 后 onPause 被阻塞 80-200ms。

执行:

设备 100 次 apply onPause 阻塞时长
高端机(Pixel 6) < 5ms 30-50ms
低端机(Android 8) 8ms 80-200ms

验证:

  • apply 异步只是"返回快",但生命周期切换时仍会同步等。
  • 累积 100 次 apply 后 onPause 被阻塞 80-200ms。

思考:

  • apply ≠ 异步,只是"返回快"——这是 API 表面认知 vs 物理本质认知的最经典反例。
  • 用 MMKV 替代 SP;高频写场景永远不要用 SP。
  • MMKV 用 mmap + 自定义 protobuf,写入性能提升 100x。

▶▶ 回扣 §02 案例:经验派第 1 周"改 apply 以为异步"完全踩中本实验"apply 累积后 onPause 卡 80-200ms"的陷阱。

# 17.2 实验二:批量 vs 逐条写入

猜想:批量比逐条快几倍。

假设:批量比逐条快 50 倍。

执行:

实现 1 万条耗时 fsync 次数
逐条 INSERT 28 秒 10000
事务包裹批量 INSERT 0.4 秒(70×) 1
多行 INSERT 0.3 秒(93×) 1

验证:

  • fsync 是瓶颈,事务把 1 万次 fsync 合并成 1 次。

思考:

  • 任何 SQLite 批量写必须包事务。
  • 进一步合并成多行 INSERT 还能再省 25%。
  • WAL 模式下事务的可见性边界变化,需评估业务一致性需求。

# 17.3 实验三:小文件惩罚

猜想:小文件慢一些但差距有限。

假设:小文件慢 10 倍以上。

执行:

实现 总耗时
1 个 100MB 文件 0.8s
100 个 1MB 文件 1.5s
1 万个 10KB 文件 22s(27×)

验证:

  • 每个文件都要 open/close/fsync 元数据,inode 锁竞争严重。

思考:

  • 缓存设计避免"一 key 一文件",用单文件分段或 LevelDB/SQLite 聚合存。
  • iOS 上小文件惩罚比 Android 更严重(APFS 的写时复制机制开销大)。

# 17.4 实验四:WAL vs DELETE

猜想:WAL 略快,但差异不大。

假设:WAL 模式读写并发,并发场景吞吐 3-5×。

执行:

模式 读吞吐 写吞吐 读 P99 写 P99 磁盘开销
DELETE 1,200 op/s 350 op/s 28 ms 180 ms 0 额外
WAL 8,500 op/s 1,800 op/s 5 ms 15 ms +5-50MB

验证:

  • WAL 写不阻塞读:吞吐 3-7× 提升。
  • 写也大幅加速。
  • 副作用:WAL 文件可能膨胀;读隔离从 SERIALIZABLE 变 SNAPSHOT。

思考:

  • 90% 的 SQLite 场景都应启用 WAL。
  • 除非业务严格需要 SERIALIZABLE 隔离(如金融转账)。

# 17.5 实验五:mmap vs fsync

猜想:mmap 比 fsync IO 快几倍。

假设:高频小写场景 mmap 快 50-100×;大数据场景增益缩小到 2-5×。

执行:

场景 普通 IO + fsync mmap (MMKV) 增益
100×100B/s 持续 60s 12,500 ms 125 ms 100×
10×10KB/s 持续 60s 850 ms 45 ms 19×
单次 1MB 顺序写 8 ms 4 ms 2×
断电模拟 0 丢数据 最近 30s 可能丢 -

验证:

  • 高频小写是 mmap 的杀手锏。
  • 大数据场景增益小(IO 主导,不是 syscall 主导)。
  • 副作用:mmap 数据需 msync 才保证落盘。

思考:

  • 高频小数据(KV、日志、缓存)用 mmap;大数据不必。
  • 需要严格落盘的场景调用 msync。

# 17.6 五大实验启示

   SP apply 假异步     → onPause 累积阻塞,必须用 MMKV       ─┐
   批量 vs 逐条        → 事务让 fsync 合并,70×                │
   小文件惩罚          → 1 万个 10KB 比 1 个 100MB 慢 27×     ├─▶ IO 优化 = 物理介质 + API 选型 + 使用方式
   WAL vs DELETE       → 读写并发,吞吐 3-7×                  │
   mmap vs fsync       → 高频小写 100×,大数据 2×              ─┘
1
2
3
4
5

统一启示:

  • 物理介质决定上限:低端机颗粒劣化时单次写可达 200ms,必须从源头减少 IO。
  • API 选型决定基线:MMKV/WAL/mmap 是 IO 优化的"基础设施"。
  • 使用方式决定 90% 性能:批量 + 事务 + 异步 = 三大铁律。
  • fsync 是隐形杀手:所有"慢" 90% 是它造成的。
  • 小文件比大文件慢得多:缓存设计要聚合存。

▶▶ 回扣 §02 案例:方法派 6 天闭环每一步都对应本节实验。实验是优化前的"必经之路"。


# 18.实战案例

# 18.1 跨端同构案例:日志写崩主线程

背景:某 App 接入新日志库,所有日志同步写文件 + fsync。线下没事,线上爆出大量 ANR。

根因:低端机 fsync 单次 200ms,日志一多累积阻塞。

治理:

  • Android:日志走 mmap + 异步刷盘,单次写 < 1μs
  • iOS:使用 os_log(系统级异步)或自定义 mmap
  • Web:sendBeacon API 替代同步 console.log

效果:

  • Android ANR 率 0.61% → 0.05%
  • iOS Watchdog Kill -90%
  • Web 性能基本无影响(浏览器自管)

洞察:任何"看起来很轻量"的写操作,到了低端机都可能爆炸。日志库设计必须考虑最差设备。

# 18.2 平台特异案例:iOS Core Data 主线程查询

背景:某 iOS App 启动慢,profile 看不到明显热点,但启动时间 P50 1.8s。

根因:AppDelegate 里有一个 Core Data 查询,主线程同步执行。低存储设备上 Core Data 文件碎片化导致查询 1.2s。

治理:

// 反例:主线程查询
let users = try context.fetch(request)

// 正例:background context
container.performBackgroundTask { ctx in
    let users = try ctx.fetch(request)
    DispatchQueue.main.async {
        self.updateUI(with: users)
    }
}
1
2
3
4
5
6
7
8
9
10

效果:启动 P50 1.8s → 0.6s。

洞察:iOS 的 Core Data 比直接 SQLite 多一层 ORM,性能问题更隐蔽。

# 18.3 反例案例:批量但不包事务

背景:某 App 工程师按"批量写"建议改造日志库,把"每条 commit"改成"每 50 条写一次"。

结果:

  • 性能仅提升 15%
  • ANR 率没明显下降
  • 工程师疑惑"为什么没效果"

根因:50 条 INSERT 仍然是 50 次 fsync,因为每条 INSERT 默认自动 commit。

修复:

// 错误:仅"批量调用"
items.forEach { db.execSQL("INSERT...", it) }  // 每条仍是独立事务

// 正确:包事务
db.beginTransaction()
try {
    items.forEach { db.execSQL("INSERT...", it) }
    db.setTransactionSuccessful()
} finally {
    db.endTransaction()  // 仅 1 次 fsync
}
1
2
3
4
5
6
7
8
9
10
11

效果:性能从 +15% 升到 +6900%(70×)。

洞察:"批量"必须配合"事务",否则等于零。这是 §02 案例第 4 周翻车的根因。


# 19.防劣化体系

# 19.1 三道防线总览

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

# 19.2 编码期 Lint

自定义规则:

  • 主线程 IO(StrictMode + Lint 双重)
  • SharedPreferences 直接使用 → 警告(建议 MMKV)
  • SQLite 操作未在事务中(批量场景)→ 警告
  • 每条循环中调用 contentResolver → 警告
  • BufferedOutputStream 未使用 → 警告
  • mmap 后未 msync → 警告

# 19.3 CI 卡口

性能基线测试:

@Test
fun ioBenchmark() = benchmarkRule.measureRepeated(
    metrics = listOf(TraceSectionMetric("file_io")),
    iterations = 5
) {
    startActivityAndWait()
    repeat(100) { writeLog("test entry") }
}
1
2
3
4
5
6
7
8

卡口规则:

  • 启动期 IO 量超基线 20% → 阻断 PR
  • 主线程 IO 时长 P99 退化 → 阻断
  • 数据库写入吞吐退化 ≥ 10% → 警告

# 19.4 线上 SLO

指标 目标 告警阈值
主线程单次 IO 耗时 P95 ≤ 16ms(一帧) > 50ms
主线程总 IO 时长占比 ≤ 5% > 15%
数据库主线程查询 P99 ≤ 50ms > 200ms
启动期 IO 总量 ≤ 5MB > 10MB
ANR 中 IO 类占比 < 10% > 30%

# 19.5 文化建设

  • IO 预算:新模块申报"启动期 IO 预算"
  • IO Code Review:IO 相关 PR 必有 perf reviewer
  • IO OKR:主线程 IO 时长进 OKR

探索性思考:为什么 IO 防退化特别需要"自动化"?因为 IO 退化是"渐进性的"——单次新增功能加一行 prefs.edit().apply() 看不出问题,10 个功能累积就 ANR。自动化拦截 = 在源头拒绝累积 —— 这是工程上的"剪枝"哲学。


# 20.跨平台速查

# 20.1 工具速查

平台 系统级 进程级 应用层
Android iostat / /proc/diskstats /proc/[pid]/io StrictMode + AOP
iOS task_info task_info Instruments File Activity
Web (沙盒) (沙盒) DevTools Performance
Linux iostat / blktrace /proc/[pid]/io strace

# 20.2 关键 API 速查

目的 Android iOS Web
主线程 IO 拦截 StrictMode (自实现) (无)
KV 高性能 MMKV MMKV / 自定义 mmap IndexedDB
数据库 SQLite + WAL Core Data + WAL IndexedDB transaction
大文件读 mmap mmap OPFS
异步 IO Coroutines / Executor DispatchQueue async/await
强制落盘 fsync / FileChannel.force fsync (无)
文件缓冲 BufferedOutputStream NSFileHandle buffered (浏览器自管)

# 20.3 各平台优化清单

Android:

  • [ ] StrictMode 全开(Debug penaltyDeath,Release penaltyLog)
  • [ ] SP 全面迁移 MMKV
  • [ ] SQLite 启用 WAL + 事务批量
  • [ ] 日志走异步缓冲
  • [ ] 大文件 mmap
  • [ ] 小文件聚合存(LevelDB / SQLite)
  • [ ] 协议升级 PB / FlatBuffer
  • [ ] 数据压缩(gzip / brotli)
  • [ ] 主线程 IO SLO 监控
  • [ ] CI 基线测试

iOS:

  • [ ] Core Data 必用 background context
  • [ ] UserDefaults 仅小数据
  • [ ] 大文件 mmap
  • [ ] os_log 替代 print
  • [ ] APFS 注意写时复制开销

Web:

  • [ ] localStorage 仅极小数据
  • [ ] 大数据用 IndexedDB(transaction 批量)
  • [ ] OPFS 用于高性能场景
  • [ ] sendBeacon 异步上报
  • [ ] Service Worker 离线缓存

# 21.总结与延伸

# 21.1 五条核心原则

  1. 主线程 0 IO:StrictMode 强制约束,§02 案例ANR 0.78%→0.05% 的根。
  2. MMKV 替代 SP:§17.5100× 性能,无需再讨论。
  3. SQLite 必启用 WAL + 事务:§17.2 + §17.4双重证据,3-70× 收益。
  4. 小文件必须聚合:§17.327× 差距是物理事实。
  5. fsync 是隐形杀手:90% 的"IO 慢"都是它造成的。

# 21.2 五个常见误区

误区 真相
"apply 是异步的所以安全" 错(onPause 累积阻塞)
"加索引必加速" 错(写入慢 3-5×)
"批量就是好" 错(不包事务等于没批量)
"mmap 数据安全" 错(崩溃丢未 msync 数据)
"SSD 随机写不慢" 错(仍有 2-5× 写放大)

# 21.3 一句话总结

IO 是"看不见的杀手"——90% 的卡顿、ANR、启动慢都源于它,但 CPU profiler 看不到。主线程 0 IO、MMKV 替代 SP、SQLite 必 WAL+事务、小文件必聚合、fsync 必批量。这五条铁律落地,应用 IO 性能就达到行业前 10%。IO 性能问题的根因 90% 在"如何使用 API",不在"用什么 API"——MMKV、WAL、事务批量这些"老技术"在每个项目都值得被重新检查。

# 21.4 延伸阅读

  • 卷二·02 内存监控与治理:mmap 是内存与 IO 的桥梁
  • 卷二·03 OOM 异常与低内存治理:mmap 共享地址空间
  • 卷二·04 线程模型调度优化:IO 必须异步线程
  • 卷三·04 ANR 监控与治理:IO 是 ANR 的高频根因
  • 卷四·01 App 冷启动优化:启动期 IO 是大头
  • 卷四·05 功耗与电量优化:IO 频次直接影响功耗

# 21.5 给团队的建议

  • 第 1 周:StrictMode 全量打开,治理所有主线程 IO 红屏
  • 第 2 周:用 MMKV 替换 SharedPreferences
  • 第 3 周:SQLite 全部启用 WAL + 检查索引覆盖率
  • 第 4 周:日志、缓存的写入路径统一异步化

下一篇预告:卷三·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
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式