本文学习参考自iOS启动时间优化
文内部分内容和图片来自该博客
1、启动耗时测量
在 Xcode 中 菜单栏 Product —> Scheme —> Edit Scheme -> Run -> Auguments 设置环境变量DYLD_PRINT_STATISTICS
为1 :
启动后打印结果:
Total pre-main time: 176.51 milliseconds (100.0%)
dylib loading time: 51.00 milliseconds (28.8%)
rebase/binding time: 411015771.6 seconds (356090203.5%)
ObjC setup time: 58.12 milliseconds (32.9%)
initializer time: 117.07 milliseconds (66.3%)
slowest intializers :
libSystem.B.dylib : 6.27 milliseconds (3.5%)
libBacktraceRecording.dylib : 15.98 milliseconds (9.0%)
libMainThreadChecker.dylib : 75.84 milliseconds (42.9%)
libViewDebuggerSupport.dylib : 12.59 milliseconds (7.1%)
启动时间信息:
Total pre-main time: ========= main()函数之前耗时
dylib loading time:=========== 动态库加载耗时
rebase/binding time:========= 指针重定位耗时
ObjC setup time:============ OC类初始化耗时
initializer time:==============初始化耗时
slowest intializers:===========以下是最耗时的几个动态库加载
2、App启动过程
App 的启动过程主要分为 pre-main()
阶段和 main()
阶段。
-
解析Info.plist (准确性待考证)
加载相关信息,例如闪屏
沙箱建立、权限检查 -
Mach-O加载
如果是胖二进制文件,寻找合适当前CPU架构的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为__attribute__((constructor))的C函数
加载类扩展(Category)中的方法
C++静态对象加载、调用ObjC的 +load 函数 -
程序执行
调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching
pre-main过程:
main过程:
3、启动优化
3.1 Pre-Main() 阶段优化
3.1.1 加载动态库的优化
每一个动态库的加载过程:
1.分析所依赖的动态库
2.找到动态库的mach-o文件
3.打开文件
4.验证文件
5.在系统核心注册文件签名
6.对动态库的每一个segment调用mmap()
动态库优化建议:
- 减少非系统库的依赖;
- 使用静态库而不是动态库;
- 合并非系统动态库为一个动态库;
3.1.2 Rebase&&Binding 阶段的优化
-
Rebase
由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。这就需要重复不断地对 __DATA 段中需要 rebase 的指针加上这个偏移量。 -
Binding
将指针指向镜像外部的内容,binding就是将这个二进制调用的外部符号进行绑定的过程。比如我们objc代码中需要使用到NSObject, 即符号_OBJC_CLASS_$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起; -
总结
Objective-C 中有很多数据结构都是靠 Rebasing 和 Binding 来修正(fix-up)的,比如 Class 中指向父类的指针和指向方法的指针。
Rebase&&Binding该阶段的优化关键在于减少__DATA segment中的指针数量。 -
优化建议:
1.减少Objc类数量, 减少selector数量,把未使用的类和函数都可以删掉 ;
2. 减少C++虚函数数量;
3. 转而使用swift stuct(其实本质上就是为了减少符号的数量,使用swift语言来开发?);
3.1.3 Initializers 阶段优化
- 优化建议:
1.使用 +initialize 来替代 +load
2.不要使用 atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。
比如使用 dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用时才初始化,推迟了一部分工作耗时。
3.也尽量不要用到C++的静态对象。
3.1.4 pre-main() 阶段优化总结
1.减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;
如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;
2.检查下 framework应当设为optional和required,
如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
3.合并或者删减一些OC类和函数;
关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类(也可以用根据linkmap文件来分析,但是准确度不算很高);
有一个叫做FUI的开源项目能很好的分析出不再使用的类,准确率非常高,唯一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++的类模板。
4.删减一些无用的静态变量。
5.删减没有被调用到或者已经废弃的方法,
方法见http://stackoverflow.com/questions/35233564/how-to-find-unused-code-in-xcode-7
和https://developer.Apple.com/library/ios/documentation/ToolsLanguages/Conceptual/Xcode_Overview/CheckingCodeCoverage.html。
6.将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)
7.类和方法名不要太长:iOS每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的;
因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来;
8.用dispatch_once()代替所有的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load函数;
9.在设计师可接受的范围内压缩图片的大小,会有意外收获。
压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了,比较靠谱的压缩算法是TinyPNG。
3.2 main() 阶段优化
优化建议:
1.减少启动初始化的流程,能懒加载的就懒加载,能放后台初始化的就放后台,能够延时初始化的就延时,不要卡主线程的启动时间,已经下线的业务直接删掉;
2.优化代码逻辑,去除一些非必要的逻辑和代码,减少每个流程所消耗的时间;
3.启动阶段使用多线程来进行初始化,把CPU的性能尽量发挥出来;
4.使用纯代码而不是xib或者storyboard来进行UI框架的搭建,尤其是主UI框架比如TabBarController这种,尽量避免使用xib和storyboard,因为xib和storyboard也还是要解析成代码来渲染页面,多了一些步骤。