iOS 的 WebKit 是 Apple 自研的多进程 Web 引擎(WebKit2 架构):应用进程只持有 WKWebView 这个"遥控器",真正解析 HTML、执行 JS、排版绘制的是独立的 WebContent 进程,网络走 Networking 进程,整套进程由 XNU 内核的 per-task 内存账本(ledger)、jetsam 优先级带、freezer 冻结机制精细治理——内核源码里甚至有对 com.apple.WebKit.WebContent 的按名字特判。
鸿蒙的 ArkWeb 渲染进程则基于 Chromium + CEF 构建(官方明示),是"五进程模型"中的一环:Web 渲染进程负责 HTML 解析/排版/绘制 + JS/WASM 执行,GPU 光栅化与合成送显被拆到独立的 Web GPU 进程并与系统 RenderService 对接;移动端默认多个 Web 实例共享一个渲染进程以省内存,2in1 设备才默认独立进程。鸿蒙侧官方文档明确写着「Web 内核对内存大小的申请无限制约束」,内存治理策略与 iOS 完全不同。
本质区别一句话:两者都把"网页代码"隔离进无特权的独立渲染进程(防沙箱逃逸),但 iOS 走的是自研引擎 + 内核级强内存配额 + 按站点拆进程的路线;鸿蒙走的是 Chromium 血统 + 共享渲染进程省内存 + 独立 GPU 进程对接自研合成器的路线。
WebKit 是 Apple 主导的开源 Web 浏览器引擎(C++ 为主,部分 C/汇编在 JavaScriptCore,Cocoa 集成为 Objective-C)。它是 iOS/macOS 上的系统框架,Safari、邮件、备忘录、图书、新闻、App Store 都用它,第三方 App 内嵌网页也必须通过 WKWebView。
| 组件 | 功能 |
|---|---|
| bmalloc | WebKit 自己的 malloc:bump pointer allocator + IsoHeap(每种类型隔离到独立页面),核心价值是防御 use-after-free 引发的类型混淆攻击。 |
| WTF | Web Template Framework:模板/容器库(Vector、HashMap、HashSet)与智能指针(Ref/RefPtr/WeakPtr),全代码库的基础设施。 |
| JavaScriptCore (JSC) | JS 引擎,四层执行体系:解释器 → Baseline JIT → DFG JIT → FTL JIT(B3 后端),逐层用编译时间换执行速度;JSC 在 iOS 上的 JIT 依赖 XNU 的 mman.h L127 MAP_JIT 机制。 |
| WebCore | WebKit 最大的组件:HTML/XML/CSS 解析器、DOM 树 / Render Tree、HTML/SVG/MathML 元素实现、独有的 CSS JIT(现存唯一的 CSS 即时编译器)。 |
| WebCore/PAL | 平台抽象层,让 WebCore 在 macOS/iOS/Windows/Linux 保持平台无关。 |
| WebKitLegacy (WebKit1) | 单进程时代的旧架构,实现 WebView/UIWebView(iOS 8 起废弃)。 |
| WebKit (WebKit2) | 多进程架构,实现 WKWebView:UI 进程、WebContent 进程、Networking 进程、WebInspector/WebDriver。 |
| 进程 | 职责 | iOS 特点 |
|---|---|---|
| UI 进程 (即宿主 App) |
持有 WKWebView、处理用户输入、页面生命周期、权限弹窗、原生 UI(菜单/弹窗)。 | 与 App 同进程;WebKit 以系统 framework 形态嵌入。 |
| WebContent 进程 com.apple.WebKit.WebContent |
跑网站的代码:HTML 解析、DOM/RenderTree、JS 执行(JSC JIT)、排版、绘制、Media 处理。Safari 每个标签页通常各一个。 | 沙箱最严:无直接网络 socket(出网必须经 Networking 进程)、无任意文件写、IPC 白名单;受 MACF 策略管控(见 §6.6)。 |
| Networking 进程 | 所有网络请求、TLS、HTTP 缓存、Cookie;存储管理(默认/隐私会话隔离)。 | 同一会话的所有 WebContent 共享一个 Networking 进程的网络会话。 |
| WebInspector / WebDriver | 开发者工具与自动化。 | Inspect WebContent 进程。 |
WebKit 文档定义 Site = scheme + eTLD+1(可注册域)。开启站点隔离后,跨站 iframe 不再和主框架同进程:
为什么要拆进程?渲染引擎解析不可信网页,是整个系统最大的攻击面。任何 Blink/WebCore/JSC 的内存破坏漏洞,最坏也只能拿到 WebContent 的无特权上下文——这正是 iOS WebKit 与鸿蒙 ArkWeb 共同的设计出发点。
iOS 上 WebContent 进程把绘制结果编码成 RemoteLayer 树(IOSurface 后端的 backing store),提交给系统的 RenderServer(即 backboardd 的合成职责)做最终合成送显。WebContent 自身不直接与 GPU 驱动做全量光栅化提交——这层经由平台图形栈与 ANGLE(Metal) 完成。
| 进程 | 唯一性 | 职责(华为官方文档原文语义) |
|---|---|---|
| 应用进程 | 应用唯一 | 主进程,含 UI 主线程与 Web 相关线程:网络线程、Video 线程、Audio 线程、IO 线程;负责 Web 组件对外接口与回调、需与其他系统服务交互的功能(网络、媒体服务)。 |
| Foundation 进程 | 系统唯一 | 接收应用进程的孵化请求,管理应用进程与 Web 渲染进程的绑定关系。 |
| Web 孵化进程 | 系统唯一 | 接收 Foundation 请求,孵化 Web 渲染进程与 Web GPU 进程;孵化后做安全沙箱降权、预加载动态库提速。 |
| Web 渲染进程 | 应用可指定多实例共享或独立 | 运行 Web 渲染进程引擎(HTML 解析、排版、绘制、渲染)+ ArkWeb 执行引擎(JavaScript、WebAssembly)。 |
| Web GPU 进程 | 应用唯一 | 光栅化、合成送显,与 GPU、RenderService 交互,提升应用进程稳定性与安全性。 |
| 模式 | 机制 | 限制与适用 |
|---|---|---|
| 异步渲染 (默认) |
Web 组件作为图形 surface 节点,独立送显(自己管合成节拍)。 | 高度 ≤ 7680 物理像素,超出白屏;不支持动态切换。适合纯 Web 为主体的页面,性能功耗更优。 |
| 同步渲染 | Web 组件作为图形 canvas 节点,跟随系统组件一起送显。 | 不支持 DSS(显示子系统)合成;高度最大 500,000 px。适合 Web 与 ArkUI 组件同滚动的超长混合页面。 |
另有同层渲染(same-layer rendering)支撑小程序类宿主场景,让 Web 内容与原生组件处于同一渲染层。
OpenHarmony web_webview 仓库 README 原文:"nweb is the native engine of the OpenHarmony webview component and is built based on Chromium and the Chromium Embedded Framework (CEF)"。华为官方版本对应表:
| HarmonyOS 版本 | ArkWeb 内核(Chromium) |
|---|---|
| 4.0 及之前 | M99 |
| 4.1 – 5.1 | M114 |
| 6.0 | M132(默认)/ M114(可选) |
| 6.1 | M132 |
| 7.0 | M144(默认)/ M132(可选) |
因此 ArkWeb 的"Web 渲染进程"在血统上是 Chromium RenderProcess:Blink 排版引擎 + V8 JS 引擎(ARM64 上同时配 Les Wasm 解释器),经 CEF/nweb 适配层接入鸿蒙图形栈(RenderService)与系统服务。
| 维度 | iOSWebKit WebContent | 鸿蒙ArkWeb Web 渲染进程 |
|---|---|---|
| 引擎血统 | Apple 自研 WebKit:WebCore + JavaScriptCore(四层 JIT) | Chromium/Blink + V8(经 CEF 适配),版本 M99→M144 |
| 进程数策略 | 每标签页通常一个 WebContent;站点隔离下跨站 iframe 再拆进程 | 移动端默认多实例共享一个渲染进程;2in1 默认独立;token 定向复用 |
| 进程内职责 | HTML 解析、DOM/RenderTree、JS(JSC JIT)、排版绘制、媒体 | HTML 解析、排版、绘制、渲染 + JS/WASM 执行引擎(职责声明几乎对等) |
| 网络 | 独立 Networking 进程;WebContent 无 socket 权限 | 网络线程在应用进程(与 Video/Audio/IO 线程并列) |
| GPU/合成 | 图形栈经 ANGLE(Metal);结果以 RemoteLayer/IOSurface 交 RenderServer 合成 | 独立 Web GPU 进程做光栅化与合成送显,对接 RenderService;异步/同步两种送显模式 |
| 进程创建方 | UI 进程请求系统(launchd 系)拉起 WebContent | 应用 → Foundation 进程 → Web 孵化进程(孵化 + 沙箱降权 + 预加载动态库) |
| 内存治理 | XNU per-task phys_footprint ledger + jetsam band + freezer 冻结;内核对 WebContent 有专门统计与冻/解冻特判(§7) | 官方文档:「Web 内核对内存大小的申请无限制约束」;治理交由上层(OOM 时 onRenderExited 回调通知应用) |
| 崩溃/无响应感知 | 进程退出由 WebKit 内部 + 系统服务处理,页面级 reload | 显式 API:onRenderExited(OOM/crash/正常)、onRenderProcessNotResponding/Responding |
| JS JIT 与 W^X | JSC JIT 用 MAP_JIT;XNU 强制 JIT 区 RWX 全权限、W^X 互斥检查(§6.5) | V8 JIT;鸿蒙内核为 Linux 血统,沿用标准 W^X/双映射策略 |
| 可丢弃内存 | MADV_FREE_REUSABLE/REUSE(XNU 定义)支撑 WebKit 内存缓存丢弃 | Chromium 的 discardable memory(ashmem/memfd 系) |
| 宿主组件 | WKWebView(iOS 8+,唯一现代入口;UIWebView 已废弃) | ArkUI Web 组件 + WebviewController(@kit.ArkWeb) |
以下全部锚点基于 xnu-11215.1.10(iOS 18),文件归档于本报告仓库 xnu-render-analysis/xnu-11215.1.10/,行号可直接复核。跨版本数据来自 iOS 12/13/14/16/17/18 六版本同口径对比。
task.c L1178-1196 的注释块给出了权威定义——WebContent 进程的内存上限按这个口径计量:
/* phys_footprint: This is the sum of: * + (internal - alternate_accounting) * + (internal_compressed - alternate_accounting_compressed) * + iokit_mapped * + purgeable_nonvolatile + purgeable_nonvolatile_compressed * + page_table */ 1241 task_ledgers.internal = ledger_entry_add(t, "internal", ...); 1243 task_ledgers.iokit_mapped = ledger_entry_add_with_flags(t, "iokit_mapped", ...); 1249 task_ledgers.page_table = ledger_entry_add_with_flags(t, "page_table", ...); 1251 task_ledgers.phys_footprint = ledger_entry_add(t, "phys_footprint", "physmem", "bytes"); 1253 task_ledgers.internal_compressed = ledger_entry_add(t, ...); 1290 task_ledgers.frozen_to_swap = ledger_entry_add(t, "frozen_to_swap", ...); // CONFIG_FREEZE
要点:匿名脏页 + 压缩器里的份额 + IOKit 映射 + 非易失 purgeable + 页表都算进足迹——WebContent 里庞大的解码图像、IOSurface、JIT 页无一漏网。iOS 18 还加了 media/graphics/neural 分类记账(task.c L1259-1295),为分类限费铺路。
超限回调直接挂在账本上:task.c L1460
1460 ledger_set_callback(t, task_ledgers.phys_footprint, task_footprint_exceeded, NULL, NULL);
7081 PROC_CROSSED_HIGH_WATERMARK__SEND_EXC_RESOURCE_AND_SUSPEND(...) // 先挂起进程(SUSPEND),再发 EXC_RESOURCE 例外; // hwm_user_cores 时还会先落一份 coredump(L7104-7112) 7207 task_footprint_exceeded(int warning, ...) 7240 // CONFIG_DEFERRED_RECLAIM:先同步回收再复核余额, // 余额已回落则直接 return(给进程自救机会) 7284 task_process_crossed_limit_no_diag(...) 7297 PROC_CROSSED_HIGH_WATERMARK__SEND_EXC_RESOURCE_AND_SUSPEND(...); // 真超限 7319 PROC_CROSSED_HIGH_WATERMARK__SEND_EXC_RESOURCE_AND_SUSPEND(...); // 诊断阈值
memlimit 分 active/inactive 两档(App 前后台切换时换限额),fatal 与否由 memorystatus 决定。WebContent 进程超限的最终结局通常是 jetsam 的 per-process-limit 原因被杀。
Jetsam 原因码全表 kern_memorystatus.c L103-117:
jettisoned | highwater | vnode-limit | vm-pageshortage | proc-thrashing
fc-thrashing | per-process-limit | disk-space-shortage | idle-exit
zone-map-exhaustion | vm-compressor-thrashing | vm-compressor-space-shortage
low-swap | sustained-memory-pressure | vm-pageout-starvation
优先级带 kern_memorystatus.c L126-143:FOREGROUND / AUDIO_AND_ACCESSORY / CONDUCTOR / DRIVER_APPLE / HOME / EXECUTIVE / IMPORTANT / CRITICAL(数值定义在 sys/proc.h 的 JETSAM_PRIORITY_*)。kill 主循环 kern_memorystatus.c L3884-3990(memorystatus_thread_internal) 的次序:先清超 highwater 的进程 → 再 swap 全部 app → 然后逐 band 从最不重要往重要杀,每杀一个都回头重查 highwater。
关键限额参数:
// kern_memorystatus_freeze.c (xnu-11215.1.10) 104 unsigned int memorystatus_frozen_count_webcontent = 0; 224 static void memorystatus_demote_frozen_processes(bool urgent_mode); 312 calculate_thaw_percentage(uint64_t frozen_count, uint64_t thaw_count); 366 get_thaw_percentage_webcontent(); // WebContent 专属解冻率
Freezer 把后台进程的用户态匿名页整进程收进压缩器/swap(iOS 18 桌面级 iPad 与 Mac 上 swap 路径完备:vm_compressor_backing_file.c L47/L88/L108/L146/L182,含 FIOPINSWAP ioctl、VSWAP 标记与 ENCRYPTED_SWAP)。冻结名额有限(memorystatus_frozen_processes_max),解冻率决定下一轮还愿不愿意冻它——这正是 §7 里 WebContent 特判的意义。
冻/解冻会计账进 ledger 的 frozen_to_swap 条目 task.c L1290 / L5099-5121。
// bsd/sys/mman.h 127 #define MAP_JIT 0x0800 // Allocate a region that will be used for JIT purposes 212 #define MADV_FREE_REUSABLE 7 // pages can be reused (by anyone) 213 #define MADV_FREE_REUSE 8 // caller wants to reuse those pages
// osfmk/vm/vm_map.c — JIT 区域的三重约束 626/2882 printf("CODE SIGNING: ... curprot cannot be write+execute..."); // W^X 互斥 2895 if (entry_for_jit && cur_protection != VM_PROT_ALL) { // 非 macOS 平台:MAP_JIT 区必须是全 RWX,否则拒绝 2930 printf("CODE SIGNING: ... JIT requires RWX: failing."); return KERN_PROTECTION_FAILURE; } 2945 if (map->map_disallow_new_exec == TRUE) { ... } // 可执行锁死:封掉新增可执行映射
解读:iOS 上 W 与 X 永久互斥(L626/L2882);带 com.apple.security.cs.allow-jit 身份的进程才能用 MAP_JIT,且该区域权限必须是完整的 RWX(L2895-2930,"JIT requires RWX");进程还能自愿锁死后续可执行映射(L2945)。WebContent 里 JSC 的 Baseline/DFG/FTL JIT 代码页就活在这个 MAP_JIT 区里,受 pg_tlb/code signing 监控。对照:鸿蒙的 V8 JIT 走 Linux 血统内核的标准 W^X 双映射,无 MAP_JIT 概念。
MADV_FREE_REUSABLE/REUSE 则是 WebKit 内存缓存"可丢弃"语义的内核支撑——内存吃紧时这些页可被任何人复用,不计入进程压力。
XNU 的强制访问控制框架(TrustedBSD MAC)在内核埋了几百个策略点,sandbox/ContainerManager 等策略模块(闭源)挂上来:
// security/mac_process.c (xnu-11215.1.10) 209 mac_cred_label_associate_fork(kauth_cred_t cred, proc_t proc); 323 mac_proc_check_debug(...); // 谁能调试谁 // 加上 mac_cred_label_associate_kernel/user、mac_proc_check_visible 等全套钩子
WebContent 进程"无网络 socket、无任意文件写、IPC 白名单"的沙箱画像,就是用户态沙箱 profile 编译成 MACF 规则后在全部内核策略点上过滤的结果。
// bsd/kern/kern_memorystatus_notify.c (xnu-11215.1.10) 150 vm_pressure_level_t memorystatus_vm_pressure_level = kVMPressureNormal; 129 static struct knote *vm_pressure_select_optimal_candidate_to_notify(...); 228 if (level == kVMPressureWarning || kVMPressureUrgent) { notify... } 232 else if (level == kVMPressureCritical) { ... } 443 uint64_t memorystatus_kill_on_sustained_pressure_window_s = 60*10; // 10 分钟窗口 453 static void sustained_pressure_handler(void*, void*); // iOS16+ 引入
内核按 Normal→Warning→Urgent→Critical 分级,经 knote 通知到"最值得通知的进程"(前台优先)。WebContent 收到 pressure 事件后清 JS GC、丢内存缓存、释放解码位图——这是它超限前的自救窗口。持续高压 10 分钟仍不缓解,iOS 16+ 会直接触发 sustained-memory-pressure 击杀(六版本实测:iOS 12/13/14 无此逻辑,16 起出现)。
iOS 16(xnu-8792.61.2)起,freezer 代码里出现了对渲染进程的按进程名硬编码统计——内核为 WebContent 单独建了一本"冻结台账":
// kern_memorystatus_freeze.c (xnu-11215.1.10) // 冻结时: 628 if (strcmp(p->p_name, "com.apple.WebKit.WebContent") == 0) { 629 memorystatus_frozen_count_webcontent++; 630 os_atomic_inc(&memorystatus_freezer_stats.mfs_processes_frozen_webcontent, ...); } // 同样逻辑在 memorystatus_freeze_process() 内再出现一次:L1903-1905 // kern_memorystatus.c (xnu-11215.1.10) // 进程移除时核销: 2715 if (strcmp(p->p_name, "com.apple.WebKit.WebContent") == 0) { 2716 assert(memorystatus_frozen_count_webcontent > 0); 2717 memorystatus_frozen_count_webcontent--; } // memorystatus_on_resume() 里解冻记账: 3217 if (strcmp(p->p_name, "com.apple.WebKit.WebContent") == 0) { 3218 os_atomic_inc(&memorystatus_freezer_stats.mfs_processes_thawed_webcontent, ...); }
并用专属 sysctl 暴露 WebContent 解冻率:kern_memorystatus_freeze.c L366-380(get_thaw_percentage_webcontent / kern.memorystatus_freezer_thaw_percentage_webcontent)。
为什么值得单列?这是"内核反过来给某个用户态引擎开小灶"的直接证据:iOS 设备上后台标签页的 WebContent 进程既多又大,Freezer 需要单独评估"冻 WebContent 划不划算"(解冻率太高说明反复冻/解冻浪费带宽),进而影响冻结名额分配策略。鸿蒙侧没有任何等价的内核特判——ArkWeb 的内存治理完全在框架层(且官方明示内核不设申请上限),这是两平台最深刻的设计哲学差异之一。
| iOS | xnu tag | 与 Web 渲染进程相关的变化 |
|---|---|---|
| 12 | xnu-4903.270.47 | freezer 代码还内联在 kern_memorystatus.c(freeze 文件不存在);对 WebContent 无任何特判(仅 L2942 注释提到 WebContent 用 inactive limit) |
| 13 | xnu-6153.141.1 | freeze 拆分成独立文件 |
| 14.8 | xnu-7195.141.2 | 仍然无 WebContent 特判(L2626 注释同 iOS12) |
| 16 | xnu-8792.61.2 | 转折点:① freezer 出现 com.apple.WebKit.WebContent 硬编码统计(冻结台账);② 新增 sustained-memory-pressure 击杀(10 分钟持续高压);③ swap 相关引用大增(48→55→101,对应桌面级 swap 能力) |
| 17 | xnu-10063.141.1 | 延续 iOS16 机制(WebContent 特判 20 处、sustained kill 3 处) |
| 18 | xnu-11215.1.10 | 延续并微调;ledger 新增 media/graphics/neural 分类记账与 frozen_to_swap 条目;CONFIG_DEFERRED_RECLAIM 给超限进程同步回收自救的机会 |
鸿蒙侧并行演化:HarmonyOS 4.0(Chromium M99)→ 4.1-5.1(M114)→ 6.0(M132 默认)→ 7.0(M144 默认)。同一时期把渲染进程模式 API 化(setRenderProcessMode / sharedRenderProcessToken / onRenderProcessNotResponding 均为 API 12 引入),并新增 M114→M132 差异适配指南。
给开发者的实用结论:在 iOS 上做混合开发,WKWebView 的崩溃与内存问题要盯 WebContent 进程的 phys_footprint(可在 Xcode 的 Memory Report 里看分进程曲线),系统会在超限时杀进程救全局;在鸿蒙上则要主动用 setRenderProcessMode(MULTIPLE) 换隔离性、用 onRenderExited/onRenderProcessNotResponding 兜住共享进程的连坐风险——两个平台对"渲染进程"的治理哲学完全不同,移植方案不能照抄。
| 来源 | 用途 |
|---|---|
| apple-oss-distributions/xnu @ xnu-11215.1.10(GitHub) | osfmk/kern/task.c、osfmk/vm/vm_map.c、bsd/kern/kern_memorystatus(_freeze/_notify).c、bsd/vm/vm_compressor_backing_file.c、security/mac_process.c、bsd/sys/mman.h 全文抓取归档 |
| xnu 六版本存档(iOS 12/13/14/16/17/18) | 跨版本对比:WebContent 特判出现于 iOS16;sustained-pressure kill 出现于 iOS16;swap 引用 48→50→55→101→102→102 |
| WebKit 官方文档(docs.webkit.ac.cn 中文镜像:WebKit 简介 / Site Isolation) | 组件构成、JSC 四层 JIT、WebKit2 多进程、站点隔离机制(RemoteFrame/BrowsingContextGroup/FrameProcess/WebProcessCache) |
| Apple Developer Documentation — WKWebView / WKContentView | WKWebView 定位、UIWebView 废弃关系、导航与 UI 代理 |
| 华为开发者文档 — ArkWeb进程 / Web组件渲染模式 / ArkWeb简介 / 使用离线Web组件 / ArkWeb术语 | ArkWeb 五进程模型、渲染进程模式 API、渲染布局两模式、Chromium 版本对应表、离线组件内存数据 |
| OpenHarmony web_webview 仓库 README | "nweb 基于 Chromium + CEF 构建"的官方表述 |
本页所有 XNU 行号引用格式为 文件 L行号,对应归档文件 xnu-render-analysis/xnu-11215.1.10/(task.c = osfmk/kern/task.c 10396 行、vm_map.c = osfmk/vm/vm_map.c 24141 行 等),可逐条 grep 复核。文档类论断以各官方文档原文为准。
场景:淘宝里连续点进多个商品详情页,旧页面早已不可见——iOS 会管理它们吗?答案是会,但不是一个框架,而是四层各管一段;而且 iOS 的"管理"是"停止渲染 + 冻结执行 + 按需清缓存 + 超限杀进程",从不主动替 App 销毁页面对象。
| 层 | 框架/组件 | 对不可见页面做什么 |
|---|---|---|
| ① 视图层 | UIKit(UINavigationController)+ Core Animation/RenderServer | 导航栈持有所有 VC(不销毁);不可见 view 被移出 window,RenderServer 不再合成(不耗 GPU);但 iOS 6 起不再自动卸载 view,内存警告只发通知,释放与否由 App 决定 |
| ② 引擎层 | WebKit(WebContent 进程) | H5 页面的挂起与回收:BackForwardCache(容量仅 0–2 页)、activity state 冻结渲染、MemoryPressureHandler 清缓存、缓存类进程遇压直接退出(本节源码锚点) |
| ③ 进程层 | RunningBoard / launchd | App 退后台时挂起全部 WebContent 进程(task_suspend) |
| ④ 内核层 | XNU | pressure 通知(⑦的 notify 链)、ledger 限额、jetsam、freezer——最终裁判 |
如果详情页是原生 VC:UINavigationController 把整个栈都留在内存里,UIKit 只负责可见性——push 完成后旧 view 移出 window(view.window == nil),RenderServer 的合成树里没有它,GPU 零开销;CPU 侧也不再给它布局/绘制回调。但内存不会自动回收:iOS 6 起 viewDidUnload 废弃,系统不再卸载视图;内存紧张时 UIKit 只投递 didReceiveMemoryWarning,旧页面释放多少完全取决于 App 自己。
旧页面离开时若可缓存,整个 Page 被挂起后放进 BackForwardCache(旧称 PageCache)——JS 停、定时器停、渲染停,但 DOM 树和 JS 堆留在 WebContent 进程内存里,回退命中即瞬时恢复(触发 pageshow persisted)。
// WebCore/history/BackForwardCache.cpp(WebKit main,本仓 webkit-src/ 归档) 590 std::unique_ptr<CachedPage> BackForwardCache::suspendPage(Page& page); 726 void BackForwardCache::prune(PruningReason) { while (pageCount() > maxSize()) { ... 逐出最旧 ... } } // 逐出原因只有三种:MemoryPressure / ProcessSuspended / ReachedMaxSize(L462-470)
容量小得惊人(按页面数计,WebKit/Shared/CacheModel.cpp 的 calculateMemoryCacheSizes):
| CacheModel | RAM ≥512MB | RAM ≥256MB | 更小 |
|---|---|---|---|
| DocumentViewer(纯展示型) | 0 页 | 0 | 0 |
| DocumentBrowser | 2 页 | 1 | 0 |
| PrimaryWebBrowser(Safari) | 2 页 | 1 | 0 |
也就是说:连点 5 个商品页,只有最近 1–2 页能留在缓存里,更早的已被销毁,回退时只能重新加载。此外缓存还有过期时间(backForwardCacheExpirationInterval)。
不可见 webview 的 view 不在 window 里 → WebKit 的 activity state 丢失 IsVisible → 渲染更新冻结、requestAnimationFrame 停止、DOM 定时器节流。WebKit 对"整进程全是不可见页面"还有专门策略——把普通警告当致命级处理:
// WebKit/WebProcess/WebProcess.cpp L509-511(原文注释) // If this process contains only non-visible content (e.g. only contains // background tabs), then treat the memory warning as if it was a critical // warning to maximize the amount of memory released for foreground apps. if (m_pagesInWindows.isEmpty() && critical == Critical::No) critical = Critical::Yes;
而预热的、被缓存的(CachedWebContent/PrewarmedWebContent/Suspended)进程遇到内存压力直接退出自己,不尝试省内存(WebProcess.cpp L519-529)。
// WTF/wtf/cocoa/MemoryPressureHandlerCocoa.mm L84-117(WebContent 进程内) 87 memoryPressureEventSource() = dispatch_source_create( DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0, NORMAL | WARN | CRITICAL | PROC_LIMIT_WARN | PROC_LIMIT_CRITICAL, queue); // XNU 内核压力/进程限额事件 → dispatch → respondToMemoryPressure() // WebCore/page/MemoryRelease.cpp L122-128(关键释放路径) 126 BackForwardCache::singleton().pruneToSizeNow(0, pruningReason); // ↳ 内存压力 或 进程即将挂起 时:缓存清零,旧详情页全部销毁 130 MemoryCache::singleton().pruneLiveResourcesToSize(0, /*销毁全部解码图像*/); // 另有 CSSValuePool drain、布局缓存清理、JSC GC、bmalloc scavenger 归还物理页
若释放后仍超 per-process phys_footprint 限额:MemoryPressureHandler 的 memoryKillCallback 上报 UI 进程(WebProcess.cpp L545-552),进程被杀,App 收到 webViewWebContentProcessDidTerminate: 回调后重载页面——这正是用户偶见"网页白屏自动刷新"的完整因果链。App 退后台后则由 RunningBoard 挂起、XNU freezer 冻结(见 §6.4/⑦)。
一句话回答:设计目标是同一道题(把"可再生的缓存数据"标记成系统可回收,避免 OOM 时才被动释放),但实现深度完全不同——XNU 的 purgeable 是内核一等公民(状态机 + token 成熟时钟 + ledger 足迹豁免),Chromium 在鸿蒙(Linux 内核)上的 discardable 是用户态管理器 + madvise 家族,内核自始至终不知道哪些页"可丢弃"。最有说服力的证据藏在 Chromium 自己的源码里:它的苹果平台后端直接调用 MADV_FREE_REUSABLE——那正是 XNU purgeable 的用户态入口。
Mach VM 一等原语:以 VM_FLAGS_PURGABLE 建对象(或 mmap 后 madvise(MADV_FREE_REUSABLE)),对象带内核维护的状态机 NONVOLATILE / VOLATILE / EMPTY / DISCARDING。
base::DiscardableMemory 跨平台抽象,只有两个状态:locked / unlocked(API 头文件原话)。鸿蒙是 Linux 内核,走 DiscardableSharedMemory 后端:跨进程共享内存段 + 段头 CAS 锁位。
Chromium base/memory/discardable_shared_memory.cc(本仓归档 purgeable-report/src/chromium_dsm.cc)的 Purge() 按平台选择 madvise 参数——同一个抽象,两种哲学:
// Linux / ChromeOS / Android(含鸿蒙这一 Linux 血统分支): // "provide MADV_REMOVE which is preferred as it has a behavior // that can be verified in tests" #define MADV_PURGE_ARGUMENT MADV_REMOVE // 立即打洞释放 // Apple 平台: // "MADV_FREE_REUSABLE is similar to MADV_FREE, but also marks the pages // with the reusable bit, which allows both Activity Monitor and // memory-infra to correctly track the pages" #define MADV_PURGE_ARGUMENT MADV_FREE_REUSABLE // = XNU purgeable 入口!
Lock() 的苹果分支注释更直白(chromium_dsm.cc L277-296):
// On macOS, there is no mechanism to lock pages. However, we do need to call // madvise(MADV_FREE_REUSE) in order to correctly update accounting for // memory footprint via task_info(). // ... the corresponding call to MADV_FREE_REUSABLE is in Purge(), since // that's where the memory is actually released, rather than Unlock(), // which is a no-op on macOS.
解读:在苹果平台上 Chromium 的"discardable"整个儿降级为 XNU purgeable 的包装——Unlock 是空操作,丢弃决策交还内核状态机;在 Linux 系(鸿蒙)上则是用户态管理器主动 MADV_REMOVE 立即释放,内核只看到一次普通的打洞操作。"discardable" 是个跨平台抽象:落到苹果内核就是 purgeable,落到 Linux 内核就是 madvise。
| 维度 | iOSXNU purgeable | 鸿蒙Chromium discardable |
|---|---|---|
| 抽象层 | 内核 VM 一等原语(vm_purgable_control / VM_FLAGS_PURGABLE;用户态经 MADV_FREE_REUSABLE 桥接) | 用户态 C++ 抽象(base::DiscardableMemory)+ 平台后端(鸿蒙 = 共享内存段 + madvise) |
| 状态模型 | 内核状态机:NONVOLATILE / VOLATILE / EMPTY / DISCARDING,带 8 个 volatile 组 + FIFO/LIFO/obsolete 语义 | 两态 locked/unlocked:段头 64 位 CAS(1 位锁 + 时间戳),跨进程可见 |
| 谁决定丢弃 | 内核:vm_pageout_scan 每轮先问 ripe token("Before anything...",vm_pageout.c L2999-3019 / iOS18 调用点 L3171);压力等级强制清组(warning=2/urgent=5/critical=8);jetsam 杀进程前先清场可免杀 | 用户态管理器:按 last_known_usage 时间戳 LRU + 内存预算超限即 Purge;分配失败时回调 on_no_memory 释放空闲段后重试,再失败才 TerminateBecauseOutOfMemory |
| 丢弃时机哲学 | 懒:volatile 页一直驻留(真机实测:ImageIO 解码缓存 VOLATILE 状态下 draw#3 仅 2.3ms,数据还在物理页),直到系统真正需要页才零成本收割 | 急(Linux 分支):MADV_REMOVE 当场打洞,页立刻还给内核;苹果分支才是懒的(MADV_FREE_REUSABLE) |
| 公平性/成熟度 | token"邮票成熟系统":volatilize 时刻发行 token,面值 = 当时 inactive 队列流量;翻完一遍才 ripe——最久未碰牌的最先丢;受害者选择还结合 jetsam 优先级(vm_purgeable.c iOS18 L726) | 时间戳 LRU:越久未 Lock 的段越先被 Purge;无内核侧"成熟度"概念 |
| 内存记账 | 标 VOLATILE 瞬间移出 phys_footprint(ledger 记 purgeable_volatile 科目,task.c 注释:footprint 只含 purgeable_nonvolatile)——jetsam 不算它;MADV_FREE_REUSE 取回时再记回 | 内核无对应足迹概念;段的用量由浏览器侧管理器自行记账(tracing MemoryAllocatorDump);鸿蒙内核本身对 Web 内存申请无限制 |
| 丢弃后重建 | 下次访问零页 + 状态查询(VM_PURGABLE_EMPTY);libmalloc 的 malloc_make_nonpurgeable 回读旧状态即知数据已丢 | 下次 Lock() 返回 PURGED/FAILED(zero-fill),对象应销毁重建 |
| 典型使用者 | WebKit RemoteLayerBackingStore(4 个触发点:退后台快照后 / 进程挂起前 / layer 不可达 / 前台闲置 200ms–1s);ImageIO 解码缓存(VM_MEMORY_IMAGEIO / malloc purgeable zone,tag=3);CoreGraphics、QuartzCore/SkyLight、Metal、CoreML、JSC(全系统普查见 CENSUS) | 图片解码缓存、Skia 位图缓存、cc tile 光栅缓存、GPU discardable 纹理(gpu::DiscardableManager),跨渲染进程统一走浏览器侧 allocator |
// vm_purgeable.c(iOS 18 = xnu-11215.1.10,本仓 purgeable-report/src/vm_purgeable_ios18.c) 62 struct token { token_cnt_t count; ... }; // "成熟度"邮票 77 int available_for_purge = 0; // ripe token 计数:可零成本回收的对象数 158 vm_purgeable_token_add(queue); // volatilize 时发行 token,面值=当时 inactive 页流量 877 vm_purgeable_object_purge_one(force_purge_below_group, ...); // 只碰"成熟"对象;force 时清 group < 阈值(压力等级→2/5/8) 1483 vm_purgeable_accounting(); // VOLATILE 瞬间 ledger 换边:移出 phys_footprint // 回收入口(vm_pageout.c iOS 14.8 = xnu-7195.141.2) 2999 /* Before anything, we check if we have any ripe volatile objects around. */ 3019 retval = vps_purge_object(); // 页回收扫描的第一顺位,先于文件页/压缩 4822 force_purge: warning=2 / urgent=5 / critical=8 // 压力等级旁路成熟度
这条机制链的含义:purgeable 丢弃是 iOS 内存回收的"第一顺位"——零成本(不用压缩、不用写盘、不用杀人),排在文件页回收和压缩器前面。而这一切对被标 volatile 的进程是"免费"的:它的 phys_footprint 同瞬间下降,jetsam 高水位考核也随之宽松。
ImageIO 解码 4000×3000 JPEG(解码缓冲 45.8 MiB),mach_vm_region_recurse + vm_purgable_control(GET_STATE) 探测:
draw#1 (解码) : 100.0 ms 0x114230000 46880 KiB state=EMPTY tag=70 (VM_MEMORY_IMAGEIO) // 解码 scratch:用完即 EMPTY 0xbda800000 46912 KiB state=EMPTY tag=3 (MALLOC purgeable) // 解码缓存刚建 draw#2 (缓存命中): 51.5 ms 0xbda800000 46912 KiB state=VOLATILE tag=3 // ★ 缓存常驻 VOLATILE draw#3 (释放重建): 2.3 ms // volatile ≠ 已回收!页还在,命中零成本
这正是 purgeable 哲学的实证:缓存主动标 VOLATILE 让出记账,但物理页仍在;内存充裕时命中几乎免费(2.3ms),内存紧张时内核第一批收割它们。对照 Chromium 在 Linux 上的 MADV_REMOVE 是"立即还给内核",两种时机模型截然相反——但目标函数一致:别等 OOM 才动手。
以下全部结论来自 Chromium 114.0.5735.0(= HarmonyOS 4.1–5.1 的 ArkWeb 内核版本)源码逐文件验证,归档于本仓 xnu-render-analysis/chromium-discardable/consumers/。注:Chromium main 已大幅重构(cc/raster 的解码缓存已迁走),抓 main 会 404,故以 M114 为准——这正是 ArkWeb 实际跑的代码。
| 场景 | 组件 | 所在进程 | 缓存的是什么 |
|---|---|---|---|
| ① GPU 图片解码缓存 | cc/tiles/gpu_image_decode_cache.cc | 渲染进程 | 解码后的 RGBA 位图 / YUV 多平面像素数据(上传纹理前的中转) |
| ② GPU 丢弃纹理 | gpu/command_buffer/service/service_discardable_manager + client 侧 | GPU 进程 + 渲染进程 | 已上传的 GL 纹理(unlock 即可被删除回收) |
| ③ Skia 丢弃内存桥 | skia/ext/SkDiscardableMemory_chrome.cc | 渲染进程 | Skia lazy image(未解码图片对象)的像素缓存 |
| ④ 跨进程段管理 | components/discardable_memory/{service,client,common} | 浏览器进程宿主 + 渲染进程 client | 基础设施:统一段分配、LRU、前后台策略、段内 free-list 堆 |
| ⑤ 后端选择试验 | base/memory/discardable_memory_internal.h | 所有 | 三种后端(共享内存段 / MADV_FREE / ashmem)的 Finch 试验 |
// cc/tiles/gpu_image_decode_cache.cc (M114) 594 // Task which decodes an image and stores the result in discardable memory. // This task does not use GPU resources and can be run on any thread. class GpuImageDecodeTaskImpl : public TileTask { ... }; // RGBA 与 YUV 两种解码结果都包成 DecodedAuxImageData: 761 DecodedAuxImageData(const SkPixmap& rgba_pixmap, std::unique_ptr<base::DiscardableMemory> in_data); 771 DecodedAuxImageData(const SkYUVAPixmaps& yuva_pixmaps, std::unique_ptr<base::DiscardableMemory> in_data); // 分配点(真正的调用处): 2375 std::unique_ptr<base::DiscardableMemory> backing_memory; 2380 auto* allocator = base::DiscardableMemoryAllocator::GetInstance(); 2382 backing_memory = 2383 allocator->AllocateLockedDiscardableMemoryWithRetryOrDie( 2384 info.size, /*on_no_memory=*/ClearCache 回调);
三个值得注意的细节:
图片上传到 GPU 后变成 GL 纹理,继续由 discardable 体系管理(GPU 进程侧):
// gpu/command_buffer/service/service_discardable_manager.h (M114) // "A helper class used to manage discardable textures... // Used by the GLES2 Implementation."(client 侧头注释) void InsertLockedTexture(uint32_t texture_id, size_t texture_size, ...); bool UnlockTexture(...); // unlock 后纹理可被删除回收 bool LockTexture(...); // 下次绘制前锁定,失败=已被回收需重建 void HandleMemoryPressure(MemoryPressureLevel); // 压力感知 void EnforceCacheSizeLimit(size_t limit); size_t DiscardableCacheSizeLimitForPressure(base_cache_limit, level); // ↑ 缓存上限随压力等级缩水(不是只在压力事件时一次性清空) // GpuDiscardableEntry { handle, unlocked_texture_ref, size } + base::LRUCache
渲染进程侧还有对应客户端 gpu/command_buffer/client/client_discardable_manager / client_discardable_texture_manager( GLES2 实现使用),通过 handle 与 GPU 进程同步锁状态。
// skia/ext/SkDiscardableMemory_chrome.cc SkDiscardableMemory* SkDiscardableMemory::Create(size_t bytes) { auto discardable = DiscardableMemoryAllocator::GetInstance() ->AllocateLockedDiscardableMemoryWithRetryOrDie( bytes, DoNothing()); return new SkDiscardableMemoryChrome( std::move(discardable)); }
Skia 引擎自己的 lazy-image 像素缓存协议(lock/unlock/data)整个转发给 Chromium 的 allocator——引擎级的丢弃内存全部汇入同一管道。
// base/memory/discardable_memory_internal.h (M114) enum DiscardableMemoryTrialGroup : int { kEmulatedSharedMemory = 0, // 共享内存段+CAS锁(默认) kMadvFree, // MADV_FREE 单进程后端 kAshmem, // 仅 Android:ashmem pin/unpin };
Android 的 ashmem 路径(归档 chromium_dsm.cc L524-546):Lock = AshmemPinRegion,返回 ASHMEM_WAS_PURGED 即知已被内核回收;鸿蒙无 ashmem 设备,AshmemDeviceIsSupported() 为假,自动落到共享内存段路径。
cc/tiles/software_image_decode_cache.cc(M114 全文 grep 无 DiscardableMemory 引用)——软件光栅的解码位图用普通内存 + LRU 预算:
// cc/tiles/image_decode_cache_utils.cc (M114) decoded_image_working_set_budget_bytes = 128MB; // 默认 ≥4GB RAM → 256MB;低端机 → 32MB ShouldEvictCaches(): // 只有 CRITICAL 压力才全清 MODERATE → return false; CRITICAL → return true;
为什么?软件路径位图被 CPU 高频直接采样,锁语义与重建成本不划算;GPU 路径"解码→上传→闲置"的画像才配得上 discardable。Chromium 的取舍本身就是对"什么该用 discardable"的最佳注释。
Web 渲染进程 宿主进程(浏览器进程) ┌────────────────────────────┐ ┌─────────────────────────────┐ │ cc::GpuImageDecodeCache │ mojo IPC │ DiscardableSharedMemory │ │ ├ 解码任务(输出=discardable)│ ───────────► │ Manager (Get()) │ │ └ on_no_memory=ClearCache │ 请求段/时间戳 │ ├ 段集合 LRU(按now排序) │ │ Skia SkDiscardableMemory ──┤ │ ├ OnMemoryPressure │ │ gpu client: │ │ └ ReduceMemoryUsageUntil... │ │ ClientDiscardableTexture │ └─────────────────────────────┘ │ ClientDiscardableShared- │ │ MemoryManager │ Web GPU 进程 │ ├ 段内 free-list 堆 │ ┌─────────────────────────────┐ │ └ OnBackgrounded 收缩 │ │ ServiceDiscardableManager │ └────────────────────────────┘ │ ├ GL 纹理 LRU │ │ ├ 压力降容(CacheSizeForPressure)│ 后端(Backing Trial 三选一): │ └ EnforceCacheSizeLimit │ 共享内存段 | MADV_FREE | ashmem(仅安卓) └─────────────────────────────┘
对应到 ArkWeb:以上全部组件(①–⑤)就是鸿蒙 Web 渲染进程 / Web GPU 进程 / 宿主进程里的实际代码。鸿蒙差异点:无 ashmem(回落共享内存段后端);Purge 时走 Linux 分支 MADV_REMOVE(§12 已证);段本身的创建走 OHOS 的 POSIX 共享内存。