iOS 13引入的UIScene体系彻底改变了应用的生命周期模型,一个应用进程可以同时承载多个窗口,每个窗口对应一个独立的UIScene和UISceneSession。这套机制在iPad多窗口、macOS Catalyst等场景下非常强大,但如果开发者仍然沿用旧的单窗口思维去写代码,就很容易出现窗口关闭后状态丢失、恢复时界面错乱、多窗口共享单例互相污染等问题。本文将从UISceneSession的生命周期入手,深入分析各个阶段的回调时机,并给出完整的状态恢复实现方案。

一、UISceneSession到底是什么
很多开发者会把UISceneSession和UIScene混为一谈,这是理解多窗口机制的第一道障碍。简单来说,UIScene是窗口的运行时实例,负责界面展示和事件处理;而UISceneSession是窗口的持久化描述对象,即使窗口被用户关闭,session对象在应用运行期间依然存在,它承载了窗口的配置信息、状态恢复数据以及用户活动记录。
这两者的生命周期是不同步的。用户在iPad上关闭一个窗口时,系统会断开对应的UIScene,但sceneSession并不会立即销毁,而是保留下来以便系统在未来某个时刻(例如用户从应用切换器中恢复窗口,或者触发Handoff)重新创建一个新的UIScene并连接到这个session。只有当系统调用AppDelegate的application(_:didDiscardSceneSessions:)时,才意味着这些session被彻底丢弃,此时必须清理对应的持久化数据。
理解这一点的关键在于:系统终止场景时并不等于丢弃窗口。当内存紧张,系统可能主动断开后台场景的连接,但session还在,用户再次激活时系统会重新创建UIScene并带着之前的恢复数据回调scene(_:willConnectTo:options:)。如果你的状态保存逻辑依赖于scene销毁回调,那就大错特错了,因为销毁回调可能永远不会触发,或者触发时已经来不及保存任何东西。
二、UISceneSession生命周期的四个关键阶段
阶段一是配置阶段。当系统准备创建一个新窗口时,会先询问AppDelegate,调用configurationForConnectingSceneSession方法。这是定制每个窗口身份的黄金时机,你可以根据session的role(前台主场景、CarPlay场景等)返回不同的配置,也可以给session的userInfo写入自定义标识,比如这个窗口打开的是哪个文档、哪个用户账户的数据。
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// 根据场景角色返回不同配置
if connectingSceneSession.role == .windowApplication {
let config = UISceneConfiguration(name: "MainScene",
sessionRole: .windowApplication)
config.delegateClass = SceneDelegate.self
return config
}
// 其他角色返回默认配置
return UISceneConfiguration(name: "Default",
sessionRole: connectingSceneSession.role)
}
阶段二是连接阶段。系统创建好UIScene后会回调SceneDelegate的scene(_:willConnectTo:options:),这是恢复状态的入口。options参数中可能携带userActivities(来自Handoff或Spotlight)、notificationResponse(来自通知点击)等启动上下文,你需要在这里判断是全新启动还是状态恢复,分别走不同的初始化路径。
阶段三是断开阶段。用户关闭窗口或系统回收资源时,系统回调sceneDidDisconnect(_:)。苹果官方文档明确指出:这个方法被调用后,场景可能在之后被重新连接,所以你应该在这里释放昂贵资源(如视频解码器、大图缓存),但不要删除持久化数据,也不要把用户的文档状态清空。很多崩溃和数据丢失问题,都是开发者在这个回调里误以为窗口彻底销毁而做了破坏性操作导致的。
阶段四是丢弃阶段。当用户从App Switcher上划强制关闭窗口,或系统主动清理时,AppDelegate的application(_:didDiscardSceneSessions:)被调用。注意这个回调可能在应用未运行时触发,系统会先启动应用再回调。此时才是真正清理该窗口对应持久化数据的地方,比如删除该文档窗口对应的临时编辑缓存。
三、状态恢复的三种实现策略对比
第一种是基于restorationIdentifier的UIKit状态恢复。给UIWindow、UIViewController、UIView设置restorationIdentifier,系统会在场景断开前自动归档视图层级状态,重新连接时通过UIStateRestorationCoder恢复。这种方式适合恢复导航栈、滚动位置等纯UI状态,实现简单,但恢复的粒度受限于UIKit的支持范围,自定义数据的传递不够灵活。
第二种是基于NSUserActivity的活动恢复。每个场景维护一个userActivity对象,在scene(_:continueUserActivity:)中重建上下文。这种方式的优势是同一套代码可以同时服务Handoff、Spotlight搜索、Siri建议等系统功能,做到一次实现多处受益。它的成本是需要你设计好活动数据的编码格式,并在视图控制器切换时及时更新userInfo和requiredUserInfoKeys。
第三种是手动持久化方案。利用SceneDelegate的scene(_:encodeRestorableStateWith:)和scene(_:decodeRestorableStateWith:)这对回调,把自定义数据写入coder,或者干脆用session的userInfo加上磁盘文件组合存储。这种方式控制力最强,适合文档类应用。下面是一个综合示例:
// SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// 检查是否有待恢复的用户活动(如系统启动恢复)
if let activity = connectionOptions.userActivities.first {
restoreFrom(activity: activity)
} else if session.stateRestorationActivity != nil {
// 从session保存的活动恢复
restoreFrom(activity: session.stateRestorationActivity!)
} else {
// 全新窗口,走默认初始化
}
window?.makeKeyAndVisible()
}
// 场景进入后台或断开前更新活动状态
func sceneDidEnterBackground(_ scene: UIScene) {
let activity = NSUserActivity(activityType: "com.ipipp.document.open")
activity.userInfo = ["documentID": currentDocumentID]
scene.userActivity = activity
}
// 使用系统提供的状态编解码回调保存自定义数据
func scene(_ scene: UIScene, encodeRestorableStateWith coder: NSCoder) {
coder.encode(currentDocumentID, forKey: "docID")
coder.encode(scrollOffset, forKey: "scroll")
}
func scene(_ scene: UIScene, decodeRestorableStateWith coder: NSCoder) {
currentDocumentID = coder.decodeObject(forKey: "docID") as? String
scrollOffset = coder.decodeDouble(forKey: "scroll")
}
四、多窗口开发中的常见坑与应对方案
第一个坑是单例数据污染。多窗口意味着多个SceneDelegate同时存在,如果登录状态、当前文档、购物车等数据都塞在全局单例里,两个窗口就会互相覆盖。正确的做法是把窗口级状态收敛到SceneDelegate或与之绑定的Context对象中,全局单例只保留真正全局的数据(如主题配置、服务层)。
第二个坑是通知与KVO未清理。每个场景的视图控制器都在监听同一份通知,窗口关闭后监听器还在,导致向已断开的场景发送更新,轻则内存泄漏,重则崩溃。务必在sceneDidDisconnect相关的清理路径中移除观察者,或者使用基于block的通知API并持有token统一销毁。
第三个坑是requestSceneSessionActivation的误用。在代码中主动打开新窗口时,如果传入已存在的session,系统可能只是激活旧窗口而不是新建。正确做法是为新窗口调用UISceneSession.activationConditions合理设置条件,或为每个逻辑窗口维护稳定的持久标识,在激活前先查询UIApplication.shared.connectedScenes避免重复创建。
// 主动打开新窗口前先查重
let existing = UIApplication.shared.connectedScenes
.compactMap { $0.delegate as? SceneDelegate }
.first { $0.currentDocumentID == targetID }
if let scene = existing?.window?.windowScene {
// 已有窗口打开该文档,直接激活它
UIApplication.shared.requestSceneSessionActivation(
scene.session, userActivity: nil, options: nil, errorHandler: nil)
} else {
// 没有对应窗口,创建新活动让系统开新窗口
let activity = NSUserActivity(activityType: "com.ipipp.document.open")
activity.userInfo = ["documentID": targetID]
UIApplication.shared.requestSceneSessionActivation(
nil, userActivity: activity, options: nil, errorHandler: nil)
}
总结来看,管理好多窗口的核心是把思维从单窗口生命周期切换到session驱动的模型:UISceneSession是窗口的身份证,UIScene只是它的临时躯壳。状态保存在断开前完成,数据清理只在discard回调中执行,窗口级状态不要放进全局单例,状态恢复优先选择NSUserActivity以便与系统集成能力打通。遵循这些原则,多窗口应用的生命周期管理就能做到井然有序。
UISceneSession多窗口开发状态恢复修改时间:2026-09-01 08:55:03