应用启动流程
iOS 应用的启动可分为 pre-main 阶段和 main()阶段, 其中系统做的事情依次是:
pre-main 阶段
1.1. 加载应用的可执行文件
1.2. 加载动态链接库加载器 dyld(dynamic loader)
1.3. dyld 递归加载应用所有依赖的 dylib(dynamic library 动态链接库)
main()阶段
2.1. dyld 调用 main()
2.2. 调用 UIApplicationMain()
2.3. 调用 applicationWillFinishLaunching
2.4. 调用 didFinishLaunchingWithOptions
启动耗时的测量
在进行优化之前, 我们首先应该能测量各阶段的耗时.
1. pre-main 阶段
对于 pre-main 阶段, Apple 提供了一种测量方法, 在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1 . 之后控制台会输出类似内容:
- Total pre-main time: 228.41 milliseconds (100.0%)
- dylib loading time: 82.35 milliseconds (36.0%)
- rebase/binding time: 6.12 milliseconds (2.6%)
- ObjC setup time: 7.82 milliseconds (3.4%)
- initializer time: 132.02 milliseconds (57.8%)
- slowest intializers :
- libSystem.B.dylib : 122.07 milliseconds (53.4%)
- CoreFoundation : 5.59 milliseconds (2.4%)
这样我们可以清晰的看到每个耗时了.
2.main()阶段
mian()阶段主要是测量 mian()函数开始执行到 didFinishLaunchingWithOptions 执行结束的时间, 我们直接插入代码就可以了.
- CFAbsoluteTime StartTime;
- int main(int argc, char * argv[]) {
- StartTime = CFAbsoluteTimeGetCurrent();
再在 AppDelegate.m 文件中用 extern 声明全局变量 StartTime
extern CFAbsoluteTime StartTime;
最后在 didFinishLaunchingWithOptions 里, 再获取一下当前时间, 与 StartTime 的差值即是 main()阶段运行耗时.
double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);
改善启动时间
pre-main 阶段
在这一阶段, 我们能做的主要是优化 dylib
加载 Dylib
之前提到过加载系统的 dylib 很快, 因为有优化. 但加载内嵌 (embedded) 的 dylib 文件很占时间, 所以尽可能把多个内嵌 dylib 合并成一个来加载, 或者使用 static archive.
使用 dlopen() 来在运行时懒加载是不建议的, 这么做可能会带来一些问题, 并且总的开销更大.
Rebase/Binding
之前提过 Rebaing 消耗了大量时间在 I/O 上, 而在之后的 Binding 就不怎么需要 I/O 了, 而是将时间耗费在计算上. 所以这两个步骤的耗时是混在一起的.
之前说过可以从查看 __DATA 段中需要修正 (fix-up) 的指针, 所以减少指针数量才会减少这部分工作的耗时. 对于 ObjC 来说就是减少 Class,selector 和 category 这些元数据的数量. 从编码原则和设计模式之类的理论都会鼓励大家多写精致短小的类和方法, 并将每部分方法独立出一个类别, 其实这会增加启动时间. 对于 C++ 来说需要减少虚方法, 因为虚方法会创建 vtable, 这也会在 __DATA 段中创建结构. 虽然 C++ 虚方法对启动耗时的增加要比 ObjC 元数据要少, 但依然不可忽视.
Objc setup
大部分 ObjC 初始化工作已经在 Rebase/Bind 阶段做完了, 这一步 dyld 会注册所有声明过的 ObjC 类, 将分类插入到类的方法列表里, 再检查每个 selector 的唯一性.
在这一步倒没什么优化可做的, Rebase/Bind 阶段优化好了, 这一步的耗时也会减少.
Initializers
到了这一阶段, dyld 开始运行程序的初始化函数, 调用每个 Objc 类和分类的 + load 方法, 调用 C/C++ 中的构造器函数 (用 attribute((constructor)) 修饰的函数), 和创建非基本类型的 C++ 静态全局变量. Initializers 阶段执行完后, dyld 开始调用 main()函数.
在这一步, 我们可以做的优化有:
少在类的 + load 方法里做事情, 尽量把这些事情推迟到 + initiailize
减少构造器函数个数, 在构造器函数里少做些事情
减少 C++ 静态全局变量的个数
main()阶段的优化
这一阶段的优化主要是减少 didFinishLaunchingWithOptions 方法里的工作, 在 didFinishLaunchingWithOptions 方法里, 我们会创建应用的 Windows, 指定其 rootViewController, 调用 Windows 的 makeKeyAndVisible 方法让其可见. 由于业务需要, 我们会初始化各个二方 / 三方库, 设置系统 UI 风格, 检查是否需要显示引导页, 是否需要登录, 是否有新版本等, 由于历史原因, 这里的代码容易变得比较庞大, 启动耗时难以控制.
所以, 满足业务需要的前提下, didFinishLaunchingWithOptions 在主线程里做的事情越少越好. 在这一步, 我们可以做的优化有:
梳理各个二方 / 三方库, 找到可以延迟加载的库, 做延迟加载处理, 比如放到首页控制器的 viewDidAppear 方法里.
梳理业务逻辑, 把可以延迟执行的逻辑, 做延迟执行处理. 比如检查新版本, 注册推送通知等逻辑.
避免复杂 / 多余的计算.
避免在首页控制器的 viewDidLoad 和 viewWillAppear 做太多事情, 这 2 个方法执行完, 首页控制器才能显示, 部分可以延迟创建的视图应做延迟创建 / 懒加载处理.
首页控制器用纯代码方式来构建.
总结
总结起来, 好像启动速度优化就一句话: 让系统在启动期间少做一些事. 当然我们得先清楚工程里做的哪些事是在启动期间做的, 对启动速度的影响有多大, 然后 case by case 地分析工程代码, 通过放到子线程, 延迟加载, 懒加载等方式让系统在启动期间更轻松些.
来源: http://mobile.51cto.com/hot-584384.htm