启动优化的原理以及操作
首先我们要知道,启动是分为冷启动和热启动的。启动优化主要就是优化冷启动的时间。
冷启动又可以分为两个部分:pre_main阶段和main函数之后。
pre_main阶段:main函数之前,系统加载可执行文件到内存,执行一系列的加载链接等操作。dyld的加载过程。
main函数之后:从main函数开始,到构建第一个见面,渲染完成。
main函数之前
在优化main函数之前的加载过程之前,我们是不是首先要看看他的耗时。具体怎么怎么加载,然后我们再从这些方面入手。
-
检测耗时
在Environment Variables --> 点击+添加环境变量 DYLD_PRINT_STATISTICS设为YES,运行项目,就可以打印出对应的耗时时间。如下图所示
截屏2022-01-11 下午2.27.41.png
截屏2022-01-11 下午2.30.05.png
第二图就是最终打印出的耗时时间。看了上面的打印输出,有点懵,这都是些什么?不要着急 接下来慢慢看他的流程局明白了。
-
加载加载过程
截屏2022-01-12 下午4.27.37.png
上图是我自己总结画出的一个流程图。看不懂没关系,我们来一个一个的了解。
镜像
我们首先要明白,每个app都是以images为单位进行加载的。而镜像的类型包括:
- 1.executable:应用的二进制可执行文件;
- 2.dylib:动态链接库;
- 3.bundle:资源文件,属于不能被链接的 dylib
明白了镜像以后我们在看上面的图。系统通过 fork() 方法创建了一个进程,然后执行镜像通过exec() 来替换为另外一个可执行程序。
Mach-O
刚开始不知道这是个啥玩意,后来查了一些资料博客才明白。
Mach-O:是一种用于记录可执行文件、对象代码、动态加载代码和内存转存的文件格式。app生成的二进制可执行文件就是Mach-O格式的。ios所有的类编译后都会生成对应的目标文件.o文件,而这个可执行文件就是这些.o文件集合。
Mach-O 文件主要由三部分组成:
- 1.Mach header:描述 Mach-O 的 CPU 架构、文件类型以及加载命令等;
- 2.Load commands:描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令;
- 3.Data:Data 中的每个段(segment)的数据都保存在这里,每个段都有一个或多个 Section,它们存放了具体的数据与代码,主要包含这三种类型:
1.__TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)。
2.__DATA 包含全局变量,静态变量等。可读写(rw-)。
3.__LINKEDIT 包含了加载程序的元数据,比如函数的名称和地址。只读(r–-)
dylib
后缀名为.dylib的文件就是动态库。动态库是运行时加载的,可以背多个app公用(系统的动态库)。
这里动态库又分为了两种:系统的dylib和内嵌dylib(也就是我们自己引入的);
- ios中用到的所有系统 framework,比如 UIKit、Foundation;
- 系统级别的 libSystem(如 libdispatch(GCD) 和 libsystem_blocks(Block))
- 注意:系统的动态库都是可以共享的,他可以让手机上的其他app公用。但是开发者自己引入的动态库是不能共享的,在一个项目中你引入了一个自己创建的动态库,在手机上的其他app能用嘛?肯定是不可能使用的。
后续会了解动态库和静态库的优缺点和区别。
dyld
dyld:Dynamic Link Editor,看英文就可以理解出来,它是动态链接器。它是专门用来加载dylib文件的。
上面是一些我们需要知道的知识点。下面来详细说明启动的过程。
参考流程图我们知道,启动应用时,系统会通过fork()创建一个进程。然后执行镜像通过exec() 来替换为另一个可执行程序。然后执行如下的操作
1.把可执行文件加载到内存空间,从可执行文件中能够分析出dyld的路径。
2.把dyld加载到内存。
3.Load Dylibs:(1)、分析app依赖的所有dylib动态库(2)找到dylib对应的mach-o文件(3)打开、读取这些mach-o文件,并验证其有效性。(4)在系统内核中注册代码签名
4.Rebase/Binding指针重定位:
在dylib加载的过程中,系统为了安全考虑,引入了ASLR技术和代码签名。由于ASLR的存在,镜像会在新的随机地址上加载,和之前指针指向的地址会有一个偏差。所以,指针数量越少,指针修复的耗时也越少。
5.ObjC Setup
(1)dyld会注册所有声明过的objc类。
(2)将分类插入到类的方法列表中
(3)检查每个selector的唯一性。
6.Initializers和load:开始动态调整,往堆和栈中写入内容
(1)调用每个 Objc 类和分类中的 +load 方法
(2)调用 C/C++ 中的构造器函数(用 attribute((constructor)) 修饰的函数)
7.main() 的初始化。
pre_main优化操作
既然明白了main函数之前的操作过程,那么我们针对他的每一个过程就可以来优化
dyld
- 尽量不适用内嵌的dylib,也就是说别自己瞎胡创建动态库。系统的动态库都做了优化的。我们自己创建的加载很耗时。
- 如果必须使用自己的动态库,可以合并我们创建的dylib
- 可以使用静态库代替。(静态库会在编译是被打进可执行文件,造成文件体积变大)
Rebase/Binding
这个过程是dyld调整修复地址,所以指针越少,耗时就越少。
- 我们可以减少objc类、方法、分类的数量。删除无效的类和方法。(可以使用第三方fui检测无效的类,使用pytho检测无效的方法)
- 减少c++虚函数
Initializers
- 尽量避免在类的 +load 方法中初始化,可以推迟到 +initiailize 中进行;(因为在一个 +load 方法中进行运行时方法替换操作会带来 4ms 的消耗)
main函数阶段
查看main函数阶段的耗时
1.https://github.com/beiliao-mobile/BLStopwatch 创建了一个单例类,然后把每次的时间都加入到数组中。具体的操作可以查看git
2.使用xcode自带的。
Xcode → Open Developer Tool → Instruments → Time Profiler。
1.配置 Scheme。点击 Edit Scheme 找到 Profile 下的 Build Configuration,设置为 Debug。
- 配置 PROJECT。点击 PROJECT,在 Build Settings 中直接搜 Debug Information Format,把 Debug 对应的值改为 DWARF with dSYM File。
3.启动Time Profiler。上选择Call Tree 中的 Separate Thread 和 Hide System Libraries。然后就可以查看启动时间。
启动优化
关于app的初始化,除了统计、日志这种必须要在app一启动就配置的事件,有一些配置也可以考虑延迟。可以从下面的一些角度做优化:
- 用纯代码的方式,而不是用xib/sb来加载首页视图
- 可以创建一个工具单例类,根据不能的业务场景初始化。延迟暂时不需要的第三方加载,延迟执行部分的业务逻辑和UI配置。在工具类中,我们可以把必须在启动初始化的放到一个方法中。然后把首页初始化的在一个方法中,后期更方便维护。
- 在release包中移除NSLOG打印
- 在视觉可接受的范围内,压缩页面中的图片大小。
- 耗时操作,放在子线程中完成