重拾iOS-App启动性能优化
![](https://img.haomeiwen.com/i13182611/5d7c62e7114f7074.png)
关键词:冷启动
,热启动
,dyld
,+load
,+initialize
,Time Profiler
一、App启动流程
1. 冷启动与热启动
首先,我们先来区分两个启动的概念。
冷启动:
App点击启动前,此时App的进程还不在系统里。
需要系统新创建一个进程分配给App。(这是一次完整的App启动过程)
热启动:
App在冷启动后用户将App退回后台,此时App的进程还在系统里。
用户重新返回App的过程。(热启动做的事较少)
主要区别:
名称 | 区别 |
---|---|
冷启动 | 启动时,App的进程不在系统里,需要开启新进程。 |
热启动 | 启动时,App的进程还在系统里,不需要开启新进程。 |
2. App的完整启动流程(冷启动流程)
![](https://img.haomeiwen.com/i13182611/6395497ca1bb60b6.png)
以main函数为界限,App的完整启动流程主要分为两个阶段:
- main() 函数执行前(pre-main阶段)
- main() 函数执行后
第一阶段:main函数执行前
![](https://img.haomeiwen.com/i13182611/3c8ba1b86a06ffc8.png)
在pre-main阶段,主要经历了一下流程:
1)把App对应的可执行文件(Mach-O)加载到内存
2)加载dyld动态连接器
3)dyld加载动态库(包括所依赖的所有动态库)
4)Rebase(重定向)
5)Bind(符号绑定)
6)Objc setup
7)Initialize(相关初始化)
1)把App对应的可执行文件(Mach-O)加载到内存
2)加载dyld动态连接器
dyld叫做动态链接器,主要的职责是完成各种库的连接。dyld是苹果用C++写的一个开源库,可以在苹果的git上直接查看源代码。
当系统从xnu内核态把控制权转交给dyld变成用户态后dyld首先初始化程序环境,将可执行文件以及相应的系统依赖库与我们自己加入的库加载进内存中,生成对应的ImageLoader类对应的image对象(镜像文件),对这些image进行链接,调用各image的初始化方法等等(注:这里多数情况都是采用的递归,从底向上的方法调用),其中runtime就是在这个过程中被初始化的,这些事情大多数在dyld:_mian方法中被发生。
![](https://img.haomeiwen.com/i13182611/370617e5e79dc00f.png)
3)dyld加载动态库(包括所依赖的所有动态库)
dyld会首先读取Mach-O文件的Header和load commands。
接着就知道了这个可执行文件依赖的动态库。例如加载动态库A到内存,接着检查A所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个App所依赖的动态库在100-400个左右,其中大多数都是系统的动态库,它们会被缓存到dyld shared cache,这样读取的效率会很高。
4)Rebase(重定向)
Rebase在Image内部调整指针的指向。在过去,会把动态库加载到指定地址,所有指针和数据对于代码都是对的,而现在地址空间布局是随机化,所以需要在原来的地址根据随机的偏移量做一下修正。
5)Bind(符号绑定)
Bind是把指针正确地指向Image外部的内容。这些指向外部的指针被符号(symbol)名称绑定,dyld需要去符号表里查找,找到symbol对应的实现
6)Objc setup
注册Objc类 (class registration)
把category的定义插入方法列表 (category registration)
保证每一个selector唯一 (selector uniquing)
7)Initialize(相关初始化)
Objc的+load()函数
C++的构造函数属性函数
非基本类型的C++静态全局变量的创建(通常是类或结构体)
第二阶段:main函数执行后
![](https://img.haomeiwen.com/i13182611/2043ef49644d282e.png)
The app is launched, either explicitly by the user or implicitly by the system.
// 该应用程序可以由用户显式启动,也可以由系统隐式启动。The Xcode-provided main function calls UIKit's
UIApplicationMain(_:_:_:_:)
function.
// 调用UIApplicationMain(_:_:_:_:)
函数。The
UIApplicationMain(_:_:_:_:)
function creates the UIApplication object and your app delegate.
//UIApplicationMain(_:_:_:_:)
函数中,创建UIApplication对象和APPdelegate。UIKit loads your app's default interface from the main storyboard or nib file.
// UIKit从主故事板或nib文件加载应用程序的默认界面。UIKit calls your app delegate's
application(_:willFinishLaunchingWithOptions:)
method.
// UIKit调用application(_:willFinishLaunchingWithOptions:)
函数。UIKit performs state restoration, which calls additional methods of your app delegate and view controllers.
// UIKit执行状态还原,该状态调用应用程序delegate和view controllers的其他方法。UIKit calls your app delegate's
application(_:didFinishLaunchingWithOptions:)
method .
// UIKit调用application(_:didFinishLaunchingWithOptions:)
函数。When initialization is complete, the system uses either your scene delegates or app delegate to display your UI and manage the life cycle for your app.
// 初始化完成后,系统将使用scene delegates或app delegate来显示UI并管理应用程序的生命周期。
二、App启动性能优化
了解了APP的启动流程之后,我们才能更有针对性的去做优化。
1. 针对第一阶段的优化(main函数之前)
1)减小App包大小
这里主要涉及到APP包瘦身。如:
- 删除一些无用的资源文件;
- 对图片等资源进行压缩;
2)减少库的依赖
- 减少系统依赖库;
- 减少自己需要加入的各种三方库(库越少dyld加载的速度越快,就能越早的返回程序入口main函数的地址);
- 有一些自己加入的库,能选择静态库就选择静态库,少用动态库,因为动态库的加载方式比静态库慢。如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;
- 自己加入的各种framework库根据情况设为optional和required,如果该framework在当前App支持的所有iOS系统版本中都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查会导致加载变慢;
3)Objc setup 阶段优化
- Objc类越多,启动越慢。删除无用的类和方法,可以使用AppCode来检查一些无用的代码;
- category 类越多,启动越慢。当有多个分类时,功能类似的分类尽量合并到一个分类中;
如UIView+Frame,UIView+AutoLayout…合并为一个;
4)Initialize 阶段优化
- Objc的+load()函数越多,启动越慢。将不必须在+load方法中做的事情延迟到+initialize中;
- C++的构造函数属性函数越多,启动越慢;
- 少用C++全局变量;
2. 针对第二阶段的优化(main函数之后)
这一阶段主要是做一些初始化工作和一些启动项任务,比如:
- 异常监控
- 统计上报
- 自定义配置
- 路由配置
- 首页构建
- 定位
等等;
所以,在这一阶段的优化考虑的点可能更多些。
能往后放的尽量不要提前执行,能在后台运行的就不要放在主线程中。
三、App启动性能监测;
1. Time Profiler
Time Profiler在分析时间占用上非常强大。实用的时候注意三点
- 在打包模式下分析(一般是Release),这样和线上环境一样。
- 记得开启dsym,不然无法查看到具体的函数调用堆栈。
- 分析性能差的设备,对于支持iOS 8的,一般分析iphone 4s或者iphone 5。
一个典型的分析界面如下:
![](https://img.haomeiwen.com/i13182611/a25133962bee38bc.png)
几点要注意:
- 分析启动时间,一般只关心主线程。
- 选择Hide System Libraries和Invert Call Tree,这样我们能专注于自己的代码。
- 右侧可以看到详细的调用堆栈信息。
在某一行上双击,我们可以进入到代码预览界面,去看看实际每一行占用了多少时间:
![](https://img.haomeiwen.com/i13182611/afe092e623c86997.png)
2. 设置环境变量监控启动时间
启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。
在Xcode中,可以通过设置环境变量来查看App的启动时间,DYLD_PRINT_STATISTICS
和DYLD_PRINT_STATISTICS_DETAILS
。
![](https://img.haomeiwen.com/i13182611/7ff38520156b4a68.png)
Total pre-main time: 43.00 milliseconds (100.0%)
dylib loading time: 19.01 milliseconds (44.2%)
rebase/binding time: 1.77 milliseconds (4.1%)
ObjC setup time: 3.98 milliseconds (9.2%)
initializer time: 18.17 milliseconds (42.2%)
slowest intializers :
libSystem.B.dylib : 2.56 milliseconds (5.9%)
libBacktraceRecording.dylib : 3.00 milliseconds (6.9%)
libMainThreadChecker.dylib : 8.26 milliseconds (19.2%)
ModelIO : 1.37 milliseconds (3.1%)
相关参考
深入理解iOS App的启动过程
https://blog.csdn.net/Hello_Hwc/article/details/78317863
了解App的启动流程
https://juejin.im/post/5da830a4e51d457805049817
美团外卖iOS App冷启动治理
https://tech.meituan.com/2018/12/06/waimai-ios-optimizing-startup.html
iOS-APP的启动流程和生命周期
https://www.jianshu.com/p/229dd6190b95