管线(pipeline)=相机数据通路:图像数据从传感器到屏幕流经的整条硬件/软件链路——传感器 → ISP → DMA → 缓冲池(内核wired 967MB) → mediaserverd(540MB) → IOSurface → backboardd(21MB) → 屏幕。Camera.app 只是这条链路的遥控器(壳 26MB)。文中"管线需求/管线就位/管线配置"均指这条链路:需求量由会话配置(分辨率/格式/帧深度)决定,与内存环境无关;就位 ~3s 是建链时间;管线属于媒体会话不属于进程——杀 App 不立即拆管线,故释放有异步尾巴。
wired:物理页锁定驻留(不压缩/不换出/不丢弃)——相机场景里是 ISP DMA 缓冲池,记在全局计数器而非任何进程账上。
speculative 与文件页的关系:speculative ⊂ 文件页——两者是正交账本(按内容分:文件页/匿名页;按队列分:active/inactive/speculative/free/wired)。speculative 是"预读进内存、但还没有任何进程碰过的文件页"(vm_page_speculate 断言 internal==FALSE——匿名页没资格):进程读文件时 pager 顺手多读几页(clustered pagein)赌未来命中,落进 10 格×500ms=5 秒保护期的 age bins;保护期内被进程碰到就"毕业"转正式文件缓存(赌赢,省一次 I/O),到期没人碰就进 aged 队列——内存压力时第一个被摘(优先级在 inactive 之前),clean 页零 I/O 直接回 free(赌输,零损失)。池子有总量上限(可分页页数的百分比);jetsam 的可用内存公式把它算进去(active+inactive+free+speculative)——"带着收益的 free"。实测:场景二 20,445→542 页=311MB 一秒内摘光(相机供给第一笔钱);场景一 +54MB(相机框架预读落 bins)。
读文件页 ──预读──▶ speculative bins(5s 保护)──触碰──▶ 正式文件缓存(active/inactive) └──── aged 队列 ──压力──▶ 第一个被摘(先于 inactive)→ 零成本回 free 内容账本:文件页 = 正式缓存 + speculative / 匿名页(只能压缩/换出,永远不能"直接丢")
整机 ~1.0–1.6GB,四归属:内核 wired 967MB(55%,无主 DMA,不进任何进程账) + mediaserverd iokit_mapped ~540MB(40%)+ Camera 壳匿名 26–28MB(2%)+ 显示侧 ~21MB。两场景 wired 上涨差 <1%(62,3xx vs 62,391 页)——管线需求与内存环境无关,free 只剩 55MB 照样拿到 974MB。wired 属于"预览"不属于"启动"(冷启动 Δwired=0)。
内存全部就位 ~3s,free=55MB 与 free=2.32GB 持平。分配原语极快:wire 与匿名同速(5.2 vs 5.0GB/s,3.01µs/页——"wired 分配慢"是误解);活体管线斜率 356–570MB/s,瞬时 ≥1.6GB/s——慢在管线编排(DART/驱动会话),不在 wire。
水位驱动的流水线,不是预案。场景二极限局(free 55MB 供 974MB):后台压缩波抢跑 +474MB(相机启动前就发生) → 文件页瞬时逐出 295MB(speculative clean 页零 I/O 丢弃)→ 匿名压缩 695MB gross(突发 ~319MB/s)。freeze/jetsam 零触发,五 App 全存活——相机 1GB 停在页级腾挪,不需要进程级献祭。
回收成本阶梯:purgeable 丢弃 ≈0 < 文件页 clean 丢弃(≥295MB/s 零 I/O)< munlock 20GB/s < 压缩器 117–328MB/s < 驱动 teardown。杀相机:bulk 830–854MB <1s + 死会话压缩页直接摘除 223MB(不经解压)+ 尾 ~8s;存活 App 压缩页不回吐(单向阀)。所有计数器变化可由机制解释,无泄漏——回收机器是水位驱动的状态机:场景一 compressions +0 vs 场景二 +695MB。
| 场景一:后台干净 | 场景二:五大 App 后台 | |
|---|---|---|
| 前置操作 | killall 所有可杀 App;轻压触发文件页回收(40s) | 依次启动抖音、淘宝、京东、网易云音乐、bilibili(每 App ps 验证存活后下一个) |
| 进入条件 | free > 2GB ✓(vm_stat 确认 152,560 页 = 2.32GB) | free = 3,551 页 = 55MB(五 App 吃光水位) |
| 操作序列 | uiopen 相机 → 进预览静置 30s → killall Camera → 观察释放 22s | 同左(同参数对照) |
| 采样 | 0.5s×130 点全计数器:vm_stat(free/wired/file-backed/anonymous/压缩器/speculative)+ Camera/mediaserverd 双进程 appscan | 同左 |
| 累计计数器 | 三时点快照(启动前/预览稳态/杀后):reactivated/purged/compressions/decompressions/pageins/pageouts | 同左 |
| 归属 | 内存类型 | 场景一实测 | 场景二实测 | 机理 |
|---|---|---|---|---|
| 内核(无主 wired) | wired | +62,3xx 页 ≈ 967MB | +62,391 页 = 974MB | ISP DMA 缓冲池/驱动固件——kmem_alloc_contig 物理就位,不进任何进程 footprint,只有全局 wired 计数器可见 |
| mediaserverd | iokit_mapped 为主 | fp 261.1→604.2MB | 262.0→606.7MB | AVFoundation 服务端——IOSurface 回映射记它的账 |
| Camera 进程 | 匿名 internal | fp 26.2MB | fp 28.3MB | 纯 App 壳(UI/按钮/权限对话框) |
| Camera 壳+msd 会话的匿名页 | anonymous(整机计数器) | +422MB(45,321→72,322 页) | 先 +(唤醒)后 -367MB(转入压缩器) | msd internal ~93MB + 壳 + UI 工作集 |
| 相机自身文件页 | file-backed | +105MB 载入(pageins 125MB) | 同样载入 ~91–132MB(与逐出对冲) | 二进制/AVFoundation/CoreMedia/Metal shader 从磁盘载入 |
| backboardd 等显示侧 | iokit_mapped | ~+21MB(早前普查) | 同左 | 显示合成侧 surface 映射 |
关键不变量:管线需求与内存环境无关——两场景 wired 上涨差 <1%(62,3xx vs 62,391 页,≈967–974MB)。free 充足时拿 967MB,free 只剩 55MB 时照样拿到 974MB。
| 供给来源 | 场景一(free 2.32GB) | 场景二(free 55MB) |
|---|---|---|
| free 池直接支付 | 全额支付(-97,945 页 = 1,530MB) | -34,007 页 = 531MB(t=19 前后台压缩波已把 free 抬到 584MB) |
| 文件页逐出(file-backed) | 0(零逐出——反向 +105MB 载入) | 瞬时 -295MB(≤1s)→ 回填 +91MB → 净 -204MB |
| └ speculative 子集 | +54MB(预读新增) | 20,445→542 页 = -311MB 被清光 |
| 匿名页压缩转存 | 0(compressions +0!全程零压缩) | compressions +44,507 页 = 695MB 总压缩(stored +533MB 净) |
| purgeable 丢弃 | purged +291 页(5MB,微量) | purged +1,911 页(30MB) |
| 后台 App 主动压缩波(相机未启动时) | n/a | t=7 +331MB、t=17 +143MB(进场即压,抢跑供给) |
| freezer / jetsam | 0 | 0——零击杀,五 App 全存活 |
场景二物理账闭环(杀相机瞬间):wired -830MB + 压缩器死会话页直接摘除释放物理 ~104MB ≈ free +940MB ✓。一台 free 只剩 55MB 的机器,先靠后台压缩波抢跑 +474MB,再靠文件页瞬时逐出 295MB + 匿名压缩 695MB 总量,供相机拿到 974MB wired,五个后台 App 一个都没死。
| 里程碑 | 场景一 | 场景二 |
|---|---|---|
| 进程可见(pid 出现) | ≤1s | ≤1s |
| wired 主体到位(>90%) | ~2s(t=20→22) | ~2s(t=20→22) |
| mediaserverd fp 稳定(~605MB) | ~3s | ~3s |
| 内存全部就位 | ~3s | ~3s(不受影响!) |
启动耗时的本质:内存侧全就位只需 ~3 秒,且在 free=55MB 的 starvation 条件下与 free=2.32GB 持平——回收腾页与管线分配在不同核上并行,互不等待。
| 微基准(256MB 档) | ||
|---|---|---|
| 操作 | 吞吐 | µs/16K页 |
| VA 预留(mmap 不触碰) | ~瞬时 | — |
| 匿名 zero-fill 触碰 | 5,025 MB/s | 3.11 |
| mlock 冷 wire(fault+wire) | 5,189 MB/s | 3.01 |
| mlock 热 wire(页已在) | 9,959 MB/s | 1.57 |
| munlock 解 wire | 20,009 MB/s | 0.78 |
| purgeable 触碰 | 5,280 MB/s | 2.96 |
| purgeable set VOLATILE | 19,024 MB/s | 0.82 |
| purgeable set EMPTY | ~瞬时 | ≈0 |
| munmap | 31,166 MB/s | — |
| 活体斜率(相机管线实测) | |
|---|---|
| 阶段 | 速率 |
| 场景一 t=20→23:wired +889MB / 2.5s | ~356 MB/s |
| 场景二 t=20→22:wired +855MB / 1.5s | ~570 MB/s |
| 瞬时突发(0.5s 窗口,早前 0.4s 采样) | ≥1.6 GB/s |
| mediaserverd fp(+341MB / 2s) | ~170 MB/s |
| Camera 壳 footprint | ~2.4 MB/s(微不足道) |
微基准 5GB/s vs 活体 356–570MB/s 的差距 = 管线配置开销(DART 映射、驱动会话协商、surface 池建立)——慢的不是 wire 原语,是管线编排。
"wired 分配慢"是误解:冷 wire 3.01µs/页 与匿名 zero-fill 3.11µs/页同速。1GB wired 的内核机械时间只需 ~200ms。惰性申请(用到才 wire)没有任何性能代价。
五 App 后台进场(相机还没启动!) │ t=7: 压缩器 stored 48,854→70,035 = +331MB 主动压缩波(free +437MB) │ t=17: 再 +143MB(free 抬到 584MB)——抢跑供给:后台 App 进后台即被压 ▼ t=20 uiopen 相机 free 37,460→1,751(池耗尽) wired 50,278→104,653 file-backed 129,293→110,410:≤1 秒逐出 295MB(speculative 20,445→542 清光) │ ▼ t=22(~2s) wired 112,513 主体到位 anon 120,005→100,210(-311MB 转入压缩器) 压缩器 stored 79,019→109,579(+479MB / 1.5s —— 突发 ~319MB/s) │ ▼ t=25(~4s 全部就位) file-backed 回填 +91MB(相机自身框架 pagein 中,pageins 累计 +8,483 页=132MB) │ ▼ 稳态 30s:五 App 全存活(freeze=0 零击杀) │ ▼ t=57 killall Camera wired 112,907→57,457(bulk -830MB <1s); free +940MB 压缩器 stored 112,907→98,646(-223MB):死会话压缩页直接摘除(decompressions 仅 +194) t=57→65: 尾巴 wired→49,396(驱动 teardown ~8s); 存活 App 的压缩页原样保留
供给是流水线不是预案:第一波甚至发生在相机启动之前(后台 App 进场即被主动压缩——抢跑);相机分配逼近水位时同步唤醒逐级加深——free 池 → 文件页逐出(clean 页零 I/O 丢弃)→ 匿名压缩 → freezer(未触及)→ jetsam(未触及)。相机 1GB 停在前三级:页级腾挪足够,进程级献祭不需要。
| 回收机制 | 实测效率 | 说明 |
|---|---|---|
| 文件页逐出(场景二) | ≥295MB/s 瞬时(≤1s 逐出 295MB);零 I/O 成本(clean 页直接丢) | 几乎全部来自 speculative 子集(投机预读的干净页)——最便宜的供给路 |
| 压缩器收纳(场景二) | 突发 ~319–328MB/s(1.5s 收 479MB);持续 ~117MB/s;压缩比 2.15:1 | compressions 累计 +695MB(gross,含churn),stored 净 +533MB |
| 杀相机 bulk 释放 | 742–854MB <1s | munlock 微基准 20GB/s 说明解 wire 原语不是瓶颈——限速的是驱动 teardown 异步 |
| 死会话压缩页摘除 | 223MB 瞬时(随 bulk) | 压缩器条目直接删除、不经 decompression(计数器仅 +194)——释放侧的隐藏通道 |
| 释放尾巴 | ~5–8s 回到基线 | 驱动/会话异步拆除;期间不阻塞新分配 |
| 存活 App 压缩页回吐 | ≈0(单向阀) | 五 App 的压缩页杀后原样保留,恢复时按需解压(~12µs/页) |
| freezer / jetsam | 0 次 | 相机独吃 1GB 不需要进程级回收(早前 5.4GB 冲击才逼出 freezer) |
t(s) wired free filebk anon cmpstr spec cam_fp msd_fp 0 44,499 149,633 100,092 42,926 53,806 8,492 - - ← 基线(free 2.32GB) 19 44,515 145,459 100,882 45,321 53,636 10,054 - - ← uiopen 20 52,360 134,775 103,077 46,191 50,942 11,936 21.0 261.1 ← pid 可见 21 101,479 76,667 103,772 54,535 50,326 12,083 26.6 572.8 22 108,197 48,164 107,582 72,465 50,267 13,458 26.7 598.8 ← fb+105MB/anon+422MB 到位 23 108,744 47,483 107,625 72,573 50,267 13,491 27.3 604.5 25~56 106,49x 50,0xx 107,6xx 72,3xx 50,25x 13,5xx 26.2 603.0 ← 稳态30s纹丝不动 57 51,644 125,618 107,077 52,135 50,205 13,663 - - ← bulk -854MB <1s 65+ 44,613 133,500 107,294 50,994 50,195 13,677 - - ← 基线恢复(~8s)
累计计数器(预览 30s 窗口):reactivated +51、purged +291(5MB)、compressions +0(全程零压缩!)、decompressions +2,445(38MB,msd 唤醒旧压缩页)、pageins +8,038(125MB 磁盘载入)、pageouts 0。
t(s) wired free filebk anon cmpstr spec cam_fp msd_fp 0 51,526 3,551 129,194 152,383 48,854 20,391 - - ← 基线(free 55MB) 7 50,352 28,023 129,230 129,415 70,035 20,410 - - ← 后台主动压缩波+331MB 17 50,363 37,424 129,281 119,885 79,019 20,439 - - ← 再+143MB(相机未开!) 20 55,906 33,215 129,323 118,537 78,999 20,435 20.1 262.0 ← uiopen 21 104,653 1,751 110,410 121,985 78,980 542 27.5 538.2 ← fb 瞬时-295MB! 22 112,513 2,750 114,473 100,210 109,579 2,117 28.3 602.8 ← anon压缩+479MB/1.5s 25 112,669 3,453 116,235 96,526 113,356 2,881 28.1 606.6 ← fb回填+91MB,全部就位 28~56 110,60x 4,6xx 116,7xx 96,8xx 112,9xx 3,4xx 27.1 605.4 ← 稳态 57 57,457 64,783 116,175 90,873 98,646 3,526 - - ← bulk -830MB + 死会话压缩页-223MB 65+ 49,352 74,185 116,530 89,270 98,518 3,617 - - ← 存活App压缩页保留
累计计数器(整个窗口):reactivated +55,158(862MB 工作集唤醒churn)、purged +1,911(30MB)、compressions +44,507(695MB 总压缩)、decompressions +866(14MB)、pageins +8,483(132MB,与场景一的 125MB 几乎一致)、pageouts +73(1MB)。杀后:decompressions +194、其余 ≈0。
| 场景一(free 充足) | 场景二(free 枯竭) | |
|---|---|---|
| file-backed 净变化 | +105MB(纯需求侧) | -204MB(净供给方) |
| 瞬时动作 | 无逐出 | ≤1s 逐出 295MB |
| speculative 子集 | +54MB(预读增加) | 20,445→542 清光(-311MB) |
| 相机自身载入 | +105MB(pageins 125MB) | +91MB 回填(pageins 132MB——逐出与载入同时进行) |
| 磁盘读(pageins) | +8,038 页 = 125MB | +8,483 页 = 132MB(几乎相同——载入需求恒定) |
文件页的双重角色:free 充足时文件页是需求(相机二进制/AVFoundation/CoreMedia/Metal shader 从磁盘载入 +105MB,零逐出);free 枯竭时它同时是需求与供给——一边逐出 295MB 旧文件页(几乎全是 speculative:投机预读的 clean 页,零 I/O 直接丢),一边载入 132MB 相机自己的文件页。文件缓存被"换血"而不是"清空"。这是 free 池之外的第二供给路,且比压缩器更便宜(无需 CPU 压缩,无需 I/O)。
为什么场景一 compressions +0 而场景二 +695MB:压缩器只在水位压力下工作。场景一 free 2.32GB,回收器全程休眠(甚至反向解压 38MB 唤醒 msd 旧工作集);场景二每一步分配都顶着水位线,压缩器全程满负荷。同一台机器,两种内存环境下回收机器的"开/关"状态完全不同——这正是"内存统计值合理性"要看的本质:统计值的变化方向由环境水位决定,而不是常数。
| 统计值 | 场景一变化 | 场景二变化 | 合理性判定 |
|---|---|---|---|
| wired | +62,3xx(967MB) | +62,391(974MB) | ✅ 差 <1%——管线需求是会话配置的函数,与内存环境无关。峰值后小幅回落 ~2,000 页:驱动建立期的临时映射释放 |
| free | -1,530MB(全额支付) | -531MB(t=19 起;此前后台波已抬 584MB) | ✅ 降幅 > wired 涨幅的部分 = fb 载入 105MB + anon 新增 422MB + spec 预读 |
| file-backed | +105MB(载入) | 净 -204MB(逐出 295 - 回填 91) | ✅ 一需求一供给——见 §3.3 专节。pageins 两场景几乎一致(125/132MB)证明载入需求恒定 |
| anonymous | +422MB(新增工作集) | 净 -367MB(转入压缩器) | ✅ 场景二 anon 下降量 ≈ stored 上升量的内容来源(695MB gross 含唤醒churn再压缩) |
| compressor stored | -53MB(反向解压) | +533MB 净(695MB gross) | ✅ 一收一放:场景一 msd 唤醒解压 38MB;场景二满负荷收纳。杀后场景二 -223MB 为死会话条目直接摘除(decompressions 仅 +194)——释放侧隐藏通道 |
| speculative | +54MB(预读) | -311MB(清光) | ✅ 最便宜的牺牲品:clean、无引用、零成本丢弃——文件页供给的主力 |
| reactivated | +51(≈0) | +862MB | ✅ 场景二后台 App 页被反复触碰唤醒(框架共享库/回调)——高压环境工作集churn的信号,也解释 gross compressions (695MB) > 净 stored (+533MB) 的差 |
| purged | +5MB | +30MB | ✅ purgeable 丢弃量小且方向一致——相机场景非主力 |
| pageins / pageouts | 125MB / 0 | 132MB / 1MB | ✅ 磁盘读恒定(相机载入需求);几乎无写回(clean 页丢弃不需要写) |
| freeze/jetsam | 0 | 0 | ✅ 相机 1GB 在页级腾挪能力内(文件页 295MB + 压缩器 695MB 容量) |
| 杀后释放 | bulk 854MB<1s + 尾~8s | bulk 830MB<1s + 死会话摘除 223MB + 尾~8s | ✅ 两场景一致;存活 App 压缩页保留(单向阀——回吐要付解压代价,系统选择"够用就好") |
合理性总判:所有计数器变化方向均可由机制解释,无异常泄漏。文件页计数器在两场景的方向反转(+105 vs -204)与压缩器的开/关切换(0 vs 695MB)是本次复测最有价值的发现——它们证明 iOS 回收机器是按水位驱动的状态机,不是恒定速率的泵。
| 内存类型 | 申请吞吐 | 释放/回收方式 | 释放吞吐/延迟 | 相机场景的量 |
|---|---|---|---|---|
| VA 预留 | ~瞬时 | munmap | 31 GB/s | — |
| 匿名(zero-fill) | 5.0 GB/s | 压缩器收纳 | ~319–328MB/s 突发 / 117MB/s 持续(2.15:1) | 壳 26MB + 后台 App 大量 |
| wired(用户 mlock) | 5.2 GB/s(冷) | munlock | 20 GB/s | — |
| wired(内核管线) | 活体 356–570MB/s(瞬时≥1.6GB/s) | 驱动 teardown | bulk <1s + 尾 ~8s | 967–974MB(两场景不变) |
| file-backed(供给) | — | clean 页直接丢弃 | ≥295MB/s 瞬时,零 I/O | 场景二净 -204MB |
| iokit_mapped | 跟随 surface 建立(~170MB/s) | surface 生命周期 | 跟随 mediaserverd | ~540MB |
| purgeable | 5.3 GB/s | 直接丢弃 | ≈0 成本 | 30MB(微量) |
总纲:各内存类型申请效率高度一致(原语层 3µs/页量级,差异全在管线编排层);差异全在回收侧的"成本阶梯"——purgeable 丢弃 ≈0 成本 < 文件页 clean 丢弃(零 I/O,≥295MB/s)< munlock(20GB/s)< 压缩器收纳(117–328MB/s + CPU)< 驱动 teardown(异步尾巴)。相机选 wired 不是因为分配快,而是物理就位承诺(DMA 不能缺页);代价在释放侧(异步尾巴)——结构性豁免的代价是释放不能同步完成。
| 工具 | 用途 |
|---|---|
appscan | per-pid footprint/rss/internal/compressed(TASK_VM_INFO+purgeable 查询) |
alloceff | 各内存类型分配/释放效率微基准(匿名/mlock wire/purgeable,256MB 档) |
| vm_stat + ps 采样器 | 0.5s 间隔全计数器时间线(单次 vm_stat 捕获后多路 grep,zsh 后台循环,sed 提取 pid) |
swapprobe | 轻压工具(场景一前置:逼出文件页回收把 free 抬过 2GB) |
Decompressions 在 Compressions 之前,且 "Compressions" 是 "Decompressions" 的子串——grep 提取必须按行名显式锚定,否则两列 silently 互换com.360buy.jdmobile 启动页需 ~12s 等待实测设备:iPhone 12 Pro(A14/6GB/iOS 14.8 越狱);源码锚点 xnu-7195.141.2。 主书:ios-memory-book.pages.dev(§5.5m/§5.5n/§5.5o)· 相机专题站:ios-camera-memory.pages.dev · 2026-08-29 · v2.0(新增文件页全统计/累计计数器/死会话摘除机制)