面试题引发的思考:
Q: APP的启动类型?
- 冷启动(Cold Launch):从零开始启动APP,是主要优化方向;
- 热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP。
Q: 冷启动的三大阶段?
-
dyld(动态链接器dynamic link editor)
APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库; -
Runtime
由Runtime负责加载成objc
定义的结构; -
main
函数
所有初始化工作结束后,dyld就会调用main
函数;接下来就是UIApplicationMain
函数,AppDelegate
的application:didFinishLaunchingWithOptions:
方法。
Q: 冷启动优化方案?
-
dyld:
清理不必要的动态库;
清理不必要的类、分类、方法;
减少C++虚函数数量;
Swift尽量使用struct
。 -
Runtime:
用+initialize
方法和dispatch_once
取代所有的__attribute__((constructor))
、C++静态构造器、ObjC的+load
方法。 -
main:
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在didFinishLaunchingWithOptions
方法中,也就是按需加载。
1. 启动类型
APP的启动可以分为2种:
- 冷启动(Cold Launch):从零开始启动APP,是主要优化方向;
- 热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP。
通过添加环境变量可以打印出APP的启动时间分析,点击Edit scheme -> Run -> Arguments,给Environment Variables添加一个DYLD_PRINT_STATISTICS
,并且把Value
设置为1
,如下图:
如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS
改为DYLD_PRINT_STATISTICS_DETAILS
,并且Value
也是设置为1
,如下图:
一般总时间在400ms以内都是比较正常的,如果大于400ms就可以优化了。
2. 冷启动
(0) 冷启动的三大阶段
- dyld
- Runtime
main
函数
(1) dyld
- dyld(dynamic link editor),是Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)。
- 启动APP时,dyld动态链接器会装载APP的可执行文件,同时会递归加载所有依赖的动态库;
- 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理。
Q: 什么是可执行文件?
创建一个的Demo项目,项目Products -> Demo.app -> 右键Show in Finder -> Demo.app -> 右键显示包内容 -> Unix可执行文件。
这个Unix可执行文件,是一个Mach-O格式的可执行文件,包含所有编写的代码。
注意:项目没编译的时候上面的Demo.app是红色的,并且右键Show in Finder也没东西,编译之后才会多一个Demo.app的包。
但是我们项目开发中也会依赖一些动态库,比如UIKit、Foundation,这些动态库都不是包含在可执行文件里面的,可执行文件里面只有一些依赖信息,比如这个项目依赖哪些动态库,一个动态库也可能依赖另一个动态库。
在dyld阶段,dyld动态链接器会装载可执行文件,以及检查可执行文件依赖哪些动态库,并加载那些动态库,一个动态库也可能依赖另一个动态库,dyld动态链接器就是这样一个一个检查,递归查找,直到装载完所有的动态库到内存中。
当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理。
(2) Runtime
启动APP时,Runtime所做的事情有:
- 调用
map_images
函数进行可执行文件内容的解析和处理;- 在
load_images
函数中调用call_load_methods
,调用所有Class和Category的+load
方法;- 进行各种
objc
结构的初始化(注册Objc类 、初始化类对象等等);- 调用C++静态初始化器和
__attribute__((constructor))
修饰的函数。
到此为止,可执行文件和动态库中所有的符号(Class
,Protocol
,Selector
,IMP
,…)都已经按格式成功加载到内存中,被Runtime所管理.
可能你对map_images
函数比较熟悉,我们在以前看_objc_init
源码的时候接触过这个函数,如下:
(3) main
- 所有初始化工作结束后,dyld就会调用
main
函数;- 接下来就是
UIApplicationMain
函数,AppDelegate
的application:didFinishLaunchingWithOptions:
方法。
(4) 总结
APP的冷启动阶段如下:
- dyld:
APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库;- Runtime:
并由Runtime负责加载成objc
定义的结构;main
函数:
所有初始化工作结束后,dyld就会调用main
函数;接下来就是UIApplicationMain
函数,AppDelegate
的application:didFinishLaunchingWithOptions:
方法。
3. 冷启动优化
按照不同的阶段:
dyld:
减少动态库、合并一些动态库(定期清理不必要的动态库);
减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类);
减少C++虚函数数量(因为一旦有虚函数就要多维护一张虚表,有虚表的话冷启动的时候就要多耗费一点时间);
Swift尽量使用struct
(因为加载类的时候也耗费时间)。Runtime:
用+initialize
方法和dispatch_once
取代所有的__attribute__((constructor))
、C++静态构造器、ObjC的+load
方法。main:
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在didFinishLaunchingWithOptions
方法中,也就是按需加载。