进程与多进程优化
# 进程模型与多进程优化
本文核心命题:进程是"隔离单元,也是性能边界"——进程提供地址空间隔离 + 故障隔离 + 资源隔离,但代价是 IPC 开销 + 启动开销 + 内存浪费。一切多进程优化都是在"隔离收益"与"通信 / 启动 / 内存代价"之间找平衡点。
# 01.阅读说明
- 本文卷归属:卷二 · 资源篇 · 第 5 篇
- 本文目标层级:L3 专家 → L4 架构
- 适用平台:Android(主) / iOS / Web(多 Tab + Service Worker) / 嵌入式
- 前置阅读:
卷二·04 线程模型与调度优化(进程 vs 线程的对比)卷二·03 OOM 异常与低内存治理(多进程是 OOM 隔离重要手段)
全文 21 章地图:
§01 阅读说明 §02 贯穿案例 §03 进程物理本质 §04 隔离与通信原理
§05 度量与采集 §06 归因决策树
§07 进程创建全链路 ⭐ §08 IPC 通信全链路 ⭐ §09 共享内存全链路 ⭐
§10 进程生命周期全链路 ⭐ §11 跨端进程对照 ⭐ §12 跨端对照
§13 治理一层数量 ⭐ §14 治理二层 IPC ⭐ §15 治理三层隔离 ⭐ §16 治理四层重启 ⭐
§17 求证实验 ⭐ §18 实战案例 §19 防劣化体系 §20 跨平台速查
§21 总结与延伸
2
3
4
5
6
7
阅读建议:先读 §02 案例 → §03/§04 拿到原理 → §05/§06 学会度量归因 → §07-§11 五个全链路(创建/IPC/共享/生命周期/跨端)→ §13-§16 四层治理(数量收敛/IPC/隔离/重启)→ §17 求证 → §18-§20 工程闭环。
# 02.贯穿案例
本案例贯穿全文:§03 看懂代价、§04 拿到隔离模型、§05/§06 用三方案+决策树定位、§17 用实验复盘、§13-§16 给出分层策略闭环。
# 2.1 案例背景
某头部金融 App V8.5 历史上沿用"5 进程架构"(主进程 / Push 进程 / WebView 进程 / 守护进程 / 推送拉活进程),架构师初心是"模块隔离防崩溃"。但近期问题集中爆发:
- 常驻 PSS 达 420MB(主 220 + Push 80 + WebView 60 + 守护 35 + 拉活 25),中端机 OOM 率 4.8%。
- 冷启动 P95 = 3.6s(用户从 Push 点入是冷启动率 38%)。
- Android 11+ 升级后被系统认定"贪婪应用",主进程被杀率从 2% 飙到 18%。
- 应用商店"卡顿"差评/月 12,000+,用户流失加速。
研发组初步反应:"拆进程是为了稳定,不能改架构。"——这是典型的"架构惯性"。
# 2.2 经验派的 5 周折腾(典型反面教材)
| 周次 | 动作 | 结果 |
|---|---|---|
| 第 1 周 | 加强双进程互拉保活(怀疑被杀太多) | 主进程被杀率反升到 22%(系统更激进打压) |
| 第 2 周 | 把 Push 进程内存上限调小(怀疑 Push 占内存) | Push 频繁 OOM,消息丢失率涨 8 倍 |
| 第 3 周 | 加更多 IPC 缓存层(怀疑 IPC 慢) | IPC 抽象层 bug 引发数据不一致 |
| 第 4 周 | 把 WebView 进程预热常驻(怀疑冷启慢) | 常驻内存又涨 30MB |
| 第 5 周 | 改 Push 进程优先级(怀疑被低优先级杀) | 无明显改善 |
复盘:五周折腾的根本错误是默认"5 进程不能动"。所有动作都在"修补 5 进程架构",没人质疑"5 个进程真的都必要吗"。进程是重资源,每个都要算清成本收益。
# 2.3 方法派的 8 天闭环
新接手的同学按本文方法论重做:
Day 1(§04 第一性原理 + §05 三方案):
- 进程清单:5 个进程清晰列出,各自 PSS。
- 资源监控:Push 进程 80MB,但实际只有每天 8-12 次推送时干活,其余 23.5 小时空转。
- IPC trace:主→Push、主→守护进程的 Binder 调用每秒 35 次(多数是状态心跳)。
Day 2(§06 归因决策树):逐进程评估:
| 进程 | 必要性 | 真实工作量 | 占用 |
|---|---|---|---|
| 主进程 | 必要 | 全程 | 220MB |
| Push 进程 | 不必要 | 每天 8-12 次×几秒 | 80MB(23.5h 空转) |
| WebView 进程 | 必要 | 风险隔离(H5 业务多) | 60MB |
| 守护进程 | 不必要 | 仅为保活(A11+ 失效) | 35MB |
| 拉活进程 | 不必要 | 仅为保活 | 25MB |
→ 5 进程中 3 个是"为了保活"或"过度隔离"开的,可以直接删。
Day 3(§17.3 实验数据支持):用本文的保活实验数据说服架构师——"双进程互拉在 A11+ 几乎全失效"。
Day 4-5(§13-§16 分层策略):
- 第 1 层(数量收敛):Push 改 FCM 系统级推送;守护/拉活直接删;保留 WebView 进程做风险隔离。
- 第 2 层(IPC 治理):剩余主↔WebView 的 IPC 高频心跳改批量+缓存;大数据用共享内存。
- 第 3 层(风险隔离):WebView 进程仍单独,确保 H5 崩不影响主。
- 第 4 层(拥抱重启):投入"快速重启 + 状态恢复"——被杀后 < 800ms 拉起,且滚动位置/草稿/登录态全恢复。
Day 6(§17.5 实验思路验证):构造"被杀-重启-恢复"自动化用例。
Day 7-8(灰度 + 上线):
# 2.4 上线效果
| 指标 | 经验派 5 周后 | 方法派 8 天后 |
|---|---|---|
| 进程数 | 5 | 2(主 + WebView) |
| 常驻 PSS | 450MB | 240MB(-47%) |
| 冷启 P95 | 3.6s | 1.2s |
| 中端机 OOM 率 | 4.8% | 0.6% |
| 主进程被杀率 | 22% | 3%(不再被打压) |
| 推送到达率(FCM) | 91% | 97% |
| 用户主动回归率 | -8% | +5% |
核心洞察:经验派 5 周错在"把架构当宪法"。进程治理的最大杠杆是删除非必要进程,而不是优化它们。删 3 个进程让常驻内存降 47%,被杀率反而从 22% 降到 3%——系统不再把你当贪婪应用,是最大的奖励。
# 2.5 案例如何串起本文
- §03 现象与代价 ▶▶ 业务损失映射:差评/月 12K、用户流失加速。
- §04 隔离 vs 通信 ▶▶ 案例 3 个进程是"过度隔离",2 个是"合理隔离"。
- §07-§11 五大全链路 ▶▶ 创建/IPC/共享/生命周期/跨端 五条链路对应案例每一类问题。
- §17 求证实验 ▶▶ §17.1 启动代价、§17.2 IPC 吞吐、§17.3 保活失效、§17.5 快速重启都在案例中变现。
- §13-§16 四层治理 ▶▶ "数量收敛→IPC 治理→风险隔离→快速重启"四层正是案例落地路径。
探索性思考:为什么"架构惯性"是工程团队最危险的认知陷阱?因为架构往往是几年前的决策,由资历最老的工程师定的——质疑架构等于质疑权威。但系统在变(Android 11+ 打压贪婪),几年前合理的决策今天可能是负担。真正的工程精神是"持续质疑前提" —— 包括质疑自己当初的设计。
# 03.进程物理本质
# 3.1 一句话定义
进程 = 操作系统资源分配与隔离的基本单元,是"拥有独立地址空间 + 文件句柄 + 信号上下文"的执行实例。
这句话隐含三个不可商量的物理约束:
约束一:进程是地址空间的边界
每个进程拥有独立的虚拟地址空间。这是硬隔离:进程 A 不可能访问到进程 B 的内存(除非通过 IPC 显式共享)。
这是进程"安全"的根本:
- 进程 A 崩溃不会破坏进程 B 的内存(故障隔离)
- 进程 A 的内存泄漏不会影响进程 B(资源隔离)
- 进程 A 不可能读取进程 B 的私密数据(安全隔离)
代价:进程间通信必须经过内核(copy 或 mmap),有性能开销。
约束二:进程创建有固定成本
每次创建进程都要做:
- 分配地址空间(页表)
- 复制或映射代码段
- 加载动态库(dyld / linker)
- 启动运行时(VM / Native)
- 执行入口函数
Android Zygote fork: ~200-400ms(共享 ART,省去 VM 启动)
iOS exec: ~200-300ms(每次都要 dyld + libObjC)
Web 新 Tab: ~100-500ms(视浏览器实现)
Linux fork+exec: ~10-100ms(不含应用初始化)
2
3
4
这就是为什么 Android 用 Zygote 模型 fork:共享父进程的 ART,省去每次都重启 VM 的 ~500ms 开销。
约束三:进程间通信必经内核
任何 IPC 形式(Binder / Pipe / Socket / Shared Memory)都要经过内核:
进程 A 用户态 ──[陷入内核]──▶ 内核中转 ──[切回用户态]──▶ 进程 B 用户态
│ │
一次 syscall 一次 syscall
~1-5μs ~1-5μs
2
3
4
即使是"零拷贝"的 Shared Memory,初次建立映射仍需 syscall。这是 IPC 的物理底线。
# 3.2 现象与代价
进程问题往往体现为系统级问题:
- 多进程内存浪费:同一应用拆 5 个进程,每个进程都有自己的 ART / 资源副本,内存涨 2-3 倍。
- 进程拉起慢:用户从通知点入应用 → 主进程被杀 → 重新冷启动 → 体验类崩溃。
- IPC 延迟过高:跨进程查询数据 100ms+,用户感知"卡"。
- 保活滥用导致系统体验差:多个 App 互相拉活,整机内存吃紧、温度升高。
- 多进程崩溃定位难:插件进程崩溃,主进程无感,但功能失效。
业务代价(行业实测数据):
- 头部 App:减少不必要进程后,内存峰值下降 40-60%,OOM 率下降 30%。
- 进程被杀后冷启动时长是热启的 5-10 倍。
- 系统对"多进程贪婪"应用的 OOM 倾向更高(LMK 评分更差)。
▶▶ 回扣 §02 案例:金融 App 5 进程架构常驻 PSS 450MB → 删到 2 进程 240MB(-47%),OOM 率 4.8%→0.6%。
# 3.3 度量准则与基准
资源视角(USE):
| 指标 | 含义 | 阈值参考 |
|---|---|---|
| 应用进程数 | 总和 | < 3(普通) / < 5(重型) |
| 单进程 PSS | 占设备 RAM 比例 | 主 < 30%、子 < 10% |
| IPC 错误 | 调用失败率 | < 0.1% |
请求视角(RED):
| 指标 | 含义 | 阈值参考 |
|---|---|---|
| IPC 延迟 | 跨进程调用时长 | < 5ms(本地)/ < 50ms(重型) |
| 进程拉起时长 | 从拉起到 onCreate 完成 | < 800ms |
| 保活成功率 | 主进程被杀后多久重新拉起 | 视场景 |
行业基准:
| 平台 | 应用进程数 | IPC 延迟 | 进程启动开销 |
|---|---|---|---|
| Android | 1-3 主用 + 偶尔子 | Binder 同步 < 5ms | 200-500ms(fork from Zygote) |
| iOS | 几乎单进程(特殊扩展才多) | XPC < 10ms | 200ms+(exec + dyld) |
| Web | 每 Tab 一个进程 | postMessage < 10ms | 100-500ms |
| 嵌入式 Linux | 视设计 | DBus / Unix Socket | varies |
# 3.4 反直觉问题清单
带着这些问题阅读:
- 多进程一定比单进程慢吗?
- 拆进程能解决 OOM 吗?拆多少合适?
- Binder 跨进程一次调用比同进程慢多少?
- iOS 几乎单进程,是不是就没"进程问题"?
- Web Worker 是进程还是线程?
- 保活做得好就能"不被杀"吗?
- 进程间共享内存(SharedMemory)真的"零拷贝"吗?
- 子进程崩溃为什么主进程也可能被影响?
探索性思考:为什么"进程"是工程师最容易"误用"的抽象?因为它的隔离收益是"不出问题时看不到"——但代价(内存翻倍、IPC 延迟)是"立刻能感受到"。正常情况下进程隔离的价值为零,异常情况下价值无限——这种非线性收益让权衡变得困难。好的进程设计需要"概率思维":评估隔离的"事件价值 × 发生概率" vs 通信的"日常成本"。
# 04.隔离与通信原理
# 4.1 隔离收益 vs 通信代价
进程的本质收益是"隔离",本质代价是"通信"。所有多进程设计都在权衡这两者:
┌────────────────────────────────────────────────┐
│ 隔离收益 │
│ - 故障隔离(子进程崩溃,主进程不受影响) │
│ - 内存隔离(子进程内存大不影响主进程) │
│ - 安全隔离(沙箱 / 权限独立) │
├────────────────────────────────────────────────┤
│ 通信代价 │
│ - IPC 延迟(每次调用 1-50ms) │
│ - 数据序列化开销(Parcel / Codable) │
│ - 内存浪费(每个进程独立的运行时 / 资源副本) │
│ - 启动开销(200-500ms 冷启动) │
└────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
# 4.2 典型多进程设计模式
┌──────────────────────────────────────────┐
│ A. 单进程(主流) │
│ 所有功能在主进程,无 IPC │
│ 优点:无 IPC 开销 │
│ 缺点:一处崩溃全死 │
├──────────────────────────────────────────┤
│ B. 主进程 + 风险隔离 │
│ 主进程 + Web 内核进程 / 视频解码 / 推送 │
│ 优点:风险代码不会拖垮主进程 │
│ 缺点:IPC 开销可控 │
├──────────────────────────────────────────┤
│ C. 主进程 + 守护 / 保活 │
│ 主进程 + 守护进程(互相拉起) │
│ 优点:增加保活成功率(Android) │
│ 缺点:被系统打压、用户反感 │
├──────────────────────────────────────────┤
│ D. 插件化 + 进程隔离 │
│ 各业务在独立进程 │
│ 优点:彻底解耦 │
│ 缺点:内存翻倍,IPC 复杂 │
└──────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
关键认知:
- 大多数应用只需 A(单进程)
- B 是合理的折中(风险代码隔离)
- C 在 Android 8+ 后基本失效(系统强力打压)
- D 仅超大型应用使用(如微信、支付宝 30+ 进程)
▶▶ 回扣 §02 案例:金融 App 的 5 进程架构本质是 B + C + 过度拆——主+WebView 是合理 B;守护/拉活是失效的 C;Push 进程是过度拆。80% 应用应该用 A,少数用 B,几乎没人需要 C。
# 4.3 跨平台同构原理
底层都是 POSIX fork() 或其衍生(Windows CreateProcess),上层封装不同。
跨平台术语对照
| 通用术语 | Android | iOS | Web | C/C++ |
|---|---|---|---|---|
| 进程创建 | Zygote fork | exec / posix_spawn | Tab / Worker 进程 | fork |
| IPC 同步 | Binder / Messenger | NSXPCConnection | postMessage | Pipe / Socket |
| 共享内存 | MemoryFile / ASharedMemory | mmap | SharedArrayBuffer | shm_open |
| 子进程 | <service android:process=":xxx"> | App Extension | Web Worker / iframe | fork |
| 守护 | JobScheduler / WorkManager | Background Task | Service Worker | systemd |
| 进程上限 | 取决于 ROM | 严格(前台 1 + 扩展若干) | 视浏览器 | 视 OS |
# 4.4 平台差异点矩阵
| 维度 | Android | iOS | Web | C/C++ Linux |
|---|---|---|---|---|
| 默认模式 | 单进程为主,可显式拆 | 强制单进程(仅 Extension) | 每 Tab 一个 | 自由 |
| IPC 机制 | Binder(高性能) | XPC | postMessage | Pipe / Socket / shm |
| Binder 性能 | 同步 < 5ms | XPC 同步 < 10ms | postMessage < 10ms | varies |
| 共享内存 | MemoryFile(ashmem) | mmap | SharedArrayBuffer | shm |
| 启动开销 | 200-400ms(Zygote) | 200-300ms | 100-500ms | 10-100ms |
| 保活限制 | 8.0+ 严格 | 严格(仅特定模式) | Service Worker 有时限 | 视系统 |
探索性思考:为什么"四种多进程设计模式"在工程上有稳定划分?因为它们对应"隔离需求 × 通信频率"的四个象限。好的抽象划分通常对应物理本质 —— A/B/C/D 不是被发明的,而是被发现的。多数应用属于 A 是因为通信成本远大于隔离收益 —— 这是工程上的物理事实,不是个人偏好。
# 05.度量与采集
# 5.1 三类采集方案
① 进程清单采集(多少个进程,各多大)
② IPC 调用采集(频次、延迟、数据量)
③ 进程生命周期采集(拉起、被杀、ProcessExitReason)
2
3
① 进程清单采集
// Android:扫 ActivityManager.RunningAppProcesses
fun listAppProcesses(): List<ProcessInfo> {
val am = context.getSystemService(ActivityManager::class.java)
return am.runningAppProcesses?.filter {
it.uid == android.os.Process.myUid()
}?.map {
ProcessInfo(it.processName, it.pid, getPss(it.pid))
} ?: emptyList()
}
2
3
4
5
6
7
8
9
② IPC 调用采集
| 平台 | 工具 |
|---|---|
| Android | Perfetto Binder Trace |
| iOS | Instruments XPC |
| Web | Performance Panel postMessage |
③ 进程生命周期采集
// Android 11+
val am = context.getSystemService(ActivityManager::class.java)
val reasons = am.getHistoricalProcessExitReasons(null, 0, 100)
reasons.forEach {
upload(it.reason, it.processName, it.pss, it.timestamp)
}
2
3
4
5
6
# 5.2 各方案的可见盲区
| 方案 | 钩子位置 | 数据粒度 | 性能开销 | 跨端通用性 | 线上可用 | 主要局限 |
|---|---|---|---|---|---|---|
| ① 进程清单 | ActivityManager | 进程级 | 极低 | 跨端有差异 | 是 | 不知 IPC |
| ② IPC trace | 系统级 | 调用级 | 中 | 部分平台 | 部分 | 离线分析 |
| ③ 退出原因 | 系统 API | 事件级 | 极低 | Android 11+ | 是 | 平台限定 |
# 5.3 跨平台采集对照表
| 平台 | 进程清单 | IPC 采集 | 生命周期 |
|---|---|---|---|
| Android | ActivityManager | Perfetto | getHistoricalProcessExitReasons |
| iOS | sysctl | Instruments | terminationReason |
| Web | chrome://process-internals | Performance Panel | (无标准) |
# 5.4 数据可信度评估
- 进程清单:可信度高,但只看现状。
- IPC trace:可信度高,但开销中。
- 退出原因:可信度极高,是关键。
探索性思考:为什么"进程退出原因"是 Android 11 才提供?因为之前应用只能"看到自己醒着"——醒来发现自己刚被杀了,但不知道为什么。这是平台演进的真实写照:用户的痛点驱动新 API。Android 11 的 ProcessExitReason 是工程上的"福音",让"被杀分类治理"成为可能。
# 06.归因决策树
# 6.1 进程问题决策树
症状 = ?
│
├─ 常驻内存高(PSS > 300MB)
│ │
│ └─ 走"进程数过多"分支:
│ - 列出所有进程及其用途
│ - 删除非必要进程(§13.1)
│
├─ 主进程频繁被杀
│ │
│ └─ 走"被杀原因"分支:
│ - getHistoricalProcessExitReasons
│ - LMK / ANR / Force Stop / Crash
│ - 治理:减进程 + 升优先级 + 修崩溃
│
├─ IPC 慢(> 50ms)
│ │
│ └─ 走"IPC 性能"分支:
│ - 数据量大 → 共享内存(§14.3)
│ - 频次高 → 批量化 + 缓存(§14.1-14.2)
│
└─ 启动慢
│
└─ 走"进程冷启"分支:
- 如果是子进程被频繁拉起 → 改常驻
- 如果是主进程冷启 → 见冷启动篇
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
# 6.2 IPC 性能归因
IPC 慢的典型根因:
- 数据量过大(> 100KB 应该用共享内存)
- 频次过高(每秒 100+ 次需批量化)
- 同步阻塞主线程(应改 oneway 异步)
- 跨进程拉起子进程(启动 ~200ms)
# 6.3 进程拉起归因
子进程频繁拉起的典型原因:
- ContentProvider 跨进程查询
- 闹钟 / Push 唤醒
- 系统广播触发
- 第三方 SDK 主动唤醒
# 6.4 内存共享归因
多进程内存浪费来源:
- 每个进程独立的 ART/Dalvik VM
- 每个进程独立的 .so 副本(部分共享但有限)
- 每个进程独立的资源缓存
探索性思考:为什么"决策树"在进程归因上特别有效?因为进程问题的症状-根因映射是一对一的——内存高就是进程多,IPC 慢就是数据大或频次高。结构化的领域决策树足够 ML 不必要——这是工程归因的常态。
# 07.进程创建全链路
# 7.1 Android Zygote fork 全链路
① 系统启动 → Zygote 进程启动
↓ 加载 ART + 通用类(耗时 ~3s,仅一次)
② Zygote 监听 socket
↓
③ 应用启动 → AMS 通过 socket 通知 Zygote
↓ "fork 一个进程,跑 com.example.MyApp"
④ Zygote 调用 fork()
- 复制页表(COW,不复制内存)
- 子进程继承 ART + 已加载的类
↓
⑤ 子进程进入 ActivityThread.main()
- 设置进程标识 / UID
- 创建 Application
- 调用 Application.attachBaseContext + onCreate
↓
⑥ 启动 Activity / Service / 接收 IPC
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
关键开销分解:
① Zygote socket 接收命令 ~5ms
② fork(copy 页表) ~20ms
③ 设置进程标识 / 安全策略 ~20ms
④ 启动 ActivityThread.main ~50ms
⑤ Application.attachBaseContext + onCreate ~100ms+
─────────────────────────────
总计 ~200ms+
2
3
4
5
6
7
优化机会:
- Application.onCreate 是大头(业务初始化)
- 见
卷四·01 冷启动
# 7.2 iOS posix_spawn 全链路
iOS 没有 Zygote,每次 exec 都要重新加载 dyld + libObjC。
① 进程创建(posix_spawn) ~10ms
② dyld 加载所有 dylib ~100ms
③ libObjC 初始化 ~50ms
④ +load + static initializer ~50ms+
─────────────────────────────
总计 ~210ms+
2
3
4
5
6
iOS 优化路径:
- 减少 dylib 数量(动态库链接)
- 减少 +load 方法(用 +initialize 延迟)
- 减少 static initializer
- 见 dyld closures(iOS 13+)
# 7.3 Web 新 Tab 全链路
① 浏览器主进程 fork(或 spawn)渲染进程
↓
② V8 Isolate 启动(~50ms)
↓
③ 加载 HTML + 执行 JS
↓
④ 首屏渲染
2
3
4
5
6
7
Web 特殊性:
- 每个 Tab 独立进程(部分浏览器)
- iframe 跨域时也独立进程
- Service Worker 是独立进程
# 7.4 Linux fork+exec 全链路
① fork() ~5ms(COW 页表复制)
↓
② exec() 加载新二进制
↓
③ 动态链接 ld-linux ~10ms
↓
④ libc 初始化
↓
⑤ main()
─────────────────────────────
总计 ~10-100ms(不含应用初始化)
2
3
4
5
6
7
8
9
10
11
# 7.5 跨端进程创建对照
| 平台 | 创建方式 | 启动时长 | 优化空间 |
|---|---|---|---|
| Android | Zygote fork | 200-400ms | 减 Application onCreate |
| iOS | posix_spawn | 200-300ms | 减 dylib + +load |
| Web | Tab spawn | 100-500ms | 减 JS 初始化 |
| Linux | fork+exec | 10-100ms | 减动态链接 |
▶▶ 回扣 §02 案例:Push 进程被频繁拉起(每天 8-12 次×几秒),每次 ~200ms 启动开销。改 FCM 系统级推送后,这些 ~200ms 全部消失。进程启动是重资源,频繁拉起是反模式。
探索性思考:为什么 Android 选择 Zygote 而 iOS 选择 exec?因为两者的应用模型不同——Android 应用是 Java/Kotlin(需要 VM),iOS 应用是原生代码(无 VM)。Zygote 模式的本质是"分摊昂贵的初始化" —— ART 启动一次后被所有应用共享。好的架构通常对应"识别共享成本"。
# 08.IPC 通信全链路
# 8.1 Android Binder 全链路
① 进程 A 调用 IRemoteService.method()
↓ AIDL 生成的 Stub
② 参数 marshal 到 Parcel
↓
③ ioctl(BINDER_WRITE_READ)
→ 陷入内核 Binder driver
↓
④ Binder driver 从 A 拷贝到 B 进程内存
↓ (一次拷贝,比 socket 快 2×)
⑤ 进程 B 的 Binder 线程被唤醒
↓
⑥ Stub 反序列化 + 调用实际方法
↓
⑦ 返回值 marshal + ioctl 返回
↓
⑧ 进程 A 反序列化结果
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
关键性能数据:
- 单次调用 ~3μs base + 拷贝
- 100B 数据 ~4μs(250K op/s)
- 1KB 数据 ~6μs(180K op/s)
- 1MB 数据 ~10ms(100 op/s)
# 8.2 iOS XPC 全链路
① 进程 A 创建 NSXPCConnection
↓
② 通过 launchd 启动目标 service
↓
③ 调用方法(自动序列化)
↓
④ 通过 mach IPC 传递
↓
⑤ 进程 B 反序列化 + 执行
↓
⑥ 返回结果
2
3
4
5
6
7
8
9
10
11
XPC 特性:
- 基于 mach 端口 + libdispatch
- 自动管理 service 生命周期
- 类型安全(NSSecureCoding)
# 8.3 Web postMessage 全链路
① 主线程 worker.postMessage(data)
↓
② 浏览器结构化克隆数据(深拷贝)
↓
③ 跨进程传递
↓
④ Worker 端 onmessage 接收
↓
⑤ 数据反序列化(已是新对象)
2
3
4
5
6
7
8
9
Web postMessage 特殊性:
- 默认深拷贝(开销大)
- 可用 Transferable 转移所有权(零拷贝)
- 可用 SharedArrayBuffer 真正共享
# 8.4 IPC 选型决策
数据量大小:
├─ < 1KB → 直接 IPC(任何方式)
├─ 1KB-100KB → IPC 但要批量化
├─ 100KB-1MB → 共享内存或谨慎 IPC
└─ > 1MB → 必须共享内存
调用频次:
├─ < 10/s → 任意方式
├─ 10-100/s → 批量化
└─ > 100/s → 主进程缓存 + 变更 push
2
3
4
5
6
7
8
9
10
# 8.5 跨端 IPC 性能对照
| 数据大小 | Android Binder | iOS XPC | Web postMessage |
|---|---|---|---|
| 100B | 220K op/s | 180K op/s | 90K op/s |
| 1KB | 180K op/s | 150K op/s | 80K op/s |
| 10KB | 65K op/s | 55K op/s | 35K op/s |
| 100KB | 9K op/s | 7K op/s | 5K op/s |
| 1MB | 950 op/s | 700 op/s | 500 op/s |
▶▶ 回扣 §02 案例:主→Push、主→守护进程的 Binder 调用每秒 35 次(多数是状态心跳),看似不多,但累积到一天就是 300 万次——每次 4μs 也是几十秒 CPU 时间的浪费。IPC 频次比 IPC 单次开销更重要。
探索性思考:为什么 Binder 比 socket 快 2×?因为 Binder 用了"一次拷贝"——内核直接 mmap 接收方进程内存。Binder 的设计是"为 Android 量身定制" —— 它假设 IPC 是高频核心场景,所以专门优化。好的系统设计来自"理解高频路径"。
# 09.共享内存全链路
当 IPC 数据量超过 100KB 时,必须用共享内存。理解共享内存的全链路是性能优化的关键。
# 9.1 Android SharedMemory 全链路
① 进程 A 创建:
SharedMemory.create("name", size)
↓ ashmem 内核驱动分配匿名内存
② 通过 Binder 传递 SharedMemory 句柄
↓
③ 进程 B 接收句柄:
shm.mapReadOnly()
↓ mmap 到 B 进程地址空间
④ B 进程像普通内存一样访问
↓ 零拷贝(A 和 B 看到同一物理页)
⑤ A 修改 → B 立即可见(需同步原语保证一致性)
2
3
4
5
6
7
8
9
10
11
关键特性:
- 物理内存共享(零拷贝)
- 支持 Read/Write/Execute 权限位
- 进程 A 死亡后 B 仍可访问(直到都释放)
典型用例:
- 大图片传输(位图)
- 视频帧
- 大配置数据
- 大日志缓冲区
# 9.2 iOS mmap 全链路
① 进程 A 创建文件(或 anonymous):
shm_open("name", O_CREAT)
↓
② 设置大小 ftruncate
↓
③ mmap 映射到地址空间
↓
④ 通过 XPC 传递文件句柄
↓
⑤ 进程 B mmap 同一文件
↓
⑥ 两进程访问同一物理页
2
3
4
5
6
7
8
9
10
11
12
iOS 共享内存限制:
- 仅特定 entitlement 才允许
- App 沙箱默认禁止跨 App 共享内存
- 同 App 内 Extension 之间可以
# 9.3 Web SharedArrayBuffer 全链路
① 主线程:
const sab = new SharedArrayBuffer(1024 * 1024)
↓
② 通过 postMessage(sab) 传递
↓ Worker 收到后两边访问同一 buffer
③ 用 Atomics 同步:
Atomics.add(view, 0, 1)
Atomics.wait(view, 0, oldValue)
2
3
4
5
6
7
8
Web 安全限制:
- 需要 cross-origin isolation(COOP+COEP headers)
- 部分浏览器默认禁用
- 是 Spectre 漏洞缓解后逐步恢复的特性
# 9.4 共享内存同步问题
关键挑战:进程 A 写、进程 B 读,如何保证一致性?
手段:
- 跨进程 mutex(pthread_mutex_t with PTHREAD_PROCESS_SHARED)
- 原子操作(Atomic / Atomics)
- ring buffer(无锁单生产者单消费者)
- 版本号 + 双缓冲
# 9.5 跨平台共享内存对照
| 平台 | API | 启动开销 | 同步原语 | 限制 |
|---|---|---|---|---|
| Android | SharedMemory(API 27+)/ MemoryFile | ~1ms | 自管 | App 内 |
| iOS | shm_open + mmap | ~1ms | 自管 | Entitlement |
| Web | SharedArrayBuffer + Atomics | ~ms | Atomics | COOP/COEP |
| Linux | shm_open + mmap | ~1ms | 自管 | 自由 |
探索性思考:为什么"零拷贝"是个工程神话?因为它隐藏了首次建立 mmap 的开销(~1ms)和同步原语的开销。少量大数据传输用共享内存反而比 Binder 慢 —— 因为建立成本无法摊销。"零拷贝"只在"高频大数据"场景下成立 —— 这是工程实战的真实约束。
# 10.进程生命周期全链路
# 10.1 Android 进程生命周期全链路
① 应用启动 → Zygote fork
↓
② 进程进入 RUNNING 状态
↓
③ Activity 切换 / 后台 → oom_adj 分数变化
- 前台 0
- 可见 100
- 服务 500
- 后台 700
- 缓存 906
↓
④ 系统内存压力大 → LMK 选择高分进程杀掉
↓
⑤ 进程终止
↓
⑥ 用户切回 / 推送等触发拉起
→ goto ①
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 10.2 iOS 进程生命周期全链路
① 应用启动 → posix_spawn
↓
② 进程 active(前台)
↓
③ 切后台 → applicationDidEnterBackground
- 5 秒内必须完成清理
↓
④ 进入 suspended 状态
↓
⑤ 系统内存压力 → jetsam 杀掉
↓
⑥ 用户切回 → 重新启动
2
3
4
5
6
7
8
9
10
11
12
iOS 特殊性:
- 后台 5 秒生死线
- 特定模式可继续后台(VoIP / location)
- BGTask API 提供合规后台执行
# 10.3 Web Tab 生命周期全链路
① 用户打开 Tab → 进程 spawn
↓
② Tab 活跃 → 全速运行
↓
③ Tab 切到后台 → 节流(rAF 减到 1/s)
↓
④ 系统内存压力 → Tab Discarded
↓
⑤ 用户切回 → 重新加载(visibilitychange 'load' 事件)
2
3
4
5
6
7
8
9
# 10.4 ProcessExitReason 解读
Android 11+ 提供详细被杀原因:
| 原因 | 含义 | 治理方向 |
|---|---|---|
| REASON_LOW_MEMORY | LMK 杀 | 减内存 / 减进程 |
| REASON_ANR | ANR 触发 | 见 ANR 篇 |
| REASON_USER_REQUESTED | 用户主动杀 | 优化体验 |
| REASON_USER_STOPPED | 系统决定 | 配合系统 |
| REASON_INITIALIZATION_FAILURE | 启动失败 | 修崩溃 |
| REASON_PERMISSION_CHANGE | 权限变更 | 权限处理 |
| REASON_EXCESSIVE_RESOURCE_USAGE | 资源超限 | 减资源 |
| REASON_CRASH | 崩溃 | 修崩溃 |
| REASON_CRASH_NATIVE | Native 崩溃 | 修 native |
# 10.5 跨平台生命周期对照
| 平台 | 主要状态 | 系统杀机制 | 应用预警 |
|---|---|---|---|
| Android | foreground/visible/background/cached | LMK | onTrimMemory |
| iOS | active/background/suspended | jetsam | memoryWarning |
| Web | foreground/background | Tab Killer | visibilitychange |
探索性思考:为什么 Android 11 才补上"被杀原因"API?因为之前应用只能"猜测"被杀原因——是 LMK?ANR?还是用户杀的?模糊的诊断信息让 OOM 治理只能"试错" —— 直到 ProcessExitReason API 出现,才有了"分类施治"的基础。好的 API 让正确的工程实践成为可能。
# 11.跨端进程对照
# 11.1 Android 多进程模式
典型架构:
主进程(com.example.app)
├─ ":webview"(H5 隔离)
├─ ":push"(推送服务)
├─ ":daemon"(守护,已基本失效)
└─ ":sdk"(第三方 SDK 隔离)
2
3
4
5
配置方式:
<service android:name=".PushService"
android:process=":push" />
2
# 11.2 iOS App Extension 模式
iOS 强制单进程,但提供 Extension:
| Extension 类型 | 用途 |
|---|---|
| Today Widget | 通知中心小部件 |
| Share Extension | 分享菜单 |
| Action Extension | 操作菜单 |
| Photo Editing | 照片编辑 |
| Custom Keyboard | 自定义键盘 |
| Notification Service | 通知扩展 |
| iMessage App | iMessage 扩展 |
Extension 特性:
- 独立进程,独立内存限制(30-50MB)
- 通过 App Group 共享数据
- 通过 XPC 通信
# 11.3 Web 多进程模式
浏览器主进程
├─ 渲染进程(每 Tab 一个,部分共享)
├─ GPU 进程
├─ 网络进程
└─ Service Worker 进程
2
3
4
5
Site Isolation:
- Chrome 默认每个站点独立进程
- iframe 跨域时也独立
- 防御 Spectre 漏洞
# 11.4 跨端框架的进程模型
Flutter:
- 所有 Dart 代码在主进程的 Flutter Engine 中
- 平台插件通过 MethodChannel(同进程)
React Native:
- JS 在主进程的 JS 线程
- Native 模块同进程
Hybrid(WebView):
- Android 可启用 WebView 多进程
- iOS WKWebView 默认进程外
# 11.5 跨端进程模式对照
| 框架/平台 | 默认进程数 | 隔离能力 | 推荐场景 |
|---|---|---|---|
| Android Native | 1(可显式拆) | 强 | 任何场景 |
| iOS Native | 1(仅 Extension) | 中(Extension) | 任何场景 |
| Flutter | 1 | 弱(同进程) | 跨端业务 |
| RN | 1 | 弱 | 跨端业务 |
| Web | 每 Tab 1 个 | 强(默认) | Web 应用 |
探索性思考:为什么 iOS 强制单进程而 Android 允许多进程?这反映了两个平台的设计哲学——iOS 假设"应用应该简洁",Android 假设"应用可能复杂"。简洁的代价是缺少灵活性,灵活的代价是被滥用。iOS 没有"进程膨胀"问题,因为根本没法膨胀 —— 限制有时是恩赐。
# 12.跨端对照
# 12.1 五个全链路总览
| 链路 | Android | iOS | Web | Linux |
|---|---|---|---|---|
| 进程创建 | Zygote fork | posix_spawn | Tab spawn | fork+exec |
| IPC 通信 | Binder | XPC | postMessage | Pipe/Socket |
| 共享内存 | SharedMemory | mmap | SharedArrayBuffer | shm |
| 生命周期 | LMK + oom_adj | jetsam | Tab Killer | OS 调度 |
| 跨端模式 | 多进程灵活 | Extension only | Tab + Worker | 自由 |
# 12.2 各平台优化优先级
Android:
- 删除非必要进程
- WebView 多进程(H5 业务多时)
- IPC 批量化 + 主进程缓存
- 大数据用 SharedMemory
- 放弃保活,拥抱 WorkManager + 快速重启
iOS:
- 减少 dylib + +load(启动加速)
- App Group + UserDefaults 共享
- BGTask 替代保活
- Extension 内存严控(< 30MB)
Web:
- Site Isolation 利用
- Web Worker 处理 CPU 密集
- SharedArrayBuffer + Atomics(高性能场景)
- Service Worker 离线缓存
# 12.3 反直觉问题答疑
| 问题 | 答案 |
|---|---|
| 多进程比单进程慢吗? | 可能,IPC 开销大于隔离收益时是 |
| 拆进程能解决 OOM 吗? | 部分。子进程隔离能防主进程 OOM,但总内存增加 |
| Binder vs 同进程慢多少? | 单次调用 ~3μs vs 函数调用 ns 级,3 个量级 |
| iOS 没"进程问题"? | 不全是。Extension 内存限制是新挑战 |
| Worker 是进程还是线程? | Web Worker 通常是线程;Service Worker 是进程 |
| 保活做好就不被杀? | 错。Android 11+ 暗保活几乎全失效 |
| SharedMemory 真零拷贝? | 建立后零拷贝,但建立有开销 |
| 子进程崩溃影响主进程? | 通常不会,但会留下僵尸状态 |
▶▶ 回扣 §02 案例:经验派 100% 命中"反直觉问题"——把"保活"当万能药。真相是:现代 OS 设计就是"打压贪婪应用",硬抗系统是低 ROI 工作。
# 13.治理一层数量
核心命题:进程治理的最大杠杆是删除非必要进程。这一层不是"如何拆",而是"为什么不能拆"。
# 13.1 逐进程价值评估表
机理:用统一格式逐进程评估,把"模糊架构"变成"可量化决策"。
评估模板:
| 进程名 | 真实工作时长占比 | 内存占用 | 必要性等级 | 决策 |
| 主进程 | 100% | 200MB | 必要 | 保留 |
| Push 进程 | < 0.5%(每天 8-12 次)| 80MB | 不必要(可合并)| 删除 |
| 守护进程 | 0%(仅为保活,A11+ 失效)| 35MB | 无效 | 删除 |
| WebView 进程 | 30% | 60MB | 必要(风险隔离)| 保留 |
2
3
4
5
收益:§02 案例5→2 进程,PSS -47%。
边界:评估需架构师参与;删除进程涉及业务方协调。
# 13.2 Push 进程合并到主进程
机理:现代推送通道(FCM/APNs/华为/小米推送)都是系统级,应用层不需要自己保持长连接进程。
代码(FCM):
class MyFCMService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
// 直接在主进程接收,无需独立进程
handlePush(message.data)
}
}
2
3
4
5
6
收益:节省 50-80MB 常驻内存;推送到达率反而升(FCM 比自建长连接稳定)。
边界:海外用 FCM,国内用厂商推送+融合 SDK;自建长连接仅 IM 等强实时业务。
# 13.3 删除守护/拉活进程
机理:§17.3 实验双进程互拉 A11+ 几乎全失效;占内存还被系统打压。
代码:直接删除 <service android:process="..."> 和相关 BroadcastReceiver。
收益:§02 案例节省 60MB + 主进程被杀率 22%→3%。
边界:业务方可能依赖"保活"假设,需 review 并提供 WorkManager 替代方案。
# 13.4 评估每个 ContentProvider 是否真需独立进程
机理:很多 SDK 用 android:process=":xxx" 默认拆,但实际不必要。
代码:
<!-- 反例:默认拆,但实际 SDK 在主进程也工作 -->
<provider android:name="..." android:process=":sdk" />
<!-- 正例:合并到主进程 -->
<provider android:name="..." />
2
3
4
5
收益:每删一个进程节省 30-50MB。
边界:需测试 SDK 在主进程的兼容性。
# 13.5 进程数量收敛检查清单
- [ ] 列出所有
:xxx进程清单 - [ ] 每个进程评估"真实工作时长占比"
- [ ] 删除占比 < 1% 的进程(合并到主)
- [ ] 删除"为了保活"的所有进程
- [ ] 仅保留"风险隔离"或"内存隔离"必要的进程
探索性思考:为什么"删进程"是工程上最有效但最难执行的优化?因为删进程涉及架构变更——架构师本能抵抗。好的工程文化要让"删代码"成为荣誉而非耻辱。删一个无用进程比加一个新功能更有价值。
▶▶ 回扣 §02 案例:方法派 Day 4 第 1 层"删除 3 个非必要进程"是案例中最大的杠杆——单步收益 PSS -47%,远超后续所有优化之和。找到"最大杠杆"是优化的关键。
# 14.治理二层 IPC
核心命题:让 IPC 不再是瓶颈。让通信少、让通信批、让通信用对方式。
# 14.1 IPC 批量化
机理:§17.4 实验循环 100 次 vs 批量 1 次差 60×。
代码:
// 反例
configs.forEach { key ->
val value = remoteService.get(key) // 100 次跨进程
...
}
// 正例
val map = remoteService.getBatch(configs) // 1 次
2
3
4
5
6
7
8
收益:循环场景从 500ms 阻塞降到 8ms。
边界:批量 API 设计要避免单次过大(> 100KB 走共享内存)。
# 14.2 主进程本地缓存 + Push 失效
机理:跨进程数据"启动加载一次 + 变更时 push 失效"是最高效模式。
代码:
class ConfigCache {
private val cache = ConcurrentHashMap<String, String>()
fun init() {
cache.putAll(remoteService.getAll()) // 启动一次
}
fun onConfigChanged(key: String, value: String) {
cache[key] = value // 远端变更时通过 broadcast 通知
}
fun get(key: String) = cache[key] // 零 IPC
}
2
3
4
5
6
7
8
9
10
收益:日常读 0 IPC;变更时一次 push。
边界:内存换性能,仅适合数据量不大(< 10MB)的场景。
# 14.3 大数据用共享内存
机理:§17.2 实验1MB Binder < 1000 op/s 实际不可用。
代码(Android SharedMemory API 27+):
SharedMemory shm = SharedMemory.create("frame", frameSize);
ByteBuffer buffer = shm.mapReadWrite();
remoteService.submitFrame(shm); // 通过 Binder 传递句柄
2
3
收益:图片/视频帧/大 JSON 传输延迟 -90%。
边界:首次建立 mmap 有 1-2ms 开销;少次大数据可能不如直接 IPC。
# 14.4 异步化 IPC + Future 模式
机理:主线程同步等远程返回 = ANR 风险。
代码:
// oneway 方法不阻塞调用方
interface IRemoteService {
oneway void doAsync(in Request r);
}
2
3
4
收益:主线程永不被 IPC 阻塞。
边界:oneway 不能有返回值;需 RemoteCallback 异步回调。
# 14.5 IPC 治理决策表
| 数据量 | 调用频次 | 推荐方式 |
|---|---|---|
| < 1KB | < 10/s | 直接 IPC |
| < 1KB | > 100/s | 批量化 / 缓存 |
| 1KB-100KB | < 10/s | 直接 IPC |
| 1KB-100KB | > 10/s | 批量化 |
| > 100KB | 任意 | 共享内存 |
| > 1MB | 任意 | 必须共享内存 |
探索性思考:为什么"IPC 频次"比"IPC 单次开销"更重要?因为单次 4μs 的开销看似可忽略,但 100 次/s × 86400s = 864 万次/天,累积成本巨大。性能优化经常是"积少成多" —— 单点不重要,总量决定成败。
▶▶ 回扣 §02 案例:主→Push 进程每秒 35 次 IPC 心跳——单次 4μs 看不出问题,每天累积 300 万次确实是负担。删除 Push 进程后这 300 万次/天直接消失。
# 15.治理三层隔离
核心命题:不是所有"为了稳定"的拆都合理。只拆"真值得"的。
# 15.1 WebView 强制进程外(必要的拆)
机理:H5 内容 JS 崩溃 / 内存泄漏会直接拖垮主进程。Android WebView 默认在主进程,必须显式启用多进程。
代码:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
WebView.setDataDirectorySuffix("multi")
// 多进程 WebView 通过 WebView 5.0+ 自动启用(API 28+)
}
}
2
3
4
5
6
7
收益:§18.1 案例主进程崩溃率 0.18%→0.04%。
边界:WebView 进程也占 60-80MB;H5 业务少时不必拆。
# 15.2 高风险第三方 SDK 进程隔离
机理:闭源 SDK(如某些视频解码 / RTC / 推流)质量不可控,崩溃会拖垮主进程。
代码:
<service android:name=".RtcService"
android:process=":rtc" />
2
收益:第三方崩溃不再影响主进程。
边界:SDK 必须支持跨进程调用;增加 IPC 复杂度。
# 15.3 大对象处理进程隔离
机理:图像识别 / 视频解码 / 大文件处理瞬间内存可能 200MB+,独立进程被杀也不影响主进程。
代码:见 §15.2。
收益:主进程内存稳定,OOM 风险隔离。
边界:处理结果通过共享内存返回;启动开销需评估。
# 15.4 ProcessExitReason 监控
机理:Android 11+ getHistoricalProcessExitReasons 给出进程被杀原因。
代码:
val am = context.getSystemService(ActivityManager::class.java)
am.getHistoricalProcessExitReasons(null, 0, 100)
.forEach {
upload(it.reason, it.processName, it.pss, it.timestamp)
}
2
3
4
5
收益:分类统计被杀原因(LMK/ANR/Force Stop/JNI Crash),针对性优化。
边界:仅 Android 11+;部分 OEM 实现不全。
# 15.5 隔离的判断标准
值得拆的标准:
- ✅ 代码质量不可控(闭源 SDK)
- ✅ 内存占用瞬间 200MB+
- ✅ 崩溃率 > 0.5%(拖垮主进程概率大)
- ✅ 业务必要(如 Web 内容)
不值得拆的标准:
- ❌ 仅"为了保活"
- ❌ 业务正常(崩溃率 < 0.05%)
- ❌ 内存占用稳定且小
- ❌ "感觉应该拆"(无数据支持)
探索性思考:为什么"隔离"不应该是默认决策?因为隔离的代价是"立刻可见"(内存、IPC),收益是"事件性"(异常时才出现)。如果异常发生概率极低,隔离的期望收益就低于代价。默认单进程,只在数据证明必要时才拆 —— 这是工程上的"奥卡姆剃刀"。
▶▶ 回扣 §02 案例:方法派保留 WebView 进程,因为 H5 业务多崩溃概率高;删除 Push/守护/拉活进程,因为它们的"隔离收益"是零(无 H5 风险,仅为保活)。分类施治才是正解。
# 16.治理四层重启
核心命题:放弃保活,拥抱快速重启。这是范式转移。
# 16.1 关键状态持久化
机理:被杀时立即持久化(最后页面/滚动位置/输入草稿/登录态/购物车等)。
代码:
class StateRecoveryManager(context: Context) {
private val prefs = context.getSharedPreferences("recovery", MODE_PRIVATE)
fun saveState(page: String, scrollY: Int, draft: String?) {
prefs.edit().apply {
putString("last_page", page)
putInt("scroll_y", scrollY)
putString("draft", draft)
putLong("save_ts", System.currentTimeMillis())
apply() // 异步,不阻塞
}
}
fun restoreOnStart(): RestoreState? {
val saveTs = prefs.getLong("save_ts", 0)
if (System.currentTimeMillis() - saveTs > 24*3600*1000) return null
return RestoreState(/* ... */)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
收益:§17.5 实验用户回到状态时长 4.5s→0.95s。
边界:状态版本兼容(旧状态 + 新代码);敏感信息加密。
# 16.2 启动期优先恢复状态
机理:启动序列改为"先渲染上次页面骨架 → 恢复滚动位置 → 再异步加载数据"。
代码:见 卷四·01 冷启动篇分级初始化策略。
收益:感知冷启时间从 3s 降到 < 1s。
边界:复杂业务需逐场景设计;不可滥用占位骨架。
# 16.3 合规后台任务用 WorkManager
机理:替代守护进程,系统调度,合规可靠。
代码:
val work = PeriodicWorkRequestBuilder<MyWorker>(15, TimeUnit.MINUTES)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"sync",
ExistingPeriodicWorkPolicy.KEEP,
work
)
2
3
4
5
6
7
8
9
10
收益:合规且电池友好;不被系统打压。
边界:调度延迟 10s-15min(视约束),实时性差任务不适用。
# 16.4 长连接业务用前台 Service
机理:§17.3 实验唯一有效保活手段。
代码:
class ChatForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, channelId)
.setContentTitle("聊天已连接")
.setSmallIcon(R.drawable.ic_chat)
.build()
startForeground(NOTIFICATION_ID, notification)
return START_STICKY
}
}
2
3
4
5
6
7
8
9
10
收益:A13 8h 存活率 75%。
边界:必须有用户可见的通知;滥用会被举报"流氓应用"。
# 16.5 ROI 排序
| ROI | 优化项 | 收益 | 成本 | 风险 | 对应章节 |
|---|---|---|---|---|---|
| 极高 | 评估并删除非必要子进程 | 内存 -30-50% | 1-2 周 | 中 | §13.1-13.4 |
| 极高 | 大数据 IPC 改共享内存 | IPC 延迟 -90% | 1 周 | 低 | §14.3 |
| 极高 | 关键状态持久化 + 快速重启 | 体验 4.5s→1s | 2-3 周 | 中 | §16.1-16.2 |
| 高 | IPC 批量化 + 本地缓存 | IPC 频次 -80% | 1-2 周 | 低 | §14.1-14.2 |
| 高 | 放弃暗保活 + WorkManager | 系统评分提升 | 1-2 周 | 中 | §16.3 |
| 高 | Push 进程合并到主 | 节省 50-80MB | 1 周 | 中 | §13.2 |
| 中 | WebView 进程隔离 | 主进程崩溃 -75% | 几天 | 低 | §15.1 |
| 中 | ProcessExitReason 监控 | 长期防退化 | 几天 | 低 | §15.4 |
| 中 | 异步 oneway IPC | 主线程不被阻塞 | 1 周 | 低 | §14.4 |
| 中 | 前台 Service(长连接) | 长连接稳定 | 1 周 | 中 | §16.4 |
# 16.6 避免反向收益
- 过度拆进程:每个业务都独立进程,内存翻 N 倍。
- 死磕保活:A11+ 无效,投入精力对抗系统,最终被卸载。
- 小数据用共享内存:建立 mmap 有开销,反而比 Binder 慢。
- 循环跨进程查询:累积可达秒级,制造 ANR 风险。
- 加强保活治被杀:§02 案例第 1 周翻车的根因。
探索性思考:为什么"放弃保活"这么难?因为产品经理和老板"想要应用一直活着"——这是直觉但反工程现实。好的工程师要会"教育产品" —— 用数据告诉他们"暗保活不仅无效,还伤体验"。说服比写代码更难。
▶▶ 回扣 §02 案例:方法派 Day 4 第 4 层"快速重启 + 状态恢复"是范式转换的关键——把"保活成功率"指标换成"重启 + 恢复时长"指标。指标变了,思维就变了。
# 17.求证实验 ⭐
本节是"为什么这些优化生效"的实证基础。每个实验严格遵循"6 步求证法"。
# 17.1 实验一:进程启动代价
猜想:进程启动几乎瞬时。
假设:Android Zygote fork(共享父 VM)~200ms;iOS exec(每次重启 dyld)~250ms;纯 Linux fork ~10ms。
执行:
| 平台 | 测试场景 | 启动时长(中位) |
|---|---|---|
| Android Zygote fork | ContentProvider 跨进程 | 230 ms |
| Android no-Zygote(手动 spawn) | 命令行 | 800 ms |
| iOS App Extension | 触发 sharing extension | 280 ms |
| iOS posix_spawn 命令行 | /usr/bin/true | 50 ms |
| Linux fork + exec | /bin/true | 8 ms |
| Web 新 Tab | window.open 启动新进程 | 220 ms |
验证:
- Android 与公式预测 ~200ms 一致。
- iOS Extension 比公式略高(含 XPC 握手)。
思考:
- 应用级进程启动开销 200-300ms,远高于线程创建(μs 级)。
- 子进程要保持常驻,不要频繁拉起 / 销毁。
- 一次性任务用线程,长期任务才用进程。
# 17.2 实验二:IPC 吞吐量
猜想:IPC 吞吐与数据大小无关。
假设:小数据吞吐被切换开销主导;大数据吞吐受序列化 + 拷贝主导。
执行:
| 数据大小 | Android Binder | iOS XPC | Web postMessage |
|---|---|---|---|
| 100B | 220K op/s | 180K op/s | 90K op/s |
| 1KB | 180K op/s | 150K op/s | 80K op/s |
| 10KB | 65K op/s | 55K op/s | 35K op/s |
| 100KB | 9K op/s | 7K op/s | 5K op/s |
| 1MB | 950 op/s | 700 op/s | 500 op/s |
验证:
- 小数据被调用开销主导,大数据被拷贝主导。
- 1MB 数据已经接近不可用。
思考:
- IPC 吞吐量随数据大小急剧下降。
- 100B 数据 200K/s 可用;100KB 数据 < 10K/s 需要谨慎;1MB+ 必须用共享内存。
- 频繁小数据交互直接 IPC;大数据传输(图片 / 视频帧)必须共享内存。
# 17.3 实验三:保活策略效果
猜想:保活仍然有效。
假设:Android 8.0+ 后系统强力打压;只有前台 Service 仍有效。
执行:
| 保活手段 | A7 30min | A11 30min | A13 30min | A13 8h |
|---|---|---|---|---|
| 无 | 35% | 15% | 10% | 0% |
| 双进程互拉 | 70% | 25% | 18% | 2% |
| JobScheduler | 60% | 45% | 40% | 25% |
| 前台 Service | 95% | 88% | 85% | 75% |
验证:
- 双进程互拉在 A11+ 几乎失效。
- JobScheduler 是合规但效果有限。
- 只有前台 Service(带通知)才能稳定保活。
思考:
- Android 11+ 后"暗保活"几乎全部失效。
- 投入做"暗保活"是低 ROI 工作,应放弃。
- 转而做"快速重启 + 状态恢复"。
▶▶ 回扣 §02 案例:经验派第 1 周"加强双进程互拉保活"是把"硬抗系统"变现为线上事故的真实样本。
# 17.4 实验四:CP 风暴
猜想:循环 IPC 与批量 IPC 差异不明显。
假设:每次 ContentProvider 查询 ~5ms;100 次 = 500ms 主线程阻塞;批量一次 ~8ms。
执行:
| 实现 | 总耗时 | 子进程被拉起次数 |
|---|---|---|
| A 循环 100 次 | 480 ms | 1 次 |
| 极端:A + 每次后子进程退出 | 22,000 ms | 100 次 |
| B 批量一次 | 8 ms | 1 次 |
| C 本地缓存(启动加载) | 0.05 ms | 0 次 |
验证:
- A 循环 100 次主线程阻塞 480ms 直接 ANR 临界。
- 子进程反复唤醒时极端 22 秒。
- B 批量比 A 快 60×;C 本地缓存比 B 快 160×。
思考:
- 跨进程 ContentProvider 严禁循环调用。
- 必须批量化或本地缓存。
- "看起来 5ms 一次的调用"在循环中可累积到秒级。
# 17.5 实验五:快速重启 vs 暗保活
猜想:暗保活体验更好。
假设:暗保活成功率 < 40%,且拉起仍需冷启;快速重启 100% 成功,且 < 800ms 完成。
执行:
| 离开时长 | A 暗保活存活率 | A 用户回到状态时长 | B 重启时长 | B 用户回到状态时长 | A 评分 | B 评分 |
|---|---|---|---|---|---|---|
| 30 min | 38% | 2.5s(命中)/ 4.2s(冷启) | 100% | 0.9s | 3.2 | 4.6 |
| 2 h | 18% | 4.5s | 100% | 0.9s | 2.8 | 4.5 |
| 8 h | 5% | 4.8s | 100% | 0.95s | 2.4 | 4.5 |
验证:
- B 实测平均回到状态的时长 < 1s。
- A 在长时间离开后存活率几乎为零。
- 用户主观分 B 显著高于 A。
思考:
- 放弃保活 + 投资快速重启是更高 ROI 的路径:100% 成功率 vs 5-38% 存活率。
- 关键状态(最后页面 / 滚动位置 / 输入草稿 / 登录态)持久化到磁盘。
- 启动期优先恢复状态。
# 17.6 五大实验启示
进程启动代价(200-300ms) → 不要频繁拉起子进程 ─┐
IPC 吞吐量 → 大数据必用共享内存 │
保活几乎失效 → 转向"快速重启 + 恢复" ├─▶ 多进程 = 重资源,每开必算账
ContentProvider 风暴 → 严禁循环 IPC,必须批量化 │
快速重启 vs 暗保活 → 100% 成功率 + <1s 体验 ─┘
2
3
4
5
统一启示:
- 多进程是"重武器":每开一个进程都要算清成本收益。
- 顺应系统而非对抗:现代 OS 都在打压持久后台。
- 快速重启 > 永不被杀:投资"被杀后的恢复体验" ROI 高得多。
- IPC 频次 > IPC 单次开销:单次便宜的接口,循环调用一样杀人。
- 大数据必用共享内存:Binder/XPC 在 1MB+ 数据下不可用。
▶▶ 回扣 §02 案例:方法派 8 天闭环每一步都对应本节实验。实验是优化前的"必经之路"。
# 18.实战案例
# 18.1 跨端同构案例:WebView 进程隔离
背景:某社交应用主进程因加载 Web 内容偶发崩溃。
现象:
- Android:第三方网页 JS 引起 WebView 崩溃,导致主进程崩溃
- iOS:WKWebView 已经进程外(系统级),无问题
- Web:iframe 默认进程外(部分浏览器),无问题
根因:Android WebView 默认在主进程渲染(除非显式启用多进程模式)。
治理:
- Android:启用 WebView 多进程(
WebView.setDataDirectorySuffix) - iOS:保持 WKWebView(已经默认进程外)
- Web:用户层不用关心
效果:
| 平台 | 优化前主进程崩溃率(含 Web 原因) | 优化后 |
|---|---|---|
| Android | 0.18% | 0.04% |
| iOS | 0.05% | 0.05%(无变化) |
| Web | N/A | N/A |
核心洞察:跨端原则 = 风险代码必须进程隔离。Android 需要显式启用,iOS / Web 默认提供。
# 18.2 平台特异案例:iOS Extension 内存超限
背景:某 App 的 Share Extension 在分享大图时崩溃。
根因:
- iOS Extension 内存限制 30-50MB
- 用户分享 4K 图片时解码占用 80MB+
- 系统直接杀掉 Extension
治理:
- 大图分享前先降采样到 < 2K
- 用 mmap 读图避免一次性载入
- 关键状态用 App Group 与主 App 共享
效果:Share Extension 崩溃率 8% → 0.3%。
洞察:iOS Extension 是"小内存 + 短生命周期"环境 —— 工程师不能用主 App 的内存假设来设计 Extension。
# 18.3 反例案例:暗保活引发的恶性循环
背景:某 App 投入大量精力做"双进程互拉 + JobScheduler + 系统服务绑定"组合保活。
结果:
- Android 11+ 后系统识别"贪婪应用",反向打压
- 主进程被杀率从 5% 升到 25%
- 用户体验更差,差评激增
- 浪费了 3 个月研发时间
修复:
- 全部撤销保活手段
- 投资"快速重启 + 状态恢复"
- 关键长连接业务用前台 Service(带通知)
效果:
- 主进程被杀率 25% → 4%(系统不再打压)
- 用户感知"启动快"(< 1s)
洞察:对抗系统从未赢过——iOS / Android 的进化方向都是"限制后台贪婪",应用应顺应而非对抗。
# 19.防劣化体系
# 19.1 三道防线总览
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 编码期 Lint │ → │ CI 卡口 │ → │ 线上 SLO │
│ IDE 即时提示│ │ Macrobench │ │ 监控告警 │
└────────────┘ └────────────┘ └────────────┘
2
3
4
# 19.2 编码期 Lint
自定义规则:
- 新增
<service android:process=":xxx">→ 警告(必须 review) - 循环跨进程调用
contentResolver.query→ 警告 - 主线程同步等待 IPC → 错误
- 大数据(> 100KB)用 Binder → 警告(建议共享内存)
- 守护进程相关代码 → 警告(A11+ 失效)
- 未声明 ProcessExitReason 监听 → 警告
# 19.3 CI 卡口
性能基线测试:
@Test
fun processCountBenchmark() = benchmarkRule.measureRepeated(
metrics = listOf(MemoryUsageMetric(Mode.Last)),
iterations = 5
) {
startActivityAndWait()
repeat(20) { exerciseFeature() }
}
2
3
4
5
6
7
8
卡口规则:
- 进程数增加 → 阻断 PR(需架构师 review)
- 常驻 PSS 退化 ≥ 10% → 阻断
- IPC 延迟退化 ≥ 20% → 警告
- 启动期进程拉起次数增加 → 警告
# 19.4 线上 SLO
| 指标 | 目标 | 告警阈值 |
|---|---|---|
| 应用进程数稳态 | < 3 | > 5 |
| 主进程 PSS | < 200MB | > 350MB |
| 主进程被杀率 | < 5% | > 10% |
| IPC 失败率 | < 0.1% | > 1% |
| 进程冷启时长 P95 | < 800ms | > 1.5s |
| 推送到达率 | > 95% | < 90% |
# 19.5 文化建设
- 进程预算:新模块申请独立进程必须有"为什么"(不能默认)
- 进程 Code Review:进程相关 PR 必有架构师 review
- 进程 OKR:进程数稳态进 OKR
探索性思考:为什么"进程治理"特别需要架构师 review?因为进程决策具有长期效应 —— 加一个进程容易,删一个进程难(业务依赖建立后)。预防胜于治疗 —— review 阶段拦下"轻易加进程"的 PR,远比上线后清理省力。
# 20.跨平台速查
# 20.1 工具速查
| 平台 | 进程清单 | IPC 采集 | 生命周期 |
|---|---|---|---|
| Android | ActivityManager / ps | Perfetto Binder | getHistoricalProcessExitReasons |
| iOS | sysctl | Instruments XPC | terminationReason |
| Web | chrome://process-internals | Performance Panel | (无标准) |
| Linux | ps -ef | strace -e ipc | (cgroup logs) |
# 20.2 关键 API 速查
| 目的 | Android | iOS | Web |
|---|---|---|---|
| 启用子进程 | <service android:process=":xxx"> | App Extension | new Worker |
| 同步 IPC | AIDL | NSXPCConnection | postMessage |
| 异步 IPC | oneway AIDL | XPC handler | postMessage |
| 共享内存 | SharedMemory(API 27+) | mmap + shm_open | SharedArrayBuffer |
| 进程退出原因 | getHistoricalProcessExitReasons | (无) | (无) |
| 后台调度 | WorkManager | BGTask | Service Worker |
| 长连接保活 | startForeground | VoIP / location | (无) |
| WebView 进程外 | setDataDirectorySuffix | WKWebView 默认 | (默认) |
# 20.3 各平台优化清单
Android:
- [ ] 进程清单评估,删除非必要进程
- [ ] WebView 启用多进程(H5 业务多)
- [ ] Push 改 FCM/厂商推送 + 主进程接收
- [ ] 删除守护/拉活进程
- [ ] IPC 批量化,主进程缓存
- [ ] 大数据用 SharedMemory
- [ ] 关键状态持久化 + 快速重启
- [ ] WorkManager 替代守护
- [ ] ProcessExitReason 上报
- [ ] Lint 拦截新增进程
iOS:
- [ ] 减少 dylib 数量
- [ ] 减少 +load 方法
- [ ] App Group + UserDefaults 共享
- [ ] Extension 内存严控
- [ ] BGTask 替代保活
- [ ] WKWebView(自动进程外)
Web:
- [ ] Site Isolation 利用
- [ ] Web Worker 处理 CPU 密集
- [ ] SharedArrayBuffer + Atomics(高性能场景)
- [ ] Service Worker 离线缓存
# 21.总结与延伸
# 21.1 五条核心原则
- 进程是重资源:每开一个进程都要算清成本收益。
- 顺应系统而非对抗:现代 OS 都在打压持久后台,硬抗代价大。
- 快速重启 > 永不被杀:投资"被杀后的恢复体验",比投资"保活"ROI 高得多。
- IPC 频次 > IPC 单次开销:单次便宜的接口,循环调用一样杀人。
- 大数据必用共享内存:Binder/XPC 在 1MB+ 数据下不可用。
# 21.2 五个常见误区
| 误区 | 真相 |
|---|---|
| "拆进程 = 稳定性提升" | 错。除非是真正的高风险代码,拆进程只增加成本 |
| "保活做得好就不被杀" | 错。Android 11+ 暗保活几乎全失效 |
| "Binder 跟函数调用差不多" | 错。3 个量级差距(μs vs ns) |
| "iOS 没进程问题" | 错。Extension 内存限制是新挑战 |
| "SharedMemory 总比 Binder 快" | 错。建立 mmap 有开销,少次大数据用 Binder 更快 |
# 21.3 一句话总结
进程是"隔离单元,也是性能边界"。多进程优化的核心法则:删除非必要进程(最大杠杆)+ IPC 批量化与共享内存(让通信不再瓶颈)+ 风险代码精准隔离(值得拆才拆)+ 放弃保活拥抱快速重启(顺应而非对抗系统)。Android 11+ 时代,"保活"已经是反模式,"快速重启 + 状态恢复"才是正解。
# 21.4 延伸阅读
卷二·04 线程模型与调度优化:进程 vs 线程的对比卷二·03 OOM 异常与低内存治理:多进程是 OOM 隔离重要手段卷四·01 App 冷启动优化:进程启动开销是冷启大头卷三·04 ANR 监控与治理:跨进程同步 IPC 是 ANR 常见原因卷四·05 功耗与电量优化:保活伤电池
下一篇预告:
卷二·06 IO 与存储性能—— 让"持久化"不成为性能瓶颈。