冷启动 VS 热启动
04.png 05.png如果你刚刚启动过App,这时候App的启动所需要的数据仍然在缓存中,再次启动的时候称为热启动。如果设备刚刚重启,然后启动App,这时候称为冷启动。
启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。
以main函数作为分水岭,启动时间其实包括了两部分:main函数之前和main函数到第一个界面的viewDidAppear:。所以,优化也是从两个方面进行的,个人建议优先优化后者,因为绝大多数App的瓶颈在自己的代码里。
Main函数之后
我们首先来分析下,从main函数开始执行,到你的第一个界面显示,这期间一般会做哪些事情。
* 执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions
* 初始化Window,初始化基础的ViewController结构(一般是UINavigationController+UITabViewController)
* 获取数据(Local DB/Network),展示给用户。
AppDelegate
通常我们会在AppDelegate的代理方法里进行初始化工作,主要包括了两个方法:
didFinishLaunchingWithOptions
applicationDidBecomeActive
优化这些初始化的核心思想就是:
能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。
这些工作主要可以分为几类:
三方SDK初始化,比如Crash统计; 像分享之类的,可以等到第一次调用再出初始化。
初始化某些基础服务,比如WatchDog,远程参数。
启动相关日志,日志往往涉及到DB操作,一定要放到后台去做
业务方初始化,这个交由每个业务自己去控制初始化时间。
Main函数之前
Main函数之前是iOS系统的工作,所以这部分的优化往往更具有通用性。
dylibs
在每个动态库的加载过程中, dyld需要:
1. 分析所依赖的动态库
2. 找到动态库的mach-o文件
3. 打开文件
4. 验证文件
5. 在系统核心注册文件签名
6. 对动态库的每一个segment调用mmap()
通常的,一个App需要加载很多个dylibs, 但是其中的系统库被优化,可以很快的加载。应用所依赖的dylib文件可能会再依赖其他 dylib,所以dyld所需要加载的是动态库列表一个递归依赖的集合。
针对这一步骤的优化有:
1. 减少非系统库的依赖
2. 合并非系统库
启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。
- 合并动态库,比如公司内部由私有Pod建立了如下动态库:XXTableView, XXHUD, XXLabel,强烈建议合并成一个XXUIKit来提高加载速度。
Rebase & Bind & Objective C Runtime
由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。 rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。
优化该阶段的关键在于减少__DATA segment中的指针数量。我们可以优化的点有:
1. 减少Objc类数量, 减少selector数量
2. 减少C++虚函数数量
Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:
* 减少`__DATA`段中的指针数量。
* 合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个
* 删除无用的方法和类。
* 多用Swift Structs,因为Swfit Structs是静态分发的。
Initializers
通常,我们会在+load
方法中进行method-swizzling,这也是Nshipster推荐的方式。
- 用initialize替代load。不少同学喜欢用method-swizzling来实现AOP去做日志统计等内容,强烈建议改为在initialize进行初始化。
- 减少
__atribute__((constructor))
的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。 - 不要创建线程
- 使用Swfit重写代码。
pre-main阶段耗时的影响因素:
动态库加载越多,启动越慢。
ObjC类越多,函数越多,启动越慢。
可执行文件越大启动越慢。
C的constructor函数越多,启动越慢。
C++静态对象越多,启动越慢。
ObjC的+load越多,启动越慢。
整体上pre-main阶段的优化有:
①减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;
如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;②检查下 framework应当设为optional和required,
如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,
因为optional会有些额外的检查;③合并或者删减一些OC类和函数;
关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类(也可以用根据linkmap文件来分析,但是准确度不算很高);④删减一些无用的静态变量,
⑤删减没有被调用到或者已经废弃的方法。
⑥将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)
因为load是在启动的时候调用,而initialize是在类首次被使用的时候调用,不过当你把load中的逻辑移到initialize中时候,一定要注意initialize的重复调用问题。⑦类和方法名不要太长:iOS每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的;
因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来;⑧用dispatch_once()代替所有的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load函数;
⑨在设计师可接受的范围内压缩图片的大小,会有意外收获。
压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,
图片小了,IO操作量就小了,启动当然就会快了,比较靠谱的压缩算法是TinyPNG。
2、抽象重复代码
1、在iOS代码中可能会为同一个类写很多分类方法,由于参与开发同学较多,可能会导致方法重复,但是实际上运行起来只能有一个分类的方法被调用,这取决于哪个分类后被加载,然而编译的二进制代码中,两个方法应该是都存在的,这不仅会增加app体积,也会增加启动时间,所以应该杜绝这样的重复问题;
2、有很多地方可能是名字不同,但是函数的功能相同,这个不容易被发现,需要大家在写代码的过程中注意;
3、又或者两个函数名字比较接近,里面有很多相似的代码,这种情况下可以进行相同的代码的提取。
main阶段的优化大致有如下几个点:
①减少启动初始化的流程,能懒加载的就懒加载,能放后台初始化的就放后台,
能够延时初始化的就延时,不要卡主线程的启动时间,已经下线的业务直接删掉;
②优化代码逻辑,去除一些非必要的逻辑和代码,减少每个流程所消耗的时间;
③启动阶段使用多线程来进行初始化,把CPU的性能尽量发挥出来;
④使用纯代码而不是xib或者storyboard来进行UI框架的搭建,尤其是主UI框架比如TabBarController这种,
尽量避免使用xib和storyboard,因为xib和storyboard也还是要解析成代码来渲染页面,多了一些步骤;
网友评论