COMPANION · iOS SIDE

iOS UIKit:可见性语义与页面内存

appearance 回调状态机、被覆盖页的持有关系、viewDidUnload 的死亡与零回收契约——iOS 26.4 SDK 头文件逐行验证。
姊妹篇 · 主报告见导航
📊 主报告UIKit 详篇系统层详篇

1. 可见/不可见的区分机制:appearance 状态机 + 视图懒加载 + 导航栈强持有

结论

UIKit 通过 appearance 回调状态机(而非某个 isVisible 布尔属性)向页面通知可见性变化:push 覆盖时下面的 VC 收到 viewWillDisappearviewDidDisappear,回到栈顶时收到 viewWillAppearviewDidAppear。控制器本身始终被 UINavigationController.viewControllers 数组强持有,无论可见与否;但视图按需懒加载(loadView 语义)。UIKit 没有提供公开的"是否可见"查询属性(visibleViewController 只属于导航控制器视角)。

证据(头文件可证)

appearance 回调——UIKit.framework/Headers/UIViewController.h

  • UIViewController.h:162-170viewWillAppear: 及其完整注释):
/// Called when the view is about to made visible, before it is added to the hierarchy.
/// Because the view is not yet in the hierarchy at the time this method is called, it
/// is too early in the appearance transition for many usages. Prefer -viewIsAppearing:
/// instead of this method when possible. ...
- (void)viewWillAppear:(BOOL)animated;
  • UIViewController.h:183-188(appear/disappear 四个回调的后两个):
/// Called after the view has fully transitioned to visible, when any transition animations have completed.
- (void)viewDidAppear:(BOOL)animated;
/// Called when the view is about to be dismissed, covered, or otherwise hidden.
- (void)viewWillDisappear:(BOOL)animated;
/// Called after the view has fully been dismissed, covered, or otherwise hidden, when any transition animations have completed.
- (void)viewDidDisappear:(BOOL)animated;

注意 viewWillDisappear: 注释中的 "covered"(被覆盖) 一词——这正是导航栈 push 场景:不可见不等于销毁,只是被盖上。

  • UIViewController.h:459-465(供容器转发的底层原语):
// If a custom container controller manually forwards its appearance callbacks, then rather than calling
// viewWillAppear:, viewDidAppear: viewWillDisappear: or viewDidDisappear: on the children these methods
// should be used instead. This will ensure that descendent child controllers appearance methods will be
// invoked. It also enables more complex custom transitions to be implemented since the appearance callbacks are
// now tied to the final matching invocation of endAppearanceTransition.
- (void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0);
- (void)endAppearanceTransition __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0);
  • UIViewController.h:494(容器默认自动转发 appearance):
@property(nonatomic, readonly) BOOL shouldAutomaticallyForwardAppearanceMethods API_AVAILABLE(ios(6.0));

视图懒加载语义——UIViewController.h:116-125

  • UIViewController.h:116
@property(null_resettable, nonatomic,strong) UIView *view; // The getter first invokes [self loadView] if the view hasn't been set yet. Subclasses must call super if they override the setter or getter.
  • UIViewController.h:117-118
- (void)loadView; // This is where subclasses should create their custom view hierarchy if they aren't using a nib. Should never be called directly.
- (void)loadViewIfNeeded API_AVAILABLE(ios(9.0)); // Loads the view controller's view if it has not already been set.
  • UIViewController.h:119
@property(nullable, nonatomic, readonly, strong) UIView *viewIfLoaded API_AVAILABLE(ios(9.0)); // Returns the view controller's view if loaded, nil if not.
  • UIViewController.h:125
@property(nonatomic, readonly, getter=isViewLoaded) BOOL viewLoaded API_AVAILABLE(ios(3.0));

导航栈强持有——UIKit.framework/Headers/UINavigationController.h

  • UINavigationController.h:61-65
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; // Uses a horizontal slide transition. Has no effect if the view controller is already in the stack.

- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated; // Returns the popped controller.
- (nullable NSArray<__kindof UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated; // Pops view controllers until the one specified is on top. Returns the popped controllers.
- (nullable NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated; // Pops until there's only a single view controller left on the stack. Returns the popped controllers.
  • UINavigationController.h:67-70
@property(nullable, nonatomic,readonly,strong) UIViewController *topViewController; // The top view controller on the stack.
@property(nullable, nonatomic,readonly,strong) UIViewController *visibleViewController; // Return modal view controller if it exists. Otherwise the top view controller.

@property(nonatomic,copy) NSArray<__kindof UIViewController *> *viewControllers; // The current view controller stack.
  • UINavigationController.h:72
- (void)setViewControllers:(NSArray<UIViewController *> *)viewControllers animated:(BOOL)animated API_AVAILABLE(ios(3.0)); // If animated is YES, then simulate a push or pop depending on whether the new top view controller was previously in the stack.
  • 容器持有子 VC 的一般语义——UIViewController.h:428-436(containment category):
// An array of children view controllers. This array does not include any presented view controllers.
@property(nonatomic,readonly) NSArray<__kindof UIViewController *> *childViewControllers API_AVAILABLE(ios(5.0));
...
- (void)addChildViewController:(UIViewController *)childController API_AVAILABLE(ios(5.0));
  • UINavigationController.h:118-120(代理在 push/pop 时收到 willShow/didShow,与 appearance 回调时点对齐):
// Called when the navigation controller shows a new top view controller via a push, pop or setting of the view controller stack.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;

解读

  1. viewControllerscopyNSArray,Objective-C 数组对元素是强引用语义——push 进栈的商品详情页 VC(及其已加载的 view、子视图、强引用的模型数据)都被导航控制器持有,pop 出栈才释放。pushViewController:UINavigationController.h:61)与 popViewController 的注释也印证栈是唯一事实来源。
  2. 懒加载语义:访问 view getter 时才触发 loadViewUIViewController.h:116)。isViewLoaded/viewIfLoaded 允许在不触发加载的前提下查询/访问视图(UIViewController.h:119,125)——这是内存管理的关键钩子:未加载视图的栈内 VC 只占用"控制器对象"本身的开销
  3. appearance 状态机是"通知"而非"状态查询":UIKit 未提供 isAppeared 之类的公开属性,App 只能靠回调感知。beginAppearanceTransition:endAppearanceTransitionUIViewController.h:464-465)是容器(含 UINavigationController 内部实现)驱动子 VC appearance 的底层机制。

2. 被覆盖页面的视图是否仍在 window 层级中

结论

push 之后,被覆盖 VC 的 view 会从 window/容器视图层级(superview 链)中移除,但 VC 对象仍强持有该 view 实例viewIfLoaded 非 nil,isViewLoaded == YES),view 及其子视图树整体留在内存中。pop 回来时 UIKit 把同一个 view 实例重新插回层级并回调 viewWillAppear

证据

头文件间接证据(可证)

  • UIViewController.h:185-186——viewWillDisappear: 注释 "Called when the view is about to be dismissed, covered, or otherwise hidden":被覆盖(covered)本身就是 disappearance 的一种,表明 UIKit 把"被盖住"与"消失"同等对待,而非保留在层级里仅被遮挡。
  • UIViewController.h:444-457——容器切换的通用语义注释 transitionFromViewController:toViewController:...
//  ... This method will add the toViewController's view to the superview of
//  the fromViewController's view and the fromViewController's view will be removed from its superview after the
//  transition completes. ...
//  ... it is important to ensure that the toViewController's view is added to the visible view hierarchy
//  while the fromViewController's view is removed.

UIViewController.h:447-449 为 "will add the toViewController's view ... and the fromViewController's view will be removed from its superview after the transition completes",UIViewController.h:454-455 为 "ensure that the toViewController's view is added to the visible view hierarchy while the fromViewController's view is removed"。)UINavigationController 的 push/pop 正是这种容器 transition:转场结束后旧 VC 的 view 被 removeFromSuperview

  • 反证(VC 仍持有 view):UIViewController.h:119 viewIfLoaded "Returns the view controller's view if loaded, nil if not"——view 的"loaded"状态独立于其是否在 window 层级中;第 1 节已证 VC 在栈中被强持有,而 view 属性是 strongUIViewController.h:116)。

官方文档佐证(URL,本环境不可达,未能在线验证)

  • Apple《View Controller Programming Guide for iOS》(已归档):
  • [镜像来源 URL] https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/index.html (该 URL 取自 GitHub 镜像仓库 Yannmm/View-Controller-Programming-Guide-for-iOS-Chinese-Translation 中 Apple 原文链接,镜像内容经 api.github.com 获取)该指南(及 Apple 文档站现行 UINavigationControllerUIViewController 页面)对导航栈的描述为:导航控制器在转场时把新栈顶的 view 装入内容区,旧栈顶 view 移出层级、VC 留栈。[推断——指南镜像章节(Overview/Presentations 等)中未逐字出现该句,此段综合 Apple 文档惯常表述与本节头文件注释;请以上方头文件证据为准]
  • Apple 文档站现行页面(稳定地址,[未在线验证 URL]):
  • https://developer.apple.com/documentation/uikit/uinavigationcontrollerhttps://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view (view 属性)

解读

"view 不在层级中"≠"view 被释放"。层级(window 的 view tree)只决定渲染与事件参与;引用计数决定存活。被覆盖的商品详情页:view.superview == nil(转场结束后)[推断——基于 UIViewController.h:448 注释语义,UIKit 闭源无法直接读实现],但 self.view 强引用使整棵子视图树 + 约束 + layer 树全部驻留内存。这也是 Instruments 里"off-screen views 仍占内存"的根源。


3. 历史机制 viewDidUnload / viewWillUnload:iOS 6 后系统不再自动卸载视图

结论

在 iOS 6(2012 年)之前,UIKit 会在低内存时自动把"不可见 VC"的 view 置 nil(触发 viewWillUnload/viewDidUnload)。iOS 6 起这套机制被废弃:系统不再自动卸载任何 VC 的视图;当前 iOS 26.4 SDK 中两个方法仍以 API_DEPRECATED 形式保留声明(未物理删除),但按注释"iOS 6.0 起内存警告默认不再清空视图"(见 didReceiveMemoryWarning 注释)。这是"框架不再自动回收不可见页面视图"的直接证据。

证据(头文件可证)

  • UIViewController.h:121-122(声明仍在,iOS 6 弃用):
- (void)viewWillUnload API_DEPRECATED("", ios(5.0, 6.0)) API_UNAVAILABLE(tvos) API_UNAVAILABLE(visionos, watchos);
- (void)viewDidUnload API_DEPRECATED("", ios(3.0, 6.0)) API_UNAVAILABLE(tvos) API_UNAVAILABLE(visionos, watchos); // Called after the view controller's view is released and set to nil. For example, a memory warning which causes the view to be purged. Not invoked as a result of -dealloc.

注释原文即历史行为的自述:"Called after the view controller's view is released and set to nil. For example, a memory warning which causes the view to be purged."

  • UIViewController.h:207(现行行为的最权威一行注释):
- (void)didReceiveMemoryWarning; // Called when the parent application receives a memory warning. On iOS 6.0 it will no longer clear the view by default.
  • 全 SDK 范围核验:对整个 UIKit.framework/Headers/ 目录 grep -rn -iE "viewDidUnload|viewWillUnload",唯一匹配即 UIViewController.h:121-122——再无任何其他 UIKit 公开 API 与"卸载视图"相关
  • 两个 SDK 的一致性:iPhoneOS.sdkiPhoneOS26.4.sdkUIViewController.h 第 121-122 行内容完全一致(symlink 同一文件,见篇首环境说明)。当前 SDK 没有物理删除这两个声明,但 API_DEPRECATED("", ios(3.0, 6.0)) 表明自 iOS 6 起编译器层面即警告,且运行时不再调用。[推断——"运行时不再调用"依据为本节注释与第 4 节文档,UIKit 闭源无法直接验证]

官方文档佐证(URL,本环境不可达,未能在线验证)

  • Apple《View Controller Programming Guide for iOS》历史版中 "Resource Management in View Controllers" 章节(iOS 6 版本起明确表述 viewWillUnload/viewDidUnload 弃用、系统不再因内存警告自动卸载视图):
  • [镜像来源 URL] https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/index.html (本环境核对的 2017 归档版镜像章节目录中已不包含该章节——该章在归档改版时被移除,这本身佐证"自动卸载"话题已从现行指南消失;其历史文本无法在本环境获取原文,[未验证])
  • Apple 文档站 UIViewController.didReceiveMemoryWarning 页面(现行文档与 UIViewController.h:207 注释同义):[未在线验证 URL] https://developer.apple.com/documentation/uikit/uiviewcontroller/1621363-didreceivememorywarning
  • 用户任务中提到的 QA1631:本环境无法验证该编号文档的存在与内容(developer.apple.com 404;GitHub 无镜像——repository 搜索 "QA1631" 结果为 0;raw.githubusercontent.com 超时)。不引用其内容,避免编造;请以 Apple 技术文库索引页自行核验:[未在线验证 URL] https://developer.apple.com/library/archive/qa/qa1631/_index.html

解读

历史脉络:iOS ≤5 时 didReceiveMemoryWarning 的默认实现会在 VC 不可见时释放其 view 并回调 viewDidUnload,开发者习惯在其中清空 outlet(当时 outlet 用 assign/unsafe_unretained)。iOS 6 起 ARC 普及 + 系统内存管理策略改变,Apple 明确"默认不再清空 view"(UIViewController.h:207 注释原文)。对商品详情页场景的意义:今天没有任何系统路径会因"页面被覆盖"或"内存警告"自动释放被覆盖页面的 view——内存压力完全交由 App 自己管理(pop、重置 view、清缓存)。


4. didReceiveMemoryWarning 通道:与可见性无关的广播

结论

内存警告有三层广播通道:UIApplication 层(代理方法 + 全局通知)→ 每个 UIViewController 实例的 didReceiveMemoryWarning 回调。广播按对象存活状态分发,与页面可见性无关:导航栈里被覆盖的所有商品详情页 VC 都会收到 didReceiveMemoryWarning

证据(头文件可证)

  • UIViewController.h:207
- (void)didReceiveMemoryWarning; // Called when the parent application receives a memory warning. On iOS 6.0 it will no longer clear the view by default.
  • UIApplication.h:389-390UIApplicationDelegate 协议中):
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application;      // try to clean up as much memory as possible. next step is to terminate app
- (void)applicationWillTerminate:(UIApplication *)application;

注释直白:"try to clean up as much memory as possible. next step is to terminate app"——警告之后就是杀进程(jetsam)。

  • UIApplication.h:556(全局通知常量):
UIKIT_EXTERN NSNotificationName const UIApplicationDidReceiveMemoryWarningNotification API_UNAVAILABLE(watchos) NS_SWIFT_NONISOLATED;
  • UIResponder.hgrep -n -E "didReceiveMemory|MemoryWarning" 无任何匹配——didReceiveMemoryWarning 只声明于 UIViewController.h,并不存在于 UIResponder 链中,即它不是"响应者可见性事件",而是内存管理事件。
  • 同组通知常量 UIApplication.h:551-556
UIKIT_EXTERN NSNotificationName const UIApplicationDidEnterBackgroundNotification       ...
UIKIT_EXTERN NSNotificationName const UIApplicationWillEnterForegroundNotification      ...
UIKIT_EXTERN NSNotificationName const UIApplicationDidBecomeActiveNotification          ...
UIKIT_EXTERN NSNotificationName const UIApplicationWillResignActiveNotification         ...
UIKIT_EXTERN NSNotificationName const UIApplicationDidReceiveMemoryWarningNotification  ...

解读

  1. 分发范围:UIViewController.h:207 注释 "Called when the parent application receives a memory warning" 表明这是 App 级事件到 VC 级回调的转发;Apple 文档对 didReceiveMemoryWarning 的现行表述是它会被发送到每一个 UIViewController 实例(不区分可见与否)[推断——注释只证明"父应用收到警告时调用","所有 VC 包括不可见的都收到"来自 Apple 文档惯常表述与本环境未验证 URL:https://developer.apple.com/documentation/uikit/uiviewcontroller/1621363-didreceivememorywarning ,未在线验证]。
  2. 默认实现自 iOS 6 起不再清 view(第 3 节),因此被覆盖页面收到警告后框架自身不做任何回收,App 应在覆盖中清理可重建资源(图片缓存、数据快照),切勿触碰 self.view(会触发懒加载,适得其反——依据 UIViewController.h:116 getter 语义,[推断])。

5. NSCache / NSPurgeableData / NSDiscardableContent:系统提供的"可丢弃"缓存原语

结论

Foundation 提供与"内存压力下可自动丢弃"配套的原语:NSCache(自动可淘汰缓存,不参与 KVO、线程安全、可在内存压力下逐出对象)、NSDiscardableContent 协议 + NSPurgeableData(内容级可丢弃数据块)。它们是官方指定的"替代 viewDidUnload 时代的内存弹性手段"——把可丢弃的内容放进这些容器,由系统在低内存时自动回收。

证据(头文件可证,Foundation.framework/Headers/

  • NSCache.h:12-28(类与关键 API):
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0))
@interface NSCache <KeyType, ObjectType> : NSObject
...
- (nullable ObjectType)objectForKey:(KeyType)key;
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
- (void)removeObjectForKey:(KeyType)key;
- (void)removeAllObjects;

@property NSUInteger totalCostLimit;	// limits are imprecise/not strict
@property NSUInteger countLimit;	// limits are imprecise/not strict
@property BOOL evictsObjectsWithDiscardedContent;
  • NSCache.h:32-35(淘汰回调):
@protocol NSCacheDelegate <NSObject>
@optional
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end
  • NSObject.h:66-74(可丢弃内容协议):
/***********	Discardable Content		***********/

@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess;
- (void)endContentAccess;
- (void)discardContentIfPossible;
- (BOOL)isContentDiscarded;
@end
  • NSObject.h:76-78(自动访问代理,无需手动 begin/end):
@interface NSObject (NSDiscardableContentProxy)
@property (readonly, retain) id autoContentAccessingProxy API_AVAILABLE(macos(10.6), ios(4.0), tvos(9.0));
@end
  • NSData.h:230-234(内建的 purgable 数据类型):
/****************	    Purgeable Data	****************/

API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0))
@interface NSPurgeableData : NSMutableData <NSDiscardableContent>
@end
  • 开源实现佐证(Apple 官方开源仓库,经 api.github.com contents API 验证并获取):apple/swift-corelibs-foundation · Sources/Foundation/NSCache.swift——其 setObject 淘汰循环按 totalCostLimit 计算超出量并回调 willEvictObject:(该文件 167-173 行附近:var purgeAmount = (totalCostLimit > 0) ? (_totalCost - totalCostLimit) : 0; while purgeAmount > 0 { ... delegate?.cache(... willEvictObject: ...) ... })。注意:swift-corelibs 是 Foundation 的开源移植实现,与 Darwin 版行为对齐但非同一份二进制[推断]。

UIImage 解码缓存(头文件部分可证 + 资料佐证)

  • UIImage.h:104
+ (nullable UIImage *)imageNamed:(NSString *)name;      // load from main bundle

头文件未注释缓存行为(对 UIImage.h 全文 grep -iE "cache|discardable|purgeable" 无匹配)。imageNamed: 具有系统级缓存、内容可丢弃的行为由 Apple 文档与 WWDC 资料说明[未在线验证——web_search 工具不可用]:WWDC 2018 Session 219 "Images and Graphics Best Practices" 讲解了 UIImage 解码位图的生命周期与内存压力下的处理([未在线验证 URL] https://developer.apple.com/videos/play/wwdc2018/219/ )。

  • UIImage.h:305(iOS 15 的显式解码 API,佐证"默认懒解码"语义):
- (void)prepareForDisplayWithCompletionHandler:(void (^)(UIImage *_Nullable))completionHandler NS_SWIFT_ASYNC_NAME(byPreparingForDisplay()) API_AVAILABLE(ios(15.0), tvos(15.0)) API_UNAVAILABLE(watchos);

解读

  1. NSCacheNSDictionary 的本质区别:可设 totalCostLimit/countLimit("imprecise/not strict",NSCache.h:26-27 注释)、可注册淘汰回调、对象可实现 NSDiscardableContent 让缓存直接丢弃其内容(evictsObjectsWithDiscardedContentNSCache.h:28)。系统内存紧张时 NSCache 会先行逐出对象——这正是"不可见页面数据"应有的归宿。
  2. 商品详情页的大图:用 imageNamed:/系统缓存意味着位图解码缓冲可能被系统在内存压力下丢弃重建;自管缓存则应把解码后的位图放入 NSCache 并设 cost 上限,把不可丢的原始数据放 NSPurgeableData。[推断——组合第 5 节原语的工程建议]

6. App 生命周期层面:整个 App 进入后台时只通知、不卸载任何视图

结论

App/Scene 退后台、失活等生命周期事件只触发回调与通知,框架不销毁、不卸载任何 VC 或 view;唯一涉及销毁的 scene 级回调是 sceneDidDisconnect:(针对 scene 本身)。前台运行期间被覆盖页面的问题(第 2-4 节)在后台化时不会得到任何自动改善。

证据(头文件可证)

UIApplicationDelegate(旧生命周期,iOS 26 起标记弃用、转向 Scene)——UIApplication.h:363-377

  • UIApplication.h:365
/// Tells the delegate that the application has become active
/// - Note: This method is not called if `UIScene` lifecycle has been adopted.
- (void)applicationDidBecomeActive:(UIApplication *)application API_DEPRECATED("Use UIScene lifecycle and sceneDidBecomeActive(_:) from UISceneDelegate or the UIApplication.didBecomeActiveNotification instead.", ios(2.0, 26.0), tvos(9.0, 26.0), visionos(1.0, 26.0)) API_UNAVAILABLE(watchos);
  • UIApplication.h:373
/// Tells the delegate that the application is now in the background
/// - Note: This method is not called if `UIScene` lifecycle has been adopted.
- (void)applicationDidEnterBackground:(UIApplication *)application API_AVAILABLE(ios(4.0)) API_DEPRECATED("Use UIScene lifecycle and sceneDidEnterBackground(_:) from UISceneDelegate or the UIApplication.didEnterBackgroundNotification instead.", ios(4.0, 26.0), visionos(1.0, 26.0)) API_UNAVAILABLE(watchos);
  • 同组:applicationWillResignActive:UIApplication.h:369)、applicationWillEnterForeground:UIApplication.h:377)。全部带 - Note: This method is not called if 'UIScene' lifecycle has been adopted. 与指向 scene 对应方法的弃用信息。
  • 对应通知常量:UIApplication.h:551-555DidEnterBackground 551、WillEnterForeground 552、DidBecomeActive 554、WillResignActive 555)。
  • 后台执行的配套 API(说明"进后台"只是状态变化 + 有限时间预算):UIApplication.h:139 backgroundTimeRemainingUIApplication.h:141-142 beginBackgroundTask...;快照控制 UIApplication.h:277 - (void)ignoreSnapshotOnNextApplicationLaunch

UISceneDelegate(现行生命周期)——UIScene.h:63-74

API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos) NS_SWIFT_UI_ACTOR
@protocol UISceneDelegate <NSObject>
@optional
#pragma mark Lifecycle State Transitioning
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions;   // 67 行
- (void)sceneDidDisconnect:(UIScene *)scene;                                                                                        // 68 行

- (void)sceneDidBecomeActive:(UIScene *)scene;                                                                                       // 70 行
- (void)sceneWillResignActive:(UIScene *)scene;                                                                                      // 71 行

- (void)sceneWillEnterForeground:(UIScene *)scene;                                                                                   // 73 行
- (void)sceneDidEnterBackground:(UIScene *)scene;                                                                                    // 74 行

(注:任务描述中的 sceneWillResignForeground 应为 sceneWillResignActive:UIScene.h:71sceneWillEnterForeground:/sceneDidEnterBackground:UIScene.h:73-74。)

  • iOS 26 新增 scene 销毁条件(UIScene.h:58-59):
// Conditions that help the system shell determine whether the scene should be destroyed for certain actions
@property (nonatomic, copy) NSSet<UISceneDestructionCondition *> *destructionConditions API_AVAILABLE(ios(26.0), tvos(26.0), visionos(26.0)) API_UNAVAILABLE(watchos) NS_REFINED_FOR_SWIFT;
  • Scene 清单相关:UIApplication.h 中无独立 UIApplicationSceneManifest 头;scene 清单由应用的 Info.plistUIApplicationSceneManifest 键)与 UISceneSession 体系承载,UIScene.h:67 scene:willConnectToSession:options: 为入口回调。

解读

  1. 所有后台/前台回调的注释(UIApplication.h:363-377)都只说 "Tells the delegate that ..."——纯通知语义。没有任何头文件文本表明进后台会卸载 view 或释放 VC。[推断-反向论证:若存在自动卸载,应有类似 viewDidUnload 的 API/注释,而全 SDK grep(第 3 节)证明不存在。]
  2. sceneDidDisconnect:UIScene.h:68)是唯一"断开"语义的回调,作用于 scene(整个窗口场景),不针对单个 VC,且通常发生在多 scene 场景资源回收时;它不是商品详情页栈的内存机制。
  3. 与 Android onStop/onDestroy 的显著差异:iOS 退后台时整棵 view 树原样驻留内存(挂起冻结由内核层 jetsam/freezer 决定,属框架外机制),因此"后台不占内存"的策略必须由 App 显式实现(如退后台时重置不可见页面缓存)[推断]。

7. iOS 17 / 18 / 26 SDK:有无新机制、可见性语义是否变化

结论

没有出现任何替代/补充 appearance 状态机或自动内存回收的新机制。 iOS 17 引入 viewIsAppearing:(细化 appear 时序:view 已入层级、已布局),iOS 26 引入属性更新三件套 setNeedsUpdateProperties / updateProperties / updatePropertiesIfNeeded 与 UIContentUnavailable 系列等;它们都是时序与配置细化,不改变"被覆盖页面仅收到 disappear 通知、视图常驻内存、无自动回收"的根本语义。

证据(头文件可证)

  • viewIsAppearing:——UIViewController.h:171-182(完整注释 + 声明):
/// Called when the view is becoming visible at the beginning of the appearance transition,
/// after it has been added to the hierarchy and been laid out by its superview. This method
/// is very similar to -viewWillAppear: and is always called shortly afterwards (so changes
/// made in either callback will be visible to the user at the same time), but unlike
/// -viewWillAppear:, at the time when -viewIsAppearing: is called all of the following are
/// valid for the view controller and its own view:
///    - View controller and view's trait collection
///    - View's superview chain and window
///    - View's geometry (e.g. frame/bounds, safe area insets, layout margins)
/// Choose this method instead of -viewWillAppear: by default, as it is a direct replacement
/// that provides equivalent or superior behavior in nearly all cases.
- (void)viewIsAppearing:(BOOL)animated API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);

注意两点:(a) 它解决的是 viewWillAppear: 时机过早("before it is added to the hierarchy",UIViewController.h:162-163)的问题;(b) 本 SDK 将其标注为 API_AVAILABLE(ios(13.0))——该方法于 2023 年(iOS 17 SDK / WWDC23)首次引入,Apple 随后把可用性回溯下调至 iOS 13(back-deployment),当前头文件以 ios(13.0) 标注[推断——ios(13.0) 标注为头文件可证;"iOS 17 SDK 首次引入后回溯"为知识背景,本环境无法在线验证 Apple 文档变更史]。

  • iOS 17 新 API(UIViewController.h):
  • UIViewController.h:700-714:空状态配置系列——contentUnavailableConfiguration(700)、contentUnavailableConfigurationState(705)、setNeedsUpdateContentUnavailableConfiguration(710)、updateContentUnavailableConfigurationUsingState:(714),均 API_AVAILABLE(ios(17.0)...)。与内存无关,属"无内容占位 UI"。UIViewController.h:755-762traitOverrides(758)与 updateTraitsIfNeeded(762),API_AVAILABLE(ios(17.0)...)——trait 更新时序细化。
  • iOS 18 新 API:UIViewController.h:284 preferredTransitionUIViewControllerTransitionAPI_AVAILABLE(ios(18.0)))——自定义 push/pop 转场,仍不触碰内存策略。
  • iOS 26 新 API(UIViewController.h):
/// Call to manually request a properties update for this view controller.                    // 190-191
/// Multiple requests may be coalesced into a single update alongside the next layout pass.
- (void)setNeedsUpdateProperties API_AVAILABLE(ios(26.0), ...)                                // 192
/// Override point for subclasses to update properties of this view controller or its view.  // 193-194
- (void)updateProperties NS_REQUIRES_SUPER API_AVAILABLE(ios(26.0), ...)                      // 195
- (void)updatePropertiesIfNeeded API_AVAILABLE(ios(26.0), ...)                                // 198

以及 childViewControllerForInterfaceOrientationLockUIViewController.h:725)、prefersInterfaceOrientationLocked(740)、setNeedsUpdateOfPrefersInterfaceOrientationLocked(743)——界面方向锁定;UIScene.h:59 destructionConditions(scene 销毁条件);UINavigationController.h:89-91 interactiveContentPopGestureRecognizerAPI_AVAILABLE(ios(26.0)...),全屏内容区 pop 手势)。

  • appearance 骨架未变:viewWillAppear/viewIsAppearing/viewDidAppear/viewWillDisappear/viewDidDisappearbeginAppearanceTransition/endAppearanceTransition 的声明位置和语义与 iOS 13-16 完全一致(对比第 1 节引用的 162-188、464-465 行);内存相关 API 在 iOS 17/18/26 增量中零新增UIViewController.hUIApplication.hgrep "API_AVAILABLE(ios(1[7-9]|2[0-6]" 逐条核对,无任何 memory/unload/appearance 状态类新符号)。

解读

新 SDK 的增量集中在:appear 时序补全(viewIsAppearing)、配置驱动 UI(contentUnavailable、properties update)、trait/转场自定义。可见/不可见的判定模型(appearance 回调 + 层级 membership)自 iOS 5 确立以来没有结构性变化;不可见页面的内存自 iOS 6 起"不自动回收"的结论在 iOS 26.4 SDK 依然成立。[推断——"自 iOS 5 确立"为版本史知识,头文件 __IPHONE_5_0 标注(UIViewController.h:464)可部分佐证]


8. 对商品详情页场景的意义

设导航栈深度为 N(1 个列表页/根 + N-1 个商品详情页,栈顶可见),基于以上证据的完整图景:

内存里有什么(push 后的稳态)

  • N 个 UIViewController 对象本身:被 UINavigationController.viewControllersUINavigationController.h:70)数组强持有,全部驻留。
  • N 棵 view 树:每个已 loadView 的页面,self.viewstrongUIViewController.h:116)持有整棵子视图层级 + Auto Layout 约束 + layer。被覆盖页面的 view 不在 window 层级(第 2 节)但完整驻留内存
  • 各 VC 强引用的模型数据、闭包、定时器、观察者等:随 VC 驻留。
  • 栈顶页面的图片解码位图:正在渲染,必然解码驻留;被覆盖页面经 imageNamed: 等系统缓存的位图:在系统图像缓存中,可被内存压力自动丢弃(第 5 节)。

UIKit 主动释放什么

  • 仅 pop 出栈的 VC(引用计数归零后连同其 view 树、模型释放,UINavigationController.h:63-65);popToRootViewController 一次性释放至只剩根(65 行)。
  • 转场动画结束后,旧栈顶 view 从 superview 移除(UIViewController.h:448 注释语义)——但仅解除层级关系,不释放内存。
  • 系统级图像缓存、NSCache 等原语内可丢弃内容在内存压力下的自动逐出(第 5 节)——这是唯一"框架级"的内存弹性,但只作用于这些容器内部,不作用于页面/视图本身。

完全不释放什么(框架零动作)

  • 被覆盖页面的 VC、view 树、约束、非 NSCache 持有的数据:didReceiveMemoryWarning 默认实现自 iOS 6 起不清 view(UIViewController.h:207)、无任何自动 unload 路径(第 3 节全 SDK grep 佐证)。
  • App 退后台时所有页面同样原样驻留(第 6 节,纯通知语义)。
  • UIKit 不管理你的业务缓存、磁盘缓存、模型快照。

需要 App 自己做什么

  1. 控制栈深与数据模型:商品详情页间 push 会线性累积内存;同款商品避免重复入栈(pushViewController: 注释即提示"已在栈中则无效",UINavigationController.h:61——可先用 viewControllers 查重替换);长栈场景考虑 setViewControllers: 重建浅栈。
  2. didReceiveMemoryWarning 里清可重建资源(该回调对不可见页面同样送达,第 4 节):清自管图片缓存/数据快照,不要触碰 self.view(getter 会触发懒加载,UIViewController.h:116)。
  3. 用系统原语而不是裸字典:图片解码缓存用 NSCache + totalCostLimit/cost(NSCache.h:21,26);可重建的大数据块用 NSPurgeableData/NSDiscardableContentNSData.h:233NSObject.h:68-74),交给系统在压力下自动丢弃。
  4. 可选的激进策略:对不可见且可重建的页面,App 可在 viewDidDisappear 后自行置空重型子视图/数据(模拟旧 viewDidUnload 语义)并在 viewWillAppear 重建——这是 App 层策略,框架不提供、也不阻止。
  5. 退后台时sceneDidEnterBackground:UIScene.h:74)按需释放纯 UI 缓存;不要指望框架做任何事(第 6 节)。

一句话:UIKit 的契约是"导航栈 = 强持有的内存栈"——框架负责通知你可见性变化与内存警告,回收与否完全是 App 的责任;自 iOS 6 移除自动卸载后,这一契约在 iOS 26.4 SDK 中没有变化。


附:证据文件索引(均为本机 iOS 26.4 SDK 实测行号)

文件关键行号内容
UIKit/UIViewController.h116-125view 属性懒加载、loadView/loadViewIfNeeded/viewIfLoaded/isViewLoaded、viewWillUnload/viewDidUnload 弃用声明
121-122viewWillUnload/viewDidUnload API_DEPRECATED(ios 6.0)
162-188viewWillAppear/viewIsAppearing/viewDidAppear/viewWillDisappear/viewDidDisappear 及注释
207didReceiveMemoryWarning("On iOS 6.0 it will no longer clear the view by default.")
428-465childViewControllers、transitionFromViewController(view 移除 superview 注释 447-455)、begin/endAppearanceTransition
700-714, 755-762iOS 17 新 API
284iOS 18 preferredTransition
190-198, 725-743iOS 26 新 API
UIKit/UINavigationController.h61-72push/pop/viewControllers 栈
118-120willShow/didShow 代理
UIKit/UIApplication.h363-390App 生命周期回调(365/369/373/377)与 applicationDidReceiveMemoryWarning(389)
551-556全局通知常量(含 556 内存警告通知)
UIKit/UIScene.h63-74UISceneDelegate 生命周期(67/68/70/71/73/74)
UIKit/UIResponder.h—(无匹配)无 didReceiveMemoryWarning 声明
UIKit/UIImage.h104imageNamed:(无缓存注释)
305prepareForDisplayWithCompletionHandler:(iOS 15)
Foundation/NSCache.h12-35NSCache 全部 API 与 delegate
Foundation/NSObject.h66-78NSDiscardableContent 协议与 autoContentAccessingProxy
Foundation/NSData.h230-234NSPurgeableData

(路径前缀:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/