iOS 应用冷启动时,系统会先显示 Launch Screen,再进入主界面。很多项目在启动页消失后会出现一次明显的白屏或黑屏闪烁,随后首页才慢慢渲染出来。这个问题的根源往往不是启动图本身,而是 UIWindow 的根视图控制器设置时机过晚,或者直接替换 rootViewController 时没有任何过渡处理。提前把首页控制器准备好,并在合适的生命周期节点完成窗口初始化,再配合一个轻量级的淡入淡出动画,可以让启动过程看起来更连贯。

为什么启动页和首页之间会出现闪烁
从冷启动流程来看,iOS 会先读取 Info.plist 中的 UILaunchStoryboardName 配置,加载对应的 Launch Screen 文件。这个过程由系统负责,开发者无法在启动页上执行任何业务代码。Launch Screen 消失后,系统等待应用完成启动并展示第一个可交互界面。此时如果 UIWindow 还没有被设置为可见状态,或者根视图控制器尚未赋值,屏幕就会保持空白,直到首页创建完成并渲染出第一帧,用户就会看到闪一下。
另一个常见原因是应用开发者习惯在首页控制器的 viewDidLoad 中执行网络请求、数据库初始化和复杂布局计算。系统在启动页结束时会立即移除 Launch Screen,如果此时首页视图还没有完成加载,就会出现一段空白。解决办法不是在首页里推迟加载,而是让 UIWindow 尽早拥有一个可显示的根控制器,并把耗时操作移出主线程或延后执行。
还有一类闪烁发生在手动切换根视图控制器时。比如登录成功后从登录页切到首页,或者从广告页切到主界面,代码里直接写 window.rootViewController = homeVC。这种赋值方式没有任何动画,系统会在下一个 RunLoop 周期更新视图层级,中间可能插入一帧旧界面或空白内容,视觉上就会跳变。给这类切换加上 UIView.transition 动画,或者在切换前放置一张快照图,可以有效消除断层感。
提前设置根视图控制器的正确做法
在 AppDelegate 的 application(_:didFinishLaunchingWithOptions:) 方法中,应该先创建 UIWindow,再创建首页控制器,最后调用 makeKeyAndVisible。这个顺序能保证 Launch Screen 结束后窗口已经有一个确定的根控制器,不会出现悬空的空白窗口。下面是一段 Swift 示例代码:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let homeVC = HomeViewController()
window?.rootViewController = homeVC
window?.makeKeyAndVisible()
return true
}
这段代码的逻辑并不复杂,但很多项目因为使用了 Storyboard 自动配置,忽略了手动设置窗口的重要性。Storyboard 的入口配置通常会在系统完成启动后创建窗口并加载初始控制器,这个过程虽然自动,但开发者无法精确控制根控制器创建的时间点。如果首页控制器初始化非常耗时,自动加载同样可能造成启动页结束后的白屏。因此对于启动体验要求较高的应用,建议关闭 Storyboard 的自动入口,改成在代码中主动创建窗口和根控制器。
Objective-C 项目也可以采用同样的思路。需要注意在 iOS 13 之后引入了 SceneDelegate,如果应用启用了 Scene 生命周期,窗口的创建位置会从 AppDelegate 移到 SceneDelegate 的 scene(_:willConnectTo:options:) 方法中。示例如下:
- (void)scene:(UIScene *)scene
willConnectToSession:(UISceneSession *)session
options:(UISceneConnectionOptions *)connectionOptions {
if (scene) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
HomeViewController *homeVC = [[HomeViewController alloc] init];
self.window.rootViewController = homeVC;
[self.window makeKeyAndVisible];
}
}
提前设置根控制器并不代表首页一定要立刻显示所有内容。对于依赖异步数据的页面,可以先创建一个轻量级的容器控制器或带占位状态的首页,等数据返回后再刷新子视图。这样窗口有内容可渲染,启动页消失时不会出现空白,同时又不阻塞主线程。首页内部的骨架屏、加载指示器都可以在这个阶段绘制完成,让用户感知到应用已经启动。
利用 Launch Screen 快照实现平滑过渡
仅仅提前设置根控制器有时还不足以完全消除视觉跳变。如果启动页和首页在设计上差异较大,比如背景色不同、导航栏样式不一致,切换瞬间仍然会有一个明显的变化。更自然的做法是在首页显示前先创建一张与启动页相同的快照视图,覆盖在首页之上,然后通过透明度动画让快照淡出,底下的首页逐渐显露出来。
快照的来源有两种:一种是直接读取 Launch Screen 对应的 Storyboard 并实例化第一个视图控制器,把它加到当前窗口中;另一种是用一张与启动图相同的图片生成 UIImageView 作为覆盖层。前者能保证布局完全一致,后者实现更简单,适合启动页只有静态图片的项目。下面是一段基于 UILaunchStoryboardName 配置加载启动页快照的 Swift 代码:
func addLaunchScreenSnapshot() {
guard let launchStoryboardName = Bundle.main.object(forInfoDictionaryKey: "UILaunchStoryboardName") as? String,
let launchVC = UIStoryboard(name: launchStoryboardName, bundle: nil).instantiateInitialViewController(),
let snapshot = launchVC.view else {
return
}
snapshot.frame = window?.bounds ?? UIScreen.main.bounds
window?.addSubview(snapshot)
UIView.animate(withDuration: 0.3,
delay: 0.0,
options: .curveEaseOut,
animations: {
snapshot.alpha = 0.0
},
completion: { _ in
snapshot.removeFromSuperview()
})
}
这段代码在首页根控制器已经设置完成之后调用。快照被添加到窗口最上层,启动页消失后用户看到的是完全一样的画面,随后快照逐渐透明,底下的首页平滑露出。动画时长一般控制在 0.2 到 0.4 秒之间,过短显得仓促,过长则会拖慢启动感受。如果首页数据加载很快,甚至可以不用动画,直接移除快照即可。
如果首页和启动页在布局上并不完全相同,还可以使用 UIView.transition 做整体转场。比如登录成功后要切换根控制器,可以用下面的代码实现淡入淡出效果:
guard let window = window else { return }
let homeVC = HomeViewController()
UIView.transition(with: window,
duration: 0.35,
options: .transitionCrossDissolve,
animations: {
window.rootViewController = homeVC
},
completion: nil)
这种方式的优势是代码简单,不需要手动管理快照视图。但它只对根控制器切换有效,且动画过程中旧控制器和新控制器都会参与渲染,对于内存紧张的设备可能会产生短暂的性能压力。快照方案则更轻量,适合仅为了消除启动页闪烁的场景。两者可以结合使用:启动阶段用快照淡出,业务中的根控制器切换用 UIView.transition。
常见误区与性能注意事项
很多开发者会把启动页闪烁归咎于 Launch Screen 的图片加载速度,于是反复压缩图片、更换格式,甚至用纯色背景替代设计图。实际上 Launch Screen 只支持静态资源,系统会在启动阶段提前加载并在显示前完成解码,图片本身很少成为闪烁主因。真正需要关注的是根控制器创建时机、首页首屏渲染耗时,以及是否有额外的 viewDidAppear 中的重绘操作。
另外,在首页控制器还没有完全加载时,不要急于执行数据库迁移、日志初始化或第三方 SDK 配置。这些任务会挤占主线程,导致首帧渲染延迟。可以把不紧急的初始化工作放到后台队列,或者延迟到 viewDidAppear 之后的空闲时间。对于必须阻塞主线程的配置,也要尽量拆分成多个小任务,给 RunLoop 留出渲染机会。
还有一点容易忽略:如果使用了 Scene 生命周期,AppDelegate 中不再自动创建窗口,但仍可以通过 UIApplication.shared.connectedScenes 获取当前场景并配置窗口。不过更推荐在 SceneDelegate 中集中处理,避免多个窗口场景下的状态混乱。无论采用哪种生命周期,保持窗口和根控制器创建顺序清晰,是解决启动体验问题的基础。
iOS启动页Launch Screen根视图控制器修改时间:2026-08-21 22:23:59