性能优化-应用启动时间
性能优化-应用启动时间
设置环境变量
这里的应用启动时间指,应用启动到显示第一个页面展示时的时间。
应用启动有冷启动和热启动,热启动是指应用在后台活着,然后再启动应用。这里只谈冷启动。
启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。
Xcode11,通过添加环境变量可以打印出APP的启动时间分析(Product- scheme-Edit scheme -> Arguments-如下),DYLD_PRINT_STATISTICS设置为1,如果查看更详细的信息可以DYLD_PRINT_STATISTICS_DETAILS设置为1。
截屏2020-03-24上午11.48.34.png 截屏2020-03-24上午11.48.34.png跑下工程:
Total pre-main time: 6.3 seconds (100.0%)
dylib loading time: 2.1 seconds (34.7%)
rebase/binding time: 3.7 seconds (59.5%)
ObjC setup time: 70.06 milliseconds (1.1%)
initializer time: 287.55 milliseconds (4.5%)
slowest intializers :
...........
可以看到,在执行main函数前,应用准备了执行了4个流程:dylib loading、rebase/binding、ObjC setup、initializer,下面我们将好好分析这几个流程。
- load dylibs:加载动态库,包括系统的、自己添加的(第三方的),递归一层一层加载所依赖的库。
加载dylib
分析每个dylib(大部分是iOS系统的),找到其Mach-O文件,
打开并读取验证有效性,找到代码签名注册到内核,
最后对dylib的每个segment调用mmap()。
- Rebase&Bind:修复指针,mach-o内部的存储逻辑是,信息的存储地址是虚拟内存,不是直接对应物理内存;每一次应用启动的时候,内存的开始地址又是随机的,因此需要对接虚拟内存和物理内存地址。为了安全,防止黑客攻击。
rebase/bind
dylib加载完成之后,它们处于相互独立的状态,需要绑定起来。
在dylib的加载过程中,系统为了安全考虑,引入了ASLR(Address Space Layout Randomization)技术和代码签名。
由于ASLR的存在,镜像(Image,包括可执行文件、dylib和bundle)会在随机的地址上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。
Rebase在前,Bind在后,Rebase做的是将镜像读入内存,修正镜像内部的指针,性能消耗主要在IO。
Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。
- Objc:注册类信息到全局Table中
OC setup
OC的runtime需要维护一张类名与类的方法列表的全局表。
dyld做了如下操作:
对所有声明过的OC类,将其注册到这个全局表中(class registration)
将category的方法插入到类的方法列表中(category registration)
检查每个selector的唯一性(selector uniquing)
-
Initializers:初始化部分,+load方法初始化,C/C++静态初始化对象和标记
__attribute__(constructor)
的方法
如果在各个 OC 类别的 ‘load’方法里做了不少事情(如在里面使用 Method swizzle),那么这是pre-main阶段最耗时的部分。dyld运行APP的初始化函数,调用每个OC类的+load方法,调用C++的构造器函数(attribute((constructor))修饰),创建非基本类型的C++静态全局变量,然后执行main函数。
- Main():执行main函数,执行APPDelegate的方法
-
加载Window+加载RootViewController+初始化操作:主要在
didFinishLaunchingWithOptions
执行操作,比如初始化第三方库,初始化基础信息,加载RootViewController等
优化整体思路:
1. 移除不需要用到的动态库
2. 移除不需要用到的类
3. 合并功能类似的类和扩展
4. 尽量避免在+load方法里执行的操作,可以推迟到+initialize方法中。
在了解了应用启动流程后,那对应用启动优化的工作就细分到了对每个流程的优化上。
<a name="LeY0o"></a>
1. load dylibs加载动态库
<a name="AdEI7"></a>
启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。合并动态库。
<a name="P6Oqv"></a>
2. Rebase & Bind & Objective C Runtime
<br />Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:
1)减少__DATA段中的指针数量。
2)合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个<br />删除无用的方法和类。
3)多用Swift Structs,因为Swfit Structs是静态分发的。
<a name="1nmJG"></a>
3. Initializers
<br />通常,我们会在+load方法中进行method-swizzling,但这会影响应用启动的时间。
1)用initialize替代load。不少同学喜欢用method-swizzling来实现AOP去做日志统计等内容,强烈建议改为在initialize进行初始化。
2)减少atribute((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。
3)不要创建线程
4)重写代码。
<a name="gvLVs"></a>
4. main函数之后
优化的核心思想:能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。
我们首先来分析下,从main函数开始执行,到你的第一个界面显示,这期间一般会做哪些事情。
- 执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions,applicationDidBecomeActive,
- 初始化第三方skd
- 初始化Window,初始化基础的ViewController
- 获取数据(Local DB/Network),展示给用户。
在这个过程中我们可以借助工具来进行检测
-
知道这个过程后,可以借助Time Profiler工具查找具体的耗时模块,几点要注意:
- 分析启动时间,一般只关心主线程
- 选择Hide System Libraries和Invert Call Tree,这样我们能专注于自己的代码
- 右侧可以看到详细的调用堆栈信息
-
另外,也可以借用C语言函数查看模块运行时间:
CFTimeInterval startTime = CACurrentMediaTime();
<br />//执行某个方法
<br />CFTimeInterval endTime = CACurrentMediaTime();
<br />当检测出耗时的模块时,就可以按照优化的核心思想来进行处理了;
能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。
main以后的优化思路
梳理各个三方库,找到可以延迟加载的库,做延迟加载处理,比如放到首页控制器的viewDidAppear方法里。
梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
避免复杂/多余的计算。
避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理。
采用性能更好的API。
首页控制器用纯代码方式来构建。