结论速览
| 问题 | OpenHarmony (ArkUI) | iOS (UIKit + XNU) |
|---|---|---|
| 页面间可见/不可见关系? | 有,且是框架一等公民:页面节点挂 Stage 节点下,栈顶"激活+VISIBLE",被覆盖页"失活+INVISIBLE",状态由 PagePattern 显式管理 | 有,但是通知语义:被覆盖 VC 收 viewWillDisappear/viewDidDisappear(头文件注释原词 "covered"),无公开 isAppeared 属性 |
| 框架如何区分? | 三级状态机:生命周期回调(onPageHide/onPageShow)+ 节点可见性(VisibleType::INVISIBLE)+ 节点激活(SetActive(false)/SetJSViewActive(false),递归整棵子树)+ 转场期节点冻结(SetNodeFreeze) | appearance 回调状态机(beginAppearanceTransition/endAppearanceTransition 驱动),容器强持有 VC 数组;view 懒加载(loadView)是唯一按需分配点 |
| 不可见页内存优化? | 有,多条路径:① 页面图片回收/重建(MemoryManager,ImagePattern::RecycleImageData,每页≤20 张);② NavDestination 隐藏→500ms 延迟回收图片、显示→重建;③ 应用退后台 500ms 后回收所有不可见页图片;④ 系统层 memmgr 分级回收/zswapd/purgeable | 框架层零回收:viewDidUnload 于 iOS 6 废除(头文件注释明证"no longer clear the view by default");优化全在内核:压缩器、purgeable token、freezer、后台 jetsam band |
一句话对比:ArkUI 把"不可见页面"当成一等管理对象,在框架层主动回收可重建资源(图片像素为主);UIKit 只通知不管回收,把内存弹性完全交给 XNU 内核(进程级)与 App 自己。
第一部分 OpenHarmony:ArkUI 框架层
1. 页面栈与"谁持有谁"——不可见页面的生命周期根源
1.1 两套页面栈(router 与 Navigation)
ArkUI(Stage 模型)里嵌套打开商品详情页有两条路,都由框架统一落到同一棵渲染树上:
router 栈(frameworks/bridge/declarative_frontend/ng/page_router_manager.cpp):
- 栈本体:
std::list<WeakPtr<FrameNode>> pageRouterStack_(page_router_manager.h:431)——注意这里存弱引用,页面的真正生命周期由渲染树决定。 - push 入口
PageRouterManager::LoadPage(page_router_manager.cpp:1597):
``cpp auto pageNode = CreatePage(pageId, target); pageRouterStack_.emplace_back(pageNode); ... OnPageReady(pageNode, needHideLast, needTransition) ... ``
OnPageReady最终调StageManager::PushPage(page_router_manager.cpp:1631附近)。
真正的强持有者是 Stage 节点的子节点列表:StageManager::PushPage 把新页面 node->MountToParent(stageNode_)(stage_manager.cpp:219)。Stage 节点是 window 根节点的子节点,其 children_(RefPtr 强引用)按 push 顺序持有全部 N 个页面节点——被覆盖的页面不会被摘除,整棵组件树(FrameNode 树 + 每个 Image/Text 的布局与渲染属性对象)持续存活。组件注册表 ElementRegister 的 itemMap_ 只存 WeakPtr<AceType>(element_register.cpp:138),不构成额外强持有。
[SRC] 证据链:
stage_manager.cpp:186-230(PushPage 全程)、page_router_manager.cpp:1597-1621(LoadPage)、element_register.cpp:138(WeakPtr 语义)。
Navigation 栈(HarmonyOS NEXT 主推):NavDestination 页面由 NavigationStack(pattern/navigation/navigation_stack.h:67)管理,同样是节点树持有,但 NavDestination 有独立的显示/隐藏生命周期(见 §3.2),且配合 LazyForEach 惰性加载。
1.2 "可见/不可见"是框架显式管理的状态
push 时框架对旧栈顶做的动作(StageManager::PushPage):
// stage_manager.cpp:206-214
if (!children.empty() && needHideLast) {
hidePageNode = srcPageNode_.Upgrade();
outPageNode = AceType::DynamicCast<FrameNode>(hidePageNode);
FireAutoSave(outPageNode, node);
if (!isNewLifecycle) {
FirePageHide(hidePageNode, needTransition ? PageTransitionType::EXIT_PUSH : PageTransitionType::NONE);
}
}随后(新旧生命周期时序不同,API 12+ 在新页面 aboutToAppear 之后):
// stage_manager.cpp:227-232
if (hidePageNode && needHideLast && isNewLifecycle) {
FirePageHide(hidePageNode, ...);
}
...
FirePageShow(node, needTransition ? PageTransitionType::ENTER_PUSH : PageTransitionType::NONE);FirePageHide → PagePattern::OnHide()(stage_manager.cpp:508-525):
pagePattern->FocusViewHide():焦点视图摘除pagePattern->OnHide():触发onPageHide生命周期回调链(page_pattern.cpp:426-470),含SetJSViewActive(false)、SetAccessibilityVisible(false)、FireOnHiddenChange(false)、UIObserver 路由状态通知(RouterPageState::ON_PAGE_HIDE)- 无转场时立即
ProcessHideState();有转场则在转场动画结束后由FinishOutPage调用(page_pattern.cpp:1027)
PagePattern::ProcessHideState(page_pattern.cpp:230-241)是"不可见"状态的落地:
host->SetActive(false); // 节点失活(递归)
host->NotifyVisibleChange(VisibleType::VISIBLE, VisibleType::INVISIBLE); // 通知子树
host->GetLayoutProperty()->UpdateVisibility(VisibleType::INVISIBLE); // 可见性属性
parent->MarkNeedSyncRenderTree(); parent->RebuildRenderContextTree(); // 渲染树同步与之对称的 ProcessShowState(page_pattern.cpp:243-269)在页面回到栈顶时 SetActive(true) + UpdateVisibility(VISIBLE),并按需重新布局(安全区/键盘变化时 MarkDirtyNode)。
结论①:每个页面之间存在明确的可见/不可见关系,由框架状态机维护,四层状态同步翻转:
- 生命周期层:onPageShow / onPageHide(回调 App)
- 可见性属性层:
UpdateVisibility(INVISIBLE)(布局属性)- 节点激活层:
SetActive(false)递归整棵子树(ui_node.cpp:2194-2199)+SetJSViewActive(false)(JS 侧状态管理同步,page_pattern.cpp:440)- 转场冻结层:
SetNodeFreeze(true)(stage_manager.cpp:106),转场期间冻结源页面,结束后SetNodeFreeze(false)(page_pattern.cpp:995)被覆盖页面不出树、不销毁,仅状态翻转;只有 pop 时才
stageNode_->RemoveChild(pageNode)(stage_manager.cpp:361,424,436)真正销毁。
2. 框架如何"区分处理"可见与不可见页面
2.1 渲染管线:不可见页不再参与布局/绘制
UpdateVisibility(INVISIBLE) + SetActive(false) 的直接效果:
- 渲染树同步时不可见页子树被跳过(
RebuildRenderContextTree后不在渲染树中); FrameNode::IsVisibleAndActive()(frame_node.cpp:1519)为 false,图片等组件的可见区域回调、帧回调不再触发;- 图片组件自带兜底:
ImagePattern::OnVisibleAreaChange不可见时只关闭弹层(image_pattern.cpp:1825),不做重活。
2.2 生命周期派发的精确时序(新旧两套)
ArkUI 对 API 12+ 使用"新生命周期时序":push 时先挂树并 Build 新页面(触发新页 onAboutToAppear)→ 再 FirePageHide 旧页(stage_manager.cpp:204-229,isNewLifecycle 分支);旧时序则先 hide 旧页。这保证了新页面创建失败时旧页面状态不被破坏。
pop 时(StageManager::PopPage,stage_manager.cpp:325-367):
pipeline->GetMemoryManager()->RebuildImageByPage(inPageNode); // L344: 回归页先重建图片!
FireAutoSave(outPageNode, inPageNode);
FirePageHide(pageNode, ...); // 栈顶页 onPageHide
FirePageShow(inPageNode, ...); // 下面的页 onPageShow
...
stageNode_->RemoveChild(pageNode); // 无转场时立即销毁栈顶页细节:
PopPageToIndex(stage_manager.cpp:369-441)批量 pop 中间页时,逐页 FirePageHide,且RebuildImageByPage在显示目标页前调用(L409)——图片重建是页面恢复显示路径的一等步骤。
2.3 页面自身的回调入口(App 视角)
| App 能感知的钩子 | 触发点 | 源码 |
|---|---|---|
onPageHide | 被覆盖 / pop / 窗口后台 | page_pattern.cpp:426-470 |
onPageShow | 回到栈顶 / 窗口前台 | page_pattern.cpp:351-396(窗口不可见时直接 return,L359-362) |
onHiddenChange(页面级) | onPageShow/Hide 之后 | page_pattern.cpp:392-394,466-468 |
NavDestination onShown/onHidden | Navigation 路由切换 | navdestination_pattern(事件中心派发) |
UIObserver RouterPageState | 全状态流转 | page_pattern.cpp:376-377,449-450 |
3. 不可见页面的内存优化策略(框架层,源码实证)
ArkUI 在 frameworks/core/components_ng/manager/memory/memory_manager.{h,cpp}(2024 年合入)建立了页面级内存回收子系统。这是与 iOS 最本质的差异所在。
3.1 机制一:不可见页面图片回收(MemoryManager::TrimMemRecycle)
注册:页面节点 attach 时自动进入回收候选表——PagePattern::OnAttachToFrameNode(page_pattern.cpp:83-86):
auto memoryManager = pipelineContext->GetMemoryManager();
if (memoryManager) { memoryManager->AddRecyclePageNode(host); }节点 detach 时移除(page_pattern.cpp:317-327)。
触发:应用窗口 OnHide(退后台)后 500ms,PipelineContext::OnHide(pipeline_context.cpp:5762-5783):
if (memoryMgr_) { memoryMgr_->PostMemRecycleTask(); } // L5780-5782PostMemRecycleTask 延迟 BACKGROUND_RECYCLE_WAIT_TIME_MS = 500ms(memory_manager.cpp:24,131-138)执行 TrimMemRecycle(memory_manager.cpp:141-160):
for (auto& weak : pageNodes_) {
auto frameNode = weak.Upgrade(); ...
if (frameNode->IsVisible() || frameNode->IsTrimMemRecycle()) { continue; } // 跳过可见页
RecycleImageByPage(frameNode); // 不可见页 → 回收
}RecycleImageByPage(memory_manager.cpp:79-85)对页面打 SetTrimMemRecycle(true) 标记,然后 RecycleImage 递归遍历整页,每页最多回收 RECYCLE_PAGE_IMAGE_NUM = 20 张图片(memory_manager.cpp:25,62-76):
if (childNode->GetTag() == V2::IMAGE_ETS_TAG) {
auto imagePattern = childNode->GetPattern<ImagePattern>();
if ((!imagePattern) || (!imagePattern->RecycleImageData())) { continue; }
recycleNum--;
...
}ImagePattern::RecycleImageData(image_pattern.cpp:1646-1686)释放的是图片解码后的像素数据与渲染上下文:
loadingCtx_ = nullptr; // 加载上下文(含解码器)
rsRenderContext->RemoveContentModifier(contentMod_); // 渲染修饰器
contentMod_ = nullptr; imagePaintMethod_ = nullptr;
image_ = nullptr; // 解码位图
altLoadingCtx_ = nullptr; altImage_ = nullptr; altErrorCtx_ = nullptr; altErrorImage_ = nullptr;
isRecycledImage_ = true;两个安全阀:
- 总开关:应用级
SetIsRecycleInvisibleImageMemory(pipeline_context.h:1114-1121)> 系统参数persist.ace.recycle.image.enabled(ohos_system_properties.cpp:57,135-138,默认 false); - 网络图片仅当缓存安全(
IsNetworkImageSafeToRecycle)才回收,避免回前台重新下载。
重建路径:页面重新显示(pop 回来 / router show)时 StageManager 调 RebuildImageByPage(stage_manager.cpp:344,409,459)→ 递归找 IsTrimMemRecycle() 的图片节点 LoadImageDataIfNeed()(memory_manager.cpp:98-119;image_pattern.cpp:1217-1240:无 loadingCtx 或 src 变化则重新 LoadImage,走图片缓存)。
3.2 机制二:NavDestination 隐藏即回收(master 新增,500ms 防抖)
对 Navigation 场景(HarmonyOS NEXT 商品页主流容器),master 分支有更积极的一页内回收:
图片节点 attach 到树时注册隐藏回调(image_pattern.cpp:1889-1930):
void ImagePattern::OnAttachToMainTree() { ... RegisterNavDestinationHiddenChange(); }受系统参数 const.arkui.recycle.navigation.image.enable 控制(ohos_system_properties.cpp:58,140-142)。
MemoryManager::RegisterNavDestinationHiddenChange(memory_manager.cpp:170-201)向上找最近的 NavDestination 祖先,把回调挂进其 EventHub。NavDestination 隐藏 → ImagePattern::OnNavDestinationHiddenChange(false)(image_pattern.cpp:1946-1964)→ PostNavDestRecycleTask:延迟 NAV_DEST_RECYCLE_DELAY_MS = 500ms(image_pattern.h:457)后 ExecuteNavDestRecycle → RecycleImageDataForNav(image_pattern.cpp:1980-2004)。shown 时取消挂起任务并 LoadImageDataIfNeed 重建。
3.3 机制三:图片组件自身对"不在渲染树"的兜底
ImagePattern::OnWindowHide(image_pattern.cpp:1799-1810):窗口退后台时,凡 !renderContext->IsOnRenderTree()(不在渲染树 = 所在页面不可见或组件未挂渲染树)的图片立即 RecycleImageData();重新挂上渲染树(OnAttachToMainRenderTree,image_pattern.cpp:1812-1820)或离屏加工(OnOffscreenProcessResource)时再加载。配合 §3.1 的"整页 20 张"限额,形成窗口级 + 页面级两层回收。
3.4 版本与开关矩阵(防止误读)
| 机制 | 代码分支 | 生效条件 | 备注 | ||
|---|---|---|---|---|---|
| 页面图片整页回收(TrimMem) | 5.1.0-Release 存在(memory_manager_r51.cpp:28-31) | persist.ace.recycle.image.enabled=true 且 App targetAPI ≥ 16;master 已改为恒关(manager/memory_manager.cpp:30 直接 isTrimMemWork_ = false) | 5.1 构造函数:`if (!SystemProperties::GetRecycleImageEnabled() | !Container::GreatOrEqualAPITargetVersion(VERSION_SIXTEEN)) isTrimMemWork_ = false;` | |
| NavDestination 隐藏回收 | master / 6.x(5.1 无此函数,diff 实证) | const.arkui.recycle.navigation.image.enable | 一页内、无 20 张/页上限语义(按图注册) | ||
| 窗口级 OnWindowHide 图片回收 | master | persist.ace.recycle.image.enabled + 不在渲染树 | 应用退后台时点 | ||
| 应用级总开关 | 全版本 | UIContext.SetIsRecycleInvisibleImageMemory(应用主动开启,pipeline_context.h:1114) | 优先级最高的应用可控开关 |
[推断] TrimMem 整页回收在 master 恒关、由 NavDestination 机制替代的趋势说明:华为把"页面级统一回收"收敛为"组件级(图片)按宿主可见性回收",粒度更细、误伤更小。对商品详情页场景,HarmonyOS NEXT 上 Navigation+NavDestination 是主路径,隐藏 500ms 后图片像素内存即被释放(开关打开时)。
3.5 框架层的其它内存手段(与可见性相关的)
- LazyForEach / 组件复用(@Reusable):List/Grid 数据懒加载,
OnRecycle/OnReuse(image_pattern.cpp:1729-1768,回收时清图片数据、复用时重加载)——滚动场景内存控制的主力,但属"数据驱动"而非"可见性驱动"。 - CustomNode 回收池:
UINode::NotifyColorModeChange中FireClearAllRecycleFunc/FireClearParentReusePoolIfNeeded(ui_node.cpp:2243-2252)在特定时机清空复用池。 - 窗口级 FlushWindowStateChangedCallback:窗口显隐时逐组件通知(ImagePattern 注册了该回调,见
RegisterWindowStateChangedCallback,image_pattern.cpp:1770-1778)。
4. OHOS 系统层:不可见页所在进程的整体待遇
以上是框架层(App 进程内)。当整个 App 退到后台(所有页面不可见)时,系统级内存管理接管(详见姊妹篇 notes/ohos-system-memory.md,本节列主干事实):
4.1 memmgr 分级回收优先级(resourceschedule_memmgr)
进程状态映射为回收优先级(services/memmgrservice/include/reclaim_priority_manager/reclaim_priority_constants.h,数值越大越先被回收):
| 优先级 | 值 | 含义 |
|---|---|---|
| RECLAIM_PRIORITY_FOREGROUND | 0 | 前台 |
| RECLAIM_PRIORITY_VISIBLE | 50 | 可见(如分屏) |
| RECLAIM_PRIORITY_BG_SUSPEND_DELAY | 100 | 可感知的挂起延迟 |
| RECLAIM_PRIORITY_BG_PERCEIVED | 200 | 可感知后台(如后台播放) |
| RECLAIM_PRIORITY_BACKGROUND | 400 | 普通后台 |
| RECLAIM_PRIORITY_FROZEN | 600 | 冻结进程 |
| RECLAIM_PRIORITY_SUSPEND | 800 | 挂起进程 |
| RECLAIM_PRIORITY_EMPTY | 900 | 空进程 |
应用退后台(AppStateUpdateReason::BACKGROUND)后优先级升至 400,ApplyReclaimPriority(reclaim_priority_manager.cpp:745 附近)把优先级写入内核:OomScoreAdjUtils::WriteOomScoreAdjToKernel + (HyperHold 使能时)ReclaimStrategyManager::NotifyAppStateChanged 更新 memcg 的 score 与回收比率。
4.2 zswapd:按 score 分档的内存压缩/换出
内核 zswapd 线程按 buffer 水位(默认下发 availBuffer/minAvailBuffer/highAvailBuffer = 800/750/850MB 档位,memcg.cpp 水位写入口;XML memmgr_config.xml:18-22 另有出厂值 100/50/150/200)对所有 memcg(含前台进程)的匿名页做压缩。每个用户 memcg 的回收比率默认 MEMCG_MEM_2_ZRAM_RATIO = 60%(zram 压缩)、MEMCG_ZRAM_2_UFS_RATIO = 10%(ESwap 换出到存储)、refaultThreshold = 50(reclaim_strategy_constants.h:27-32;root memcg 40%/0%),由 Memcg 构造时下发(memcg.cpp:77-80)到 memory.zswapd_single_memcg_param。zram 设备大小/算法不在 memmgr 仓库(产品 init/内核配置,zram 驱动在 kernel_linux_5.10/drivers/block/zram/,含按组写回的 zram_group 扩展)。
这是对前台 App 内不可见页面唯一"无需应用配合"的系统级收益:被覆盖页面持有的匿名内存(未显示的位图、JS 对象)冷下来后同样被压进 zram,物理占用直接缩小;返回该页时缺页 + zram 解压恢复(少量 CPU、无磁盘 IO)。refault 阈值避免"马上要回看"的页被过度换出。
4.3 内存分级通知(onMemoryLevel 的来源)
MemoryLevelManager::CalcSystemMemoryLevel(memory_level_manager.cpp:57-80)按内核 avail buffer 分级:critical(600MB) < low(700) < moderate(800) < purgeable(1024)(XML systemMemoryLevelConfig),PSI(/proc/pressure/memory)与 kswapd 监控双源触发。派发链:AppMgrService → MainThread → OHOSApplication::OnMemoryLevel(ohos_application.cpp:352-381:遍历所有 abilityRecord 逐个 NotifyMemoryLevel + 全部 abilityStage + DispatchMemoryLevel)→ JsAbility::OnMemoryLevel(js_ability.cpp:902-925)回调 ArkTS onMemoryLevel。通知是进程级的,发给所有 Ability,与页面可见性无关——router 嵌套 push 不产生窗口级 UN_VISIBLE 事件(memmgr 的可见性事件源是窗口级),因此系统层完全感知不到"App 内第 3 页盖住了第 2 页"。
4.4 purgeable 内存(OHOS 版 NSPurgeableData)
- 用户态库:
commonlibrary_memory_utils/libpurgeablemem(PurgeableMemBase::BeginRead/EndRead/BeginWrite/EndWrite,purgeable_mem_base.h:35-83——读写期间 OS 不可回收,其余时间可回收并按 Builder 重建)。 - PixelMap 集成:
multimedia_image_framework/.../pixel_map.cpp:171-176(purgeable PixelMap 注册进PurgeableResourceManager的 LRU,受IMAGE_PURGEABLE_PIXELMAP编译门控)。 - 前后台协同:
ability_ability_runtime/.../main_thread.cpp:3104-3106,3132-3134——App 退后台时EndAccessPurgeableMem()(允许内核回收 purgeable 页),回前台BeginAccessPurgeableMem()(重建)。 - 内核态三条回收通道(
kernel_linux_5.10):①mm/purgeable.cuxpte 用户扩展页表(缺页时do_uxpte_page_fault按标记重建);②drivers/staging/android/ashmem.c的 PUNCH_HOLE 直接丢页;③mm/purgeable_ashmem_trigger.c提供 /proc 触发口与 per-process 统计。系统侧管理在 memmgrpurgeable_mem_manager(HEAP→ASHMEM→SUBSCRIBER 三级回收 + 白名单 + 按 adj 排序,配置memmgr_config.xml的purgeablememConfig)。XNU 对应物是vm_purgeable.c的 token 状态机(见第二部分 §7.3)。
4.5 进程冻结与查杀
- 后台挂起:
HandleApplicationSuspend把进程优先级设为RECLAIM_PRIORITY_SUSPEND=800(reclaim_priority_manager.cpp,"application suspend");内核具备 cgroup v2 freezer、RSS 服务有 isFrozen 查询接口,但开源仓库中未找到执行 cgroup.freeze 的组件([推断] 由闭源的系统服务或按产品配置实施)。冻结解的是 CPU 与内存增长,不释放已占内存。 - 查杀是最后手段:用户态 LMKD 按 5 级 kill 表(XML
killConfig:avail ≤500MB 杀 minPriority≥400 即普通后台,≤400 杀 300+,…,≤100MB 杀 0+ 即除前台全杀,low_memory_killer.cpp:46-52)SIGKILL 整个后台应用。
对商品详情页场景的意义:App 在前台时(优先级 0,无窗口级 UN_VISIBLE 事件),kill/冻结/优先级机制全部失效——栈内 N 个页面的组件树与状态对象必须靠框架层(§3)或 App 自己管理;系统级真正惠及前台 App 内不可见页面的只有两条:zram 匿名页压缩(§4.2,自动无感)与 purgeable 丢页-重建(§4.4,需位图走 purgeable 通道);App 一旦整体退后台,zram 压缩/purgeable 回收/冻结/查杀逐级介入,但那是"进程粒度",不区分页面。
onMemoryLevel是系统给 App 的"主动清理不可见页面缓存"的信号——框架不替你响应。
第二部分 iOS:UIKit 框架层 + XNU 内核层
5. UIKit 的可见性语义:通知状态机,无状态查询
(详细证据见姊妹篇 notes/ios-uikit.md,以下为骨架 + 关键头文件行号,均为本机 iOS 26.4 SDK 实测)
5.1 页面间可见/不可见关系的表达
- appearance 四回调:
UIViewController.h:162-188。viewWillDisappear:的头文件注释原文——"Called when the view is about to be dismissed, covered, or otherwise hidden":"covered"(被覆盖) 正是嵌套 push 商品页的场景:下面的页面只是被盖上,不是销毁。 - 底层驱动:容器通过
beginAppearanceTransition:animated:/endAppearanceTransition(UIViewController.h:459-465)成对驱动子 VC appearance。 - 无公开状态查询:UIKit 不提供
isAppeared;导航控制器只有topViewController/visibleViewController(UINavigationController.h:67-70)视角。
5.2 谁持有谁:栈 = 强持有的内存栈
UINavigationController.viewControllers(copyNSArray,强引用元素,UINavigationController.h:70):N 个商品详情页 VC 全部驻留。UIViewController.view(strong,UIViewController.h:116)持有整棵视图树;push 转场结束后被覆盖页的 view 移出 window 层级但 VC 仍持有(容器切换语义,UIViewController.h:447-455)。- 视图懒加载是唯一的按需分配语义:
loadView只在 view getter 首次访问时触发(UIViewController.h:116-118)——但嵌套 push 场景每个页面都必然已 loadView,懒加载帮不上忙。
6. UIKit 对不可见页面的内存策略:零回收(源码级铁证)
6.1 viewDidUnload 的死亡:框架放弃自动卸载
UIViewController.h:121-122:viewWillUnload/viewDidUnload仍声明但API_DEPRECATED(ios 6.0)。- 决定性注释在
didReceiveMemoryWarning(UIViewController.h:207):"On iOS 6.0 it will no longer clear the view by default." —— iOS 6 起内存警告默认实现连 view 都不清了,更没有任何"不可见页面自动卸载"路径(全 SDK grep 无 unload 相关新 API)。 - iOS 17+ 只有
viewIsAppearing(UIViewController.h:182)这类时序细化;iOS 26.4 SDK 无新回收机制(姊妹篇第 7 节)。
6.2 内存警告:广播而非定向
UIApplicationDidReceiveMemoryWarningNotification(UIApplication.h:556)/ applicationDidReceiveMemoryWarning(UIApplication.h:389)/ UIViewController::didReceiveMemoryWarning(UIViewController.h:207)——发给所有 VC,与可见性无关。App 想让"不可见页"让内存,得自己在 viewDidDisappear 里动手、在 viewWillAppear 里重建(框架不阻止,也不帮忙)。
6.3 框架留给 App 的原语
NSCache(NSCache.h:12-35,cost/limit + 压力下自动逐出)、NSDiscardableContent(NSObject.h:68-74)、NSPurgeableData(NSData.h:230-234)——是"容器级"弹性,不作用于页面/视图本身。
结论②(iOS 侧):UIKit 对被覆盖页面的框架级内存动作 = 零。释放责任完全下沉到 App(业务层)与 XNU(内核层)。
7. XNU 内核层:进程粒度的四层内存弹性
(行号均为 xnu-7195.141.2 = iOS 14.8 源码实测,本地存档 src/xnu/;跨版本演化数据来自 skill 六版本存档)
iOS 上"多个商品页占内存"最终体现为前台进程的 RSS/phys_footprint。XNU 对它的优化与"页面可见性"完全解耦(内核不知道 App 内页面栈),但按"进程前后台状态"分四层:
7.1 Jetsam band:杀谁不杀谁
bsd/sys/kern_memorystatus.h:42-66 的优先级带:IDLE=0 < BACKGROUND_OPPORTUNISTIC=2 < BACKGROUND=3 < ... < FOREGROUND_SUPPORT=9 < FOREGROUND=10 < ... CRITICAL=19。前后台由 memorystatus_update 系列换 band;前台商品页浏览进程在 band 10,基本不被杀;后台则随内存压力逐 band 清场。与 OHOS memmgr 的 §4.1 完全同构(连数值都接近:前台 0/10,后台 400/3-400)。
7.2 内存压缩器:不杀进程的"挤水分"(对应 OHOS zram)
vm_compressor.c:c_segment 段式压缩,算法在 vm_compressor_algorithms.c(WKdm 汇编主路径 + LZ4 择优,metacompressor 分派 L296-405;16K 页 lz4_threshold=12288 编译期设定 L453)。冷页(含不可见页面的内存)被压缩存放,访问时缺页解压——iOS 没有 swap 到存储(无 zram2ufs 对应物),全靠压缩器 + purgeable + 杀进程。
7.3 Purgeable:内核级的"可丢弃"内存(iOS 版 purgeable ashmem)
osfmk/vm/vm_purgeable.c 的 token 状态机:
- 对象置
VM_PURGABLE_VOLATILE时进入 FIFO/LIFO 队列并记账为 volatile(vm_purgeable_accounting,L1505 起;ledger 区分 volatile/volatile_compressed,L1690-1715); - 内存压力下
vm_pageout_scan → vps_purge_object(vm_pageout.c:1837-1874):pressure level ≥ Warning/Urgent/Critical 时 force_purge group 阈值 2/5/8(vm_pageout.c:4822-4824); vm_purgeable_object_purge_one(vm_purgeable.c:895-1025)选受害者 →vm_object_purge直接把整对象清空(purge_now段 L1006-1013),进程不死、页直接丢;App 下次访问时按 discardable 协议重建(UIImageView 解码缓存、IOSurface 像素缓冲等大量使用)。
7.4 Freezer:后台进程冻结(iOS 版"挂起")
kern_memorystatus_freeze.c:后台 App 的用户页冻结进压缩区(freeze),唤醒时 thaw——冻结解除前不占 CPU、不再触碰内存;这是 iOS 后台 App "休眠"的内核实现(iOS 12→18 freeze 相关引用 384→887,逐年强化,六版本口径)。对前台商品页浏览无直接影响。
对商品详情页场景的意义(iOS):App 在前台时内核四层机制只提供"压缩器挤冷页"这一条被动路径;页面栈的 N 棵视图树 + 解码位图全部按 App 意志驻留。iOS 的答案:可见性归 UIKit 通知、内存归 App 自管 + 内核兜底,框架层(UIKit)对不可见页面无任何优化——这与 ArkUI 在框架层做页面图片回收形成最鲜明对比。
第三部分 总结对比表
| 维度 | OpenHarmony ArkUI | iOS UIKit/XNU |
|---|---|---|
| 可见性模型 | 四层显式状态(回调/可见属性/激活/冻结),被覆盖页 INVISIBLE+失活,仍在树中 | appearance 回调通知("covered"),被覆盖页 view 移出层级、VC 强持有 |
| 栈持有 | Stage 节点 children(RefPtr 强引用),router 栈 WeakPtr 仅记录 | viewControllers NSArray 强持有 |
| 不可见页组件树 | 完整驻留(同 iOS) | 完整驻留 |
| 框架主动回收 | 有:页面图片回收(≤20 张/页,500ms 防抖)、NavDestination 隐藏回收(500ms)、退后台整页回收、重建于页面显示前 | 无(viewDidUnload 已废除;内存警告只是广播) |
| 回收对象 | 图片像素数据(loadingCtx/image_/contentMod_)为主 | —(App 自管 NSCache/NSPurgeableData) |
| 回前台/回栈顶重建 | RebuildImageByPage / LoadImageDataIfNeed(框架自动) | —(App 自管) |
| 应用退后台(系统级) | memmgr:优先级 400→zram 压缩(60%)→ESwap 换出(10%)→purgeable 回收→冻结/挂起(suspend 800)→LMKD 查杀 | XNU:band 降级→压缩器→purgeable→freezer→jetsam |
| 内核可丢弃内存 | purgeable ashmem(uxpte/PUNCH_HOLE)+ PurgeableResourceManager LRU | vm_purgeable token 状态机 + NSPurgeableData/IOSurface |
| 内存分级通知 | onMemoryLevel(purgeable 1024/moderate 800/low 700/critical 600MB avail,进程级广播) | didReceiveMemoryWarning(avail pages 比例 kVMPressureWarning/Critical,进程级广播) |
给开发者的实操差异(商品详情页场景)
- HarmonyOS 上:用 Navigation+NavDestination(而非 router)承载嵌套详情页,打开
const.arkui.recycle.navigation.image.enable(及应用的SetIsRecycleInvisibleImageMemory),被覆盖页图片 500ms 后自动释放像素、回来自动重建——框架已经替你做了大头的内存优化;组件树/状态仍需 LazyForEach/@Reusable 控制规模。 - iOS 上:框架什么都不做。栈深控制(同款商品去重、
setViewControllers:压栈)、viewDidDisappear释放重型资源 +viewWillAppear重建、图片解码走NSCache/downsampling,是仅有的手段;内存上限由 jetsam(phys_footprint)说了算。 - 两边的共同规律:框架/系统从不主动销毁"不可见页面"的组件树与业务状态——可见性优化回收的永远只是"可按需重建的解码数据",业务状态(滚动位置、选中项)必须由 App 在页面隐藏时持久化(ArkUI 的 AutoSave 机制正是为此:
stage_manager.cpp:209FireAutoSave)。 - 两边的系统层都感知不到"App 内页面栈":OHOS 的可见性事件源是窗口级(router 嵌套 push 不产生 UN_VISIBLE),iOS 的 jetsam band 只看进程前后台。所以"不可见页面的内存优化"要么由 UI 框架做(ArkUI 做了图片回收),要么由 App 做(iOS 全靠自己),系统层只做进程粒度的压缩/回收/查杀兜底。
附:证据文件清单(本地存档路径)
| 侧 | 文件 | 路径(相对 page-stack-memory-analysis/) |
|---|---|---|
| ArkUI | StageManager | src/ace/stage/stage_manager.{h,cpp} |
| ArkUI | PagePattern | src/ace/stage/page_pattern.{h,cpp} |
| ArkUI | PageRouterManager | src/ace/ng/page_router_manager.{h,cpp} |
| ArkUI | MemoryManager | src/ace/manager/memory_manager.{h,cpp}(另存 5.1 对照 memory_manager_r51.cpp) |
| ArkUI | ImagePattern | src/ace/image_pattern.{h,cpp} |
| ArkUI | UINode/FrameNode/ElementRegister | src/ace/base/{ui_node,frame_node}.*、src/ace/element_register.* |
| ArkUI | PipelineContext / SystemProperties | src/ace/pipeline_context.{h,cpp}、src/ace/ohos_system_properties.cpp |
| OHOS | memmgr | src/memmgr/(优先级表、memory_level_manager、reclaim_strategy_manager、memmgr_config.xml) |
| OHOS | purgeable 用户态 | src/repos/commonlibrary_memory_utils/libpurgeablemem/ |
| OHOS | purgeable 内核 | src/kernel_linux_5.10/mm/{purgeable.c,purgeable_ashmem_trigger.c} |
| OHOS | ability_runtime | src/repos/ability_ability_runtime/(JsAbility::OnMemoryLevel、MainThread purgeable 前后台协同) |
| XNU | jetsam/压缩器/purgeable/pageout | src/xnu/{kern_memorystatus.*,kern_memorystatus.h,vm_compressor.c,vm_purgeable.c,vm_pageout.c} |
| iOS | UIKit/Foundation 头文件 | 本机 iOS 26.4 SDK(行号详见 notes/ios-uikit.md 附录) |
姊妹篇(更细的单侧深读):
notes/ios-uikit.md—— iOS UIKit 七节全证据 + 证据索引表notes/ohos-system-memory.md—— OHOS memmgr/purgeable/冻结/zram 详解(系统层)