性能
启动时间要拆开看
pre-main、main 到首帧、首帧之后,这三段不是同一个问题,优化手段也完全不同。
把启动说成一个毫秒数,后面几乎没法改。用户感觉到的是点图标到能点第一下。这条时间里至少有三段:进 main 之前、main 到第一帧提交、第一帧之后你还在主线程上补的初始化。
进 main 之前
这一段主要是 dyld 加载镜像、绑定符号、跑静态初始化和 Objective-C 的 +load。Swift 的全局 let、静态属性,只要在加载时被求值,也会算在这里。+load 比 +initialize 更伤:前者在 main 之前无条件执行,后者要等到类第一次收到消息。
先确认时间是不是花在这里。调试时可以打开 DYLD_PRINT_STATISTICS 和 DYLD_PRINT_STATISTICS_DETAILS,看 total time、total images、还有每个镜像的 initializer。发布包用 MetricKit 的 histogrammedTimeToFirstDraw,或者 Instruments 的 App Launch 模板。不要拿 Debug + 调试器附着的数字做结论,那个数字通常差一截。
- 第三方 SDK 的 +load 里做注册、读文件、swizzle,是这一段最常见的来源。
- 动态库越多,dyld 要处理的镜像越多。能静态链接并且确实会被用到的,不要为了“模块清爽”拆成一堆启动时都要加载的动态库。
- 预热(pre-warm)会让系统提前把进程拉起来。实验室里两次启动差很多,先分清是不是冷启动。
main 到第一帧
UIApplicationMain 之后,场景连上、根控制器装好、第一帧提交,这段才是界面出现。故事板或根控制器的 init、viewDidLoad 里同步读磁盘、解大 JSON、创建全部子页面,都会顶在第一帧前面。
原则很简单:第一帧只做能把界面摆出来的事。统计、推送、定位、远程配置、数据库迁移,都排到首帧之后。需要迁移时,界面上可以是一个明确的等待态,而不是让系统启动图多挂两秒。
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
window?.rootViewController = RootViewController()
window?.makeKeyAndVisible()
// 首帧提交后再做,不占用 time-to-first-draw
DispatchQueue.main.async {
AppServices.startAfterFirstFrame()
}
return true
}
首帧之后仍会感觉慢
第一帧出来了,但主线程接着做同步布局、解码、建表,手指点下去要等。这已经不是启动指标,是首屏可交互。优化时把“能看见”和“能用”分成两个 signpost,别用一个总时间掩盖。
改的时候一次只动一处:先拿掉一个 +load,或把一个 SDK 挪出 didFinishLaunching,再量。一起改五个库,数字变了也不知道是谁。