SOURCE-LEVEL ANALYSIS · 源码级对比研究

嵌套商品详情页的可见性与内存策略

当用户连续打开多层商品详情页(页面栈深度 N,仅栈顶可见),OpenHarmony ArkUI 与 iOS UIKit/XNU 分别如何处理"被覆盖的页面"——以真实源码逐行验证。
问题 ① 关系
页面之间存在可见/不可见关系吗?
存在。ArkUI:被覆盖页保持挂树,四层状态显式翻转为"不可见";UIKit:只发通知("covered"),无状态查询。
问题 ② 区分
框架层如何区分处理?
ArkUI:生命周期回调 + 可见性属性 + 节点激活 + 转场冻结四层联动;UIKit:appearance 回调状态机,仅通知。
问题 ③ 内存
不可见页面有内存优化吗?
ArkUI 有:隐藏页图片回收/重建(500ms 防抖、每页≤20 张);UIKit 零回收:viewDidUnload 已废除,弹性全靠 XNU 与 App。
arkui_ace_engine@masterresourceschedule_memmgr@masterxnu-7195.141.2 (iOS 14.8)iOS 26.4 SDK全部行号逐条复核
📊 主报告UIKit 详篇系统层详篇

结论速览

问题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::LoadPagepage_router_manager.cpp:1597):

``cpp auto pageNode = CreatePage(pageId, target); pageRouterStack_.emplace_back(pageNode); ... OnPageReady(pageNode, needHideLast, needTransition) ... ``

  • OnPageReady 最终调 StageManager::PushPagepage_router_manager.cpp:1631 附近)。

真正的强持有者是 Stage 节点的子节点列表StageManager::PushPage 把新页面 node->MountToParent(stageNode_)stage_manager.cpp:219)。Stage 节点是 window 根节点的子节点,其 children_(RefPtr 强引用)按 push 顺序持有全部 N 个页面节点——被覆盖的页面不会被摘除,整棵组件树(FrameNode 树 + 每个 Image/Text 的布局与渲染属性对象)持续存活。组件注册表 ElementRegisteritemMap_ 只存 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);

FirePageHidePagePattern::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::ProcessHideStatepage_pattern.cpp:230-241)是"不可见"状态的落地:

host->SetActive(false);                                          // 节点失活(递归)
host->NotifyVisibleChange(VisibleType::VISIBLE, VisibleType::INVISIBLE); // 通知子树
host->GetLayoutProperty()->UpdateVisibility(VisibleType::INVISIBLE);     // 可见性属性
parent->MarkNeedSyncRenderTree(); parent->RebuildRenderContextTree();    // 渲染树同步

与之对称的 ProcessShowStatepage_pattern.cpp:243-269)在页面回到栈顶时 SetActive(true) + UpdateVisibility(VISIBLE),并按需重新布局(安全区/键盘变化时 MarkDirtyNode)。

结论①:每个页面之间存在明确的可见/不可见关系,由框架状态机维护,四层状态同步翻转:

  1. 生命周期层:onPageShow / onPageHide(回调 App)
  2. 可见性属性层UpdateVisibility(INVISIBLE)(布局属性)
  3. 节点激活层SetActive(false) 递归整棵子树(ui_node.cpp:2194-2199)+ SetJSViewActive(false)(JS 侧状态管理同步,page_pattern.cpp:440
  4. 转场冻结层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-229isNewLifecycle 分支);旧时序则先 hide 旧页。这保证了新页面创建失败时旧页面状态不被破坏。

pop 时(StageManager::PopPagestage_manager.cpp:325-367):

pipeline->GetMemoryManager()->RebuildImageByPage(inPageNode);  // L344: 回归页先重建图片!
FireAutoSave(outPageNode, inPageNode);
FirePageHide(pageNode, ...);      // 栈顶页 onPageHide
FirePageShow(inPageNode, ...);    // 下面的页 onPageShow
...
stageNode_->RemoveChild(pageNode); // 无转场时立即销毁栈顶页

细节:PopPageToIndexstage_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/onHiddenNavigation 路由切换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::OnAttachToFrameNodepage_pattern.cpp:83-86):

auto memoryManager = pipelineContext->GetMemoryManager();
if (memoryManager) { memoryManager->AddRecyclePageNode(host); }

节点 detach 时移除(page_pattern.cpp:317-327)。

触发:应用窗口 OnHide(退后台)后 500msPipelineContext::OnHidepipeline_context.cpp:5762-5783):

if (memoryMgr_) { memoryMgr_->PostMemRecycleTask(); }   // L5780-5782

PostMemRecycleTask 延迟 BACKGROUND_RECYCLE_WAIT_TIME_MS = 500msmemory_manager.cpp:24,131-138)执行 TrimMemRecyclememory_manager.cpp:141-160):

for (auto& weak : pageNodes_) {
    auto frameNode = weak.Upgrade(); ...
    if (frameNode->IsVisible() || frameNode->IsTrimMemRecycle()) { continue; }  // 跳过可见页
    RecycleImageByPage(frameNode);      // 不可见页 → 回收
}

RecycleImageByPagememory_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::RecycleImageDataimage_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;

两个安全阀:

  • 总开关:应用级 SetIsRecycleInvisibleImageMemorypipeline_context.h:1114-1121)> 系统参数 persist.ace.recycle.image.enabledohos_system_properties.cpp:57,135-138,默认 false);
  • 网络图片仅当缓存安全(IsNetworkImageSafeToRecycle)才回收,避免回前台重新下载。

重建路径:页面重新显示(pop 回来 / router show)时 StageManagerRebuildImageByPagestage_manager.cpp:344,409,459)→ 递归找 IsTrimMemRecycle() 的图片节点 LoadImageDataIfNeed()memory_manager.cpp:98-119image_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::RegisterNavDestinationHiddenChangememory_manager.cpp:170-201)向上找最近的 NavDestination 祖先,把回调挂进其 EventHub。NavDestination 隐藏 → ImagePattern::OnNavDestinationHiddenChange(false)image_pattern.cpp:1946-1964)→ PostNavDestRecycleTask延迟 NAV_DEST_RECYCLE_DELAY_MS = 500msimage_pattern.h:457)后 ExecuteNavDestRecycle → RecycleImageDataForNavimage_pattern.cpp:1980-2004)。shown 时取消挂起任务并 LoadImageDataIfNeed 重建。

3.3 机制三:图片组件自身对"不在渲染树"的兜底

ImagePattern::OnWindowHideimage_pattern.cpp:1799-1810):窗口退后台时,凡 !renderContext->IsOnRenderTree()(不在渲染树 = 所在页面不可见或组件未挂渲染树)的图片立即 RecycleImageData();重新挂上渲染树(OnAttachToMainRenderTreeimage_pattern.cpp:1812-1820)或离屏加工(OnOffscreenProcessResource)时再加载。配合 §3.1 的"整页 20 张"限额,形成窗口级 + 页面级两层回收。

3.4 版本与开关矩阵(防止误读)

机制代码分支生效条件备注
页面图片整页回收(TrimMem)5.1.0-Release 存在(memory_manager_r51.cpp:28-31persist.ace.recycle.image.enabled=true App targetAPI ≥ 16;master 已改为恒关(manager/memory_manager.cpp:30 直接 isTrimMemWork_ = false5.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 图片回收masterpersist.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/OnReuseimage_pattern.cpp:1729-1768,回收时清图片数据、复用时重加载)——滚动场景内存控制的主力,但属"数据驱动"而非"可见性驱动"。
  • CustomNode 回收池UINode::NotifyColorModeChangeFireClearAllRecycleFunc/FireClearParentReusePoolIfNeededui_node.cpp:2243-2252)在特定时机清空复用池。
  • 窗口级 FlushWindowStateChangedCallback:窗口显隐时逐组件通知(ImagePattern 注册了该回调,见 RegisterWindowStateChangedCallbackimage_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_FOREGROUND0前台
RECLAIM_PRIORITY_VISIBLE50可见(如分屏)
RECLAIM_PRIORITY_BG_SUSPEND_DELAY100可感知的挂起延迟
RECLAIM_PRIORITY_BG_PERCEIVED200可感知后台(如后台播放)
RECLAIM_PRIORITY_BACKGROUND400普通后台
RECLAIM_PRIORITY_FROZEN600冻结进程
RECLAIM_PRIORITY_SUSPEND800挂起进程
RECLAIM_PRIORITY_EMPTY900空进程

应用退后台(AppStateUpdateReason::BACKGROUND)后优先级升至 400,ApplyReclaimPriorityreclaim_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 = 50reclaim_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::CalcSystemMemoryLevelmemory_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::OnMemoryLevelohos_application.cpp:352-381:遍历所有 abilityRecord 逐个 NotifyMemoryLevel + 全部 abilityStage + DispatchMemoryLevel)→ JsAbility::OnMemoryLeveljs_ability.cpp:902-925)回调 ArkTS onMemoryLevel通知是进程级的,发给所有 Ability,与页面可见性无关——router 嵌套 push 不产生窗口级 UN_VISIBLE 事件(memmgr 的可见性事件源是窗口级),因此系统层完全感知不到"App 内第 3 页盖住了第 2 页"。

4.4 purgeable 内存(OHOS 版 NSPurgeableData)

  • 用户态库:commonlibrary_memory_utils/libpurgeablememPurgeableMemBase::BeginRead/EndRead/BeginWrite/EndWritepurgeable_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.c uxpte 用户扩展页表(缺页时 do_uxpte_page_fault 按标记重建);② drivers/staging/android/ashmem.c 的 PUNCH_HOLE 直接丢页;③ mm/purgeable_ashmem_trigger.c 提供 /proc 触发口与 per-process 统计。系统侧管理在 memmgr purgeable_mem_manager(HEAP→ASHMEM→SUBSCRIBER 三级回收 + 白名单 + 按 adj 排序,配置 memmgr_config.xmlpurgeablememConfig)。XNU 对应物是 vm_purgeable.c 的 token 状态机(见第二部分 §7.3)。

4.5 进程冻结与查杀

  • 后台挂起:HandleApplicationSuspend 把进程优先级设为 RECLAIM_PRIORITY_SUSPEND=800reclaim_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-188viewWillDisappear: 的头文件注释原文——"Called when the view is about to be dismissed, covered, or otherwise hidden":"covered"(被覆盖) 正是嵌套 push 商品页的场景:下面的页面只是被盖上,不是销毁。
  • 底层驱动:容器通过 beginAppearanceTransition:animated: / endAppearanceTransitionUIViewController.h:459-465)成对驱动子 VC appearance。
  • 无公开状态查询:UIKit 不提供 isAppeared;导航控制器只有 topViewController/visibleViewControllerUINavigationController.h:67-70)视角。

5.2 谁持有谁:栈 = 强持有的内存栈

  • UINavigationController.viewControllerscopy NSArray,强引用元素,UINavigationController.h:70):N 个商品详情页 VC 全部驻留。
  • UIViewController.viewstrongUIViewController.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-122viewWillUnload/viewDidUnload 仍声明但 API_DEPRECATED(ios 6.0)
  • 决定性注释在 didReceiveMemoryWarningUIViewController.h:207):"On iOS 6.0 it will no longer clear the view by default." —— iOS 6 起内存警告默认实现连 view 都不清了,更没有任何"不可见页面自动卸载"路径(全 SDK grep 无 unload 相关新 API)。
  • iOS 17+ 只有 viewIsAppearingUIViewController.h:182)这类时序细化;iOS 26.4 SDK 无新回收机制(姊妹篇第 7 节)。

6.2 内存警告:广播而非定向

UIApplicationDidReceiveMemoryWarningNotificationUIApplication.h:556)/ applicationDidReceiveMemoryWarningUIApplication.h:389)/ UIViewController::didReceiveMemoryWarningUIViewController.h:207)——发给所有 VC,与可见性无关。App 想让"不可见页"让内存,得自己在 viewDidDisappear 里动手、在 viewWillAppear 里重建(框架不阻止,也不帮忙)。

6.3 框架留给 App 的原语

NSCacheNSCache.h:12-35,cost/limit + 压力下自动逐出)、NSDiscardableContentNSObject.h:68-74)、NSPurgeableDataNSData.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.cc_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_objectvm_pageout.c:1837-1874):pressure level ≥ Warning/Urgent/Critical 时 force_purge group 阈值 2/5/8(vm_pageout.c:4822-4824);
  • vm_purgeable_object_purge_onevm_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 ArkUIiOS 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 LRUvm_purgeable token 状态机 + NSPurgeableData/IOSurface
内存分级通知onMemoryLevel(purgeable 1024/moderate 800/low 700/critical 600MB avail,进程级广播)didReceiveMemoryWarning(avail pages 比例 kVMPressureWarning/Critical,进程级广播)

给开发者的实操差异(商品详情页场景)

  1. HarmonyOS 上:用 Navigation+NavDestination(而非 router)承载嵌套详情页,打开 const.arkui.recycle.navigation.image.enable(及应用的 SetIsRecycleInvisibleImageMemory),被覆盖页图片 500ms 后自动释放像素、回来自动重建——框架已经替你做了大头的内存优化;组件树/状态仍需 LazyForEach/@Reusable 控制规模。
  2. iOS 上:框架什么都不做。栈深控制(同款商品去重、setViewControllers: 压栈)、viewDidDisappear 释放重型资源 + viewWillAppear 重建、图片解码走 NSCache/downsampling,是仅有的手段;内存上限由 jetsam(phys_footprint)说了算。
  3. 两边的共同规律:框架/系统从不主动销毁"不可见页面"的组件树与业务状态——可见性优化回收的永远只是"可按需重建的解码数据",业务状态(滚动位置、选中项)必须由 App 在页面隐藏时持久化(ArkUI 的 AutoSave 机制正是为此:stage_manager.cpp:209 FireAutoSave)。
  4. 两边的系统层都感知不到"App 内页面栈":OHOS 的可见性事件源是窗口级(router 嵌套 push 不产生 UN_VISIBLE),iOS 的 jetsam band 只看进程前后台。所以"不可见页面的内存优化"要么由 UI 框架做(ArkUI 做了图片回收),要么由 App 做(iOS 全靠自己),系统层只做进程粒度的压缩/回收/查杀兜底。

附:证据文件清单(本地存档路径)

文件路径(相对 page-stack-memory-analysis/)
ArkUIStageManagersrc/ace/stage/stage_manager.{h,cpp}
ArkUIPagePatternsrc/ace/stage/page_pattern.{h,cpp}
ArkUIPageRouterManagersrc/ace/ng/page_router_manager.{h,cpp}
ArkUIMemoryManagersrc/ace/manager/memory_manager.{h,cpp}(另存 5.1 对照 memory_manager_r51.cpp
ArkUIImagePatternsrc/ace/image_pattern.{h,cpp}
ArkUIUINode/FrameNode/ElementRegistersrc/ace/base/{ui_node,frame_node}.*src/ace/element_register.*
ArkUIPipelineContext / SystemPropertiessrc/ace/pipeline_context.{h,cpp}src/ace/ohos_system_properties.cpp
OHOSmemmgrsrc/memmgr/(优先级表、memory_level_manager、reclaim_strategy_manager、memmgr_config.xml)
OHOSpurgeable 用户态src/repos/commonlibrary_memory_utils/libpurgeablemem/
OHOSpurgeable 内核src/kernel_linux_5.10/mm/{purgeable.c,purgeable_ashmem_trigger.c}
OHOSability_runtimesrc/repos/ability_ability_runtime/(JsAbility::OnMemoryLevel、MainThread purgeable 前后台协同)
XNUjetsam/压缩器/purgeable/pageoutsrc/xnu/{kern_memorystatus.*,kern_memorystatus.h,vm_compressor.c,vm_purgeable.c,vm_pageout.c}
iOSUIKit/Foundation 头文件本机 iOS 26.4 SDK(行号详见 notes/ios-uikit.md 附录)

姊妹篇(更细的单侧深读):

  • notes/ios-uikit.md —— iOS UIKit 七节全证据 + 证据索引表
  • notes/ohos-system-memory.md —— OHOS memmgr/purgeable/冻结/zram 详解(系统层)