背景
之前通过用户反馈平台发现有部分用户反馈我们的App 启动时间较长,同时从崩溃数据监控发现部分用户在App启动后崩溃, 通过日志排查我们发现崩溃的原因是App启动超时被系统kill掉了,于是我们开始分析项目中导致启动时间变长的原因,并对启动时间进行优化。现状分析
当用户按下home键的时候,iOS的App并不会马上被kill掉,还会继续存活若干时间。 理想情况下,用户点击App的图标再次回来的时候,App几乎不需要做什么,就可以还原到退出前的状态,继续为用户服务。 这种持续存活的情况下启动App,我们称为热启动,相对而言冷启动就是App被kill掉以后一切从头开始启动的过程。 我们这里只讨论App冷启动的情况。 对于冷启动来说,启动时间是指从用户点击 APP 那一刻开始到用户看到第一个界面这中间的时间。APP启动时间优化原则
对于启动时间优化其实就是遵循一个原则: 尽早让用户看到首页内容。 根据这一原则将一些非必须的操作尽量往后移,通常是移到首页显示后执行,同时对于无法往后移的操作,尽可能不占用主线程,主线程尽量只做 UI 操作,将其他操作移到子线程。APP启动过程
iOS应用的启动可分为pre-main阶段和main()阶段,其中系统做的事情依次是: pre-main阶段 解析Info.plist 1.加载相关信息,例如如闪屏 2.沙箱建立、权限检查 Mach-O加载 1.如果是胖二进制文件,寻找合适当前CPU类别的部分 2.加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法) 3.定位内部、外部指针引用,例如字符串、函数等 4.执行声明为__attribute__((constructor))的C函数 5.加载类扩展(Category)中的方法 6.C++静态对象加载、调用ObjC的 +load 函数 main()阶段1.调用main() 2.调用UIApplicationMain() 3.调用applicationWillFinishLaunching
启动耗时的测量
在进行优化之前,我们首先应该能测量各阶段的耗时。 pre-main阶段测量在不越狱的情况下,以往很难精确的测量在main()函数之前的启动耗时,因而我们也往往容易忽略掉这部分数据。 小型App确实不需要太过关注这部分。 但如果是大型App(自定义的动态库超过50个、或编译结果二进制文件超过30MB),而我们的App的自定义动态库较多,且二进制文件接近60M,这部分耗时将会变得突出,需要我们优化pre-main阶段的时间。 所幸,苹果已经在Xcode中加入这部分的支持。 具体设置方法如下: 在Xcode 中 Edit scheme -> Run ->Auguments 将环境变量DYLD_PRINT_STATISTICS 设为1
对于main()阶段,主要是测量main()函数开始执行到didFinishLaunchingWithOptions执行结束的耗时,就需要自己插入代码到工程中了。 先在main()函数里记录当前时间,再在AppDelegate.m文件中的didFinishLaunchingWithOptions函数的最后获取一下当前时间,这两个的时间的差值即是main()阶段运行耗时。
优化思路及明确优化方向
在通过以上方法测量出各阶段的占用时间,从数据上分析哪个阶段占用的时间多,从而指导我们明确优化的方向。 是什么影响了我们的APP的启动时间? 切忌挖空心思的研究优化main()函数调用之前的占用时间,反而忽略了-applicationDidFinishLaunching:函数之后那一堆堆臃肿的网络请求以及业务流程。 所以我们先来看看-applicationDidFinishLaunching:函数之后,我们的APP都做了哪些事情: 首先会初始化window,加载tabbar,加载首页controller以及数据,可能我们还有一个loading广告页,还有各种各样的业务需求,网络请求。 所以这些都是需要去排查的地方,可以尝试通过添加打印时间戳的方式,来测量每个阶段的耗时情况。 我们根据排查结果来明确造成启动缓慢的原因。pre-main阶段加载过程
要对pre-main阶段的耗时做优化,需要再学习下dyld加载的过程,dyld的加载主要分为4步: 加载dylibs
这一阶段dyld会分析应用依赖的dylib(大部分是iOS系统的),找到其mach-o文件,打开和读取这些文件并验证其有效性,接着会找到代码签名注册到内核,最后对dylib的每一个segment调用mmap()。
Rebase/Bind
这一阶段系统主要注册 Objc 类。 所以,指针数量越少越好。 Objc setupOC的runtime需要维护一张类名与类的方法列表的全局表。 dyld做了如下操作: 对所有声明过的OC类,将其注册到这个全局表中(class registration);将category的方法插入到类的方法列表中(category registration);检查每个selector的唯一性(selectoruniquing)在这一步倒没什么优化可做的,Rebase/Bind阶段优化好了,这一步的耗时也会减少。 Initializers
这一阶段,dyld 开始运行程序的初始化函数,调用每个 Objc 类和分类的 +load 方法,调用 C/C++ 中的构造器函数。 initializer阶段执行完后,dyld 开始调用 main() 函数。
pre-main阶段耗时优化
通过以上的pre-main阶段过程的分析我们得到如下结论: 动态库加载越多,启动越慢; ObjC类越多,启动越慢; C的constructor函数越多,启动越慢; C++静态对象越多,启动越慢; ObjC的+load越多,启动越慢。 实验证明,在ObjC类的数目一样多的情况下,需要加载的动态库越多,App启动就越慢。 同样的,在动态库一样多的情况下,ObjC的类越多,App的启动也越慢。 需要加载的动态库从1个上升到10个的时候,用户几乎感知不到任何分别,但从10个上升到100个的时候就会变得十分明显。 同理,100个类和1000个类,可能也很难察觉得出,但1000个类和10000个类的分别就开始明显起来。 因此我们建议在pre-main阶段的优化如下: 1、尽量不要写__attribute__((constructor))的C函数,也尽量不要用到C++的静态对象; 至于ObjC的+load方法,似乎大家已经习惯不用它了。 任何情况下,能用dispatch_once()来完成的,就尽量不要用到以上的方法。 2、由于苹果对app二进制代码大小的限制,我们将app中很多基础控件和基础功能的静态库转成了很多个动态库,这导致了加载动态库时间耗费较长,为此我们将已有的多个动态库合并为一个动态库,减少加载dylib的时间。 3、排查清理项目中未使用到的类库以及Framework。 4、清理项目中无用的类,删减没有被调用到或者已经废弃的方法。 5、减少ObjC类(class)、方法(selector)、分类(category)的数量。 6、删减一些无用的静态变量。 7、检查 +load 方法,不要在+load方法里做耗时操作,尽量把事情推迟到 +initiailize 方法里执行, 也就是到使用时才加载。 8、减少C++静态全局变量的个数。main()阶段的耗时优化