启动
启动的过程一般是指从用户点击app图标
开始到AppDelegate 的didFinishLaunching
方法执行完成为止,其中,启动也分为冷启动和热启动
冷启动
:内存中不包含app相关数据的启动,一般我们可以通过重启手机来实现冷启动热启动
:是指杀掉app进程后,数据仍然存在时的启动
而我们这里所说的启动优化,一般是指冷启动情况
下的,这种情况下的启动主要分为两部分:
- T1 :
pre-main
阶段,即main函数之前,操作系统加载App可执行文件到内存,执行一系列的加载&链接等工作,简单来说,就是dyld加载过程
- T2:main函数之后,即从main函数开始,到Appdelegate 的
didFinishLaunching
方法执行完成为止,主要是构建第一个界面,并完成渲染
所以,T1+T2
的过程 就是 从用户点击App图标到用户能看到app主界面的过程,即需要启动优化的部分
pre-main阶段的优化
在iOS-底层原理 15:dyld加载流程中,已经了解过了dyld的加载流程。pre-main阶段的启动时间其实就是dyld加载过程的时间
针对main函数之前的启动时间,苹果提供了内建的测量方法,在Edit Scheme -> Run -> Arguments ->Environment Variables
点击+添加环境变量 DYLD_PRINT_STATISTICS
设为 1
),然后运行,以下是iPhone7p正常启动的pre-main时间(以WeChat为例)
说明
pre-main阶段总共用时1.7s
dylib loading
time(动态库耗时):主要是加载动态库,用时320.32msrebase/binding
time(偏移修正/符号绑定耗时),耗时160.52ms
rebase(偏移修正)
:任何一个app生成的二进制文件,在二进制文件内部所有的方法、函数调用,都有一个地址,这个地址是在当前二进制文件中的偏移地址
。一旦在运行时刻(即运行到内存中),每次系统都会随机分配一个ASLR(Address Space Layout Randomization,地址空间布局随机化)地址值
(是一个安全机制,会分配一个随机的数值,插入在二进制文件的开头),例如,二进制文件中有一个 test方法,偏移值是0x0001,而随机分配的ASLR
是0x1f00,如果想访问test方法,其内存地址(即真实地址)变为ASLR+偏移值 = 运行时确定的内存地址
(即0x1f00+0x0001 = 0x1f01)binding(绑定)
:,例如NSLog
方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog
(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld
做的,也称为动态库符号绑定
),一句话概括:绑定就是给符号赋值的过程
ObjC setup
time(OC类注册的耗时):OC类越多,越耗时initializer
time(执行load和构造函数的耗时)
针对这几部,有以下几点优化建议:
- 尽量
少用外部动态库
,苹果官方建议自定义的动态库最好不要超过6个
,如果超过6个,需要合并
动态库 - 减少OC类,因为OC类越多,越耗时
- 将不必须在
+load
方法中做的事情延迟到+initialize
中,尽量不要用C++虚函数 - 如果是swift,尽量使用
struct
main函数阶段的优化
在main函数之后的didFinishLaunching
方法中,主要是执行了各种业务,有很多并不是必须在这里立即执行的,这种业务我们可以采取延迟加载,防止影响启动时间。
在didFinishLaunching
中的业务主要分为三个类型
- 【第一类】初始化第三方sdk
- 【第二类】app运行环境配置
- 【第三类】自己工具类的初始化等
main函数阶段的优化建议主要有以下几点:
减少启动初始化的流程
,能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间- 优化代码逻辑,
去除非必须的代码逻辑
,减少每个流程的消耗时间 - 启动阶段能
使用多线程
来初始化的,就使用多线程 - 尽量
使用纯代码
来进行UI框架的搭建,尤其是主UI框架,例如UITabBarController。尽量避免使用Xib或者SB,相比纯代码而言,这种更耗时 - 删除废弃类、方法