引言
APP 启动,对用户而言,是从点击 APP 开始,到看到 APP 首页展现的过程。
冷启动:针对APP,内存中不包含信息,需要将资源从磁盘读取到内存中。
杀掉APP并不一定进入冷启动(当然重启时可以的)。
热启动:进程的数据仍然在,在启动过程中不用再次加入内存。
启动,通过main
函数将其分为2个大的模块:
1.main
函数之前,通过系统监测启动耗时;
-
main
函数之后,我们自己打点进行时间检测。 -根据具体业务进行操作
2.1 举例:在main
函数和首屏渲染出现之前,即ViewDidLoad()
中分别打点,可同时记录此阶段中所有方法的时间消耗
,找出耗时的进行优化。
一、启动时间 - pre-main
1、启动耗时检测分析
如下图,工程配置添加DYLD_PRINT_STATISTICS
:
运行工程,输出如下:
Total pre-main time: 184.72 milliseconds (100.0%)
dylib loading time: 147.66 milliseconds (79.9%) - 动态库的耗时
rebase/binding time: 126687488.9 seconds (107600740.5%) - 偏移修正/绑定
ObjC setup time: 11.20 milliseconds (6.0%) - OC 类的注册
initializer time: 36.96 milliseconds (20.0%) - 初始化耗时:执行 load 和 constructor构造函数 的耗时
slowest intializers :// 最慢的几项
libSystem.B.dylib : 4.66 milliseconds (2.5%)
libBacktraceRecording.dylib : 5.24 milliseconds (2.8%)
libMainThreadChecker.dylib : 22.02 milliseconds (11.9%)
rebase/binding time
-
1)rebase
:偏移修正
每个应用的二进制文件
,我们所有方法
、函数调用
,编译时
在二进制文件
中都有他们的偏移地址
。以函数func01()
为例,假定函数地址为0x0001
;
运行时
,地址将会发生改变,ASLR
会所及生成一个值,插入在二进制文件的开头
。假定生成的随机值是0x1000
,那么此时,func01()
的地址就是:0x1000 + 0x0001
= 0x1001
,即方法的真实地址!
总结:
地址偏移值
+ 随机值
= 运行时刻的真实地址
即:
ASLR
+ 文件本地的偏移值
= 修正值
,此过程的耗时 --> 偏移修正耗时
。
2)binding
:符号绑定
绑定耗时。
以NSLog
为例,NSLog
的地址我们是无法直接知道的,它在Foundation
框架中 - 属于外部动态库。
*
编译时NSLog
的真实地址是拿不到的,so,在MachO文件中会创建一个NSLog
的符号(它存在在 MachO文件的数据段中),此时指向一个随机 或 固定
的无意义的值。
*
运行时会进行符号绑定binding
,将符号所指向的地址关联
为真正
的NSLog的地址
。
--> 此关联过程即绑定。在内存中进行绑定的
.
MachO文件本身是在磁盘中的,运行时从磁盘加载到内存中,从磁盘到内存的过程像一个copy,就叫做image镜像。即一个可执行文件从磁盘加载到内存便是一个镜像文件,而当镜像文件加载到内存后会进行绑定。此处理由 dyld
进行。
简单来说,此过程:dyld
加载进程时,根据NSLog的符号
所依赖的库
以及要使用的是库中的NSLog
,找到所在库及库中NSLog
的所在地址,然后进行绑定
。即:给符号绑定真正地址的过程。
2、启动耗时优化建议
1)动态库
动态库的载入要耗时
且动态库内部又可能有自身的一些关联依赖
要进行搜寻查找的过程耗时。
因此针对动态库的耗时优化,即是 减少外部动态库的使用
,系统的库都是做了高速优化的暂不考虑,主要针对外部添加的动态库,苹果官方给出建议:添加自定义的外部动态库最好不要超过6
个,多于6个便要考虑合并动态库。
2)OC类
减少OC类,它带来的耗时其实较少,优化要考虑业务实际场景。这里相较来说swift会比OC更快速。
- 项目中废弃的类要干掉,它会加载占用时间;
3)初始化耗时 - 执行 load 和构造函数的耗时
延迟
,没必要在 load()
或 constructor构造函数
中做的事情进行延迟:
- 延迟到
initialize()
去做; - 延迟到
main
之后去做。
二、main
到 首屏展现
我们通过新建一个工程,编写下面简单代码进行分析。
创建MyTabBarCtr
,将其作为根视图:
- (void)viewDidLoad {
NSLog(@"%s",__func__);
[super viewDidLoad];
// Do any additional setup after loading the view.
UIViewController *vc01 = [[MyVC_01 alloc] init];// 红背景
vc01.tabBarItem.title = @"首页";
[self addChildViewController:vc01];
UIViewController *vc02 = [[MyVC_02 alloc] init];// 绿背景
vc02.tabBarItem.title = @"二页";
[self addChildViewController:vc02];
UIViewController *vc03 = [[MyVC_03 alloc] init];// 蓝背景
vc03.tabBarItem.title = @"三页";
[self addChildViewController:vc03];
}
运行程序,程序显示首页前,所做事务顺序如下:
我们将断点打在sceneWillEnterForeground:
APP 将要从后台走向前台,重新运行工程:
由上面结果可看到,在首屏展示之前,其viewDidLoad
是要首先处理完成的。
这里我们可以想到若直接在TabBarController
或首屏
的viewDidLoad
中处理较多绘制渲染、业务逻辑处理等,那么用户看到首页的时间也会加长 --> 导致启动速度慢。
验证:
我们分别在VC01
和MyTabBarCtr
的viewDidLoad
中让其sleep(5)
,运行工程,首屏的展示均会比添加sleep
之前多至少 5 秒!我们把sleep
搁置到异步子线程去处理,此5秒便省下来了!!
优化建议:
1、针对启动时的必要耗时加载考虑用多线程
,在启动时刻可去尽力发挥CPU的性能,这一下下而已其实不必考虑耗时;
2、UI框架
的优化,例如在RootViewController
中不要做不必要的业务处理,并避免在其中处理一些不需立马展示的子VC的初始化;
3、首屏VC的优化,业务按需处理,避免大量耗时。可对页面进行预展示
,数据来了之后才显示实际数据内容;
4、异步
,开子线程是个好东西,当然也要根据也无需求合理使用。
其他一个建议
图片资源的优化
,启动过程中,难免会有图片的加载处理,且实际业务中图片一般会采用清晰度高的,若图片很大很多,IO量就会增大,自然会影响到启动速度。
虽然 Xcode 在编译APP时,已经对我们需要打包进APP里的图片做过一层压缩处理了,但 Xcode 压缩处理相对来说比较轻微保守。
so:图片的无损压缩不失为一个方法。但目前我对其还没有具体了解,日后探究。
针对启动优化的总结:
- 自定义外部动态库的添加遵守苹果官方建议,不要超 6 个;
- 减少不必要的OC类,开发总适当考虑废弃去除不再使用的业务类;
- load 和 构造函数尽量少用,可将其推迟到
initialize()
; - 启动时根据实际场景将耗时放入子线程处理;
-
RootViewController
中避免不必要的业务处理和子VC的初始化; - 首屏的预展示;
- 图片资源的压缩。
暂时 以上。
后面文章将继续针对启动优化进行分析。