一. 背景
我们从启动统计数据上发现,在iOS15
及以上相关系统,经常会出现启动耗时几十秒的情况,通常是load
方法的加载就已经达到几十秒了,而且都是相对性能较好的机型。
由于苹果系统存在watchdog
机制,针对App启动的启动,watchdog
的时间限制是20s
,如果启动超过20s
,就会被watchdog
强制杀死进程,退出应用。
但这里统计出来启动时长,很明显很大部分已经超过20s
了,但是依然正常启动,说明要不就启动统计方法有问题,要不就存在一些异常情况,出现数据异常。
二. 分析
针对出现的机型主要是在iOS15
及以上系统,因此依据这个线索进行排查。终于找到了关于prewarm
的相关资料,具体详见: 【About the app launch sequence】
1. prewarm 机制
Apple
在iOS 15
中引入了prewarm
(预热)机制,系统可能会根据设备的情况,比如当设备内存和磁盘空间充足以及用户启动该App
的时间习惯等,提前帮你预热(prewarm) 你的 App
。
也就是提前启动不再运行的 App 进程,以减少用户手动启动 App
等待的时间。prewarm
执行一个 App
的启动序列直到(但不包括)当 main()
调用 UIApplicationMain
。也就是prewarm
帮忙提前完成了pre-main
之前的所有操作。
这为系统提供了一个机会来构建和缓存它需要的任何低层结构,以期待一个完整的启动。也就是说,prewarm
机制可以减少启动时间,我们甚至可以在 load
方法中做一些资源的预加载。
2. 原因
因为iOS15
及以后的系统,苹果添加这种预热的操作:prewarm
,系统提前执行了启动的pre-main
之前的所有操作,然后暂停,等到用户真正启动的时候,才会继续执行mian
之后的相关启动操作,而我们App
启动耗时统计是从cocoapods
排序的第一个库的load
时间开始,然后到首页的viewDidAppear
结束。
因此如果是该App
被预热了,也就是库的load
等方法先执行,这时候统计起始时间已经标记,然后暂停,暂停这段时间也被算进去,导致启动耗时尤其是load
方法的加载出现几十秒这种情况,统计数据出现了偏差,之所以只有统计到几十秒的,而没有几分钟或者几个小时,是因为统计库做了过滤,超过1
分钟,就直接丢弃这次统计。
三. 解决方法
我们可以根据如下方法判断此次启动是否为预热prewarm
启动:
func isPrewarmLaunch() -> Bool {
let systemVersion = UIDevice.current.systemVersion.toFloat() ?? 0.0
if systemVersion >= 15.0 {
let environment = ProcessInfo.processInfo.environment
for key in environment.allKeys() {
if key.contains(substring: "prewarm") {
return true
}
}
}
return false
}
因此决定启动组件里面添加判断,判断此次启动是否为预热prewarm
启动,如果此次启动为预热prewarm
启动,则统计的起始点用main
函数的开始执行的时间作为起点,如果不是预热启动,则依然使用cocoapods
排序的第一个库的load
的加载时间作为起点。
不过这里需要对你的App
是否接入UIScene
做区分判断:
对于接入 UIScene
的 App
:
application:didFinishLaunchingWithOptions:
可能会被调用(并不总是发生)scene:willConnectToSession:options:
未被调用。事实上,SceneDelegate
直到App
打开才创建。
对于没有接入 UIScene
的 App
:
-
application:didFinishLaunchingWithOptions:
不会调用。
因此,如果 App
接入了 UIScene
,又没做特殊处理的话,会因为 prewarm
机制的 bug
而导致 iOS 15
用户日活异常增多。
因为我们业务线相关的App
没有接入UIScene
,因此我们这里直接采取上面的优化方案。