这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
上一篇文章聊了启动时间的监控和测量 : iOS基础学习-1-启动时间监控,这篇文章聊一下启动过程的时间优化。 上一篇也说了启动过程具体可分三个阶段:
- pre-main阶段
- main函数之后
- 首屏渲染完成之后 接下来我们具体看下各个阶段如何进行时间优化
pre-main阶段
这个阶段做的事情如下:
- 加载可执行文件(app的.o文件的集合)
- 加载动态连接库 - dylb loading
- 进行rebase指针调整和bind符号绑定 - rebase/binding
- OC运行时的初始处理(OC相关类的注册,category注册,selector唯一性检查等)- Objc setup
- 初始化(执行
+load()方法,attribute(constructor)修饰的函数的调用、创建C++静态全局变量) - initializer
可以优化的点有:
- dylb loading 阶段,加载动态库
- 这个阶段会去装载app使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取。对于Apple提供的的系统动态库,做了高度的优化。而对于开发者定义导入的动态库,则需要在花费更多的时间。Apple官方建议尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们。
- rebase/binding 阶段,
- 这个阶段进行rebase指针调整和bind符号绑定,这个阶段可以通过减少App的Objective-C类,分类和Selector的个数。这样做主要是为了加快程序的整个动态链接, 在进行动态库的重定位和绑定(Rebase/binding)过程中减少指针修正的使用,加快程序机器码的生成;
- Objc setup 阶段
- 大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会注册所有声明过的ObjC类,将分类插入到类的方法列表里,再检查每个selector的唯一性。 这个阶段的优化和上个阶段的优化是一样的,上个阶段优化好,这个阶段就不用优化
- initializers 阶段
- 这一阶段,dyld开始运行程序的初始化函数,调用每个Objc类和分类的+load方法,调用C/C++ 中的构造器函数(用attribute((constructor))修饰的函数),和创建非基本类型的C++静态全局变量。Initializers阶段执行完后,dyld开始调用main()函数。
- 这个阶段的优化有:1. 减少在+load方法中做事情,推迟到initializers方法内执行,2. 减少构造器函数个数,在构造器函数里少做些事情 3. 减少C++静态全局变量的个数
pre - main阶段的优化方案
- 尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们。
- 减少App的Objective-C类,分类和Selector的个数
- 减少在+load方法中做事情,推迟到initializers方法内执行
- 减少构造器函数个数,在构造器函数里少做些事情
- 减少C++静态全局变量的个数
main 之后
优化方案
- 检查启动路径上的方法调用,能滞后的滞后
- 二进制重排,减少启动过程的page in
- 动态库懒加载,减少启动过程代码加载量
- Background Fetch,减少冷启动次数
优化实施细节
减少、合并动态库
减少类、分类、selector个数
减少类的个数
可以分为静态扫描和线上统计两种方式。
最简单的静态扫描是基于 AppCode,但是项目大了之后 AppCode 的索引速度非常慢,另外的一种静态扫描是基于 Mach-O 的:
_objc_selrefs和_objc_classrefs存储了引用到的 sel 和 class__objc_classlist存储了所有的 sel 和 class
二者做个差集就知道那些类/sel 用不到,但objc 支持运行时调用,删除之前还要在二次确认。
还有一种统计无用代码的方式是用线上的数据统计,主流的方案有三种:
- ViewConteroller 渗透率,hook 对应的声明周期方法即可统计
- Class 渗透率,遍历运行时的所有类,通过 Objective C Runtime 的标志位判断类是否被访问
- 行级渗透率,需要用编译期插桩,对包大小和执行速度均有损。
前两种是 ROI 较高的方案,绝大多数时候 Class 级别的渗透率足够了。
+load方法内的事情拖到initializers内执行
将+load方法内的事情放到initializers方法内执行,然后增加dispatch_one防止执行多次
减少构造器函数个数,在构造器函数里少做些事情
减少C++静态全局变量的个数
检查启动路径上的方法调用,能滞后的滞后
二进制重排,减少启动过程的page in
1.获取启动时都加载了那些符号 2.生成排序文件 3.配置排序文件 4.验证
动态库懒加载,减少启动过程代码加载量
Background Fetch,减少冷启动次数
1. 设置Background Fetch
在info.plist中的UIBackgroundModes中增加fetch
2. 设置建议唤起频率
在didFinishLaunchingWithOptions设置
//设置BackgroundFetch启动间隔
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
这一步配置的minimum interval,单位是秒,只是给系统的建议,系统并不会按照给定的时间间隔按规律的唤醒进程。
3. 设置回调
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
SFIMLOGINFO(@"BackgroundFetch 启动");
//30s内必须回调block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
SFIMLOGINFO(@"BackgroundFetch 回调");
completionHandler(UIBackgroundFetchResultNewData);
});
}
由于 Background Fetch 机制是为了让App在后台拉取准备数据,但我们只是为了实现”秒起“。调用 completionHandler 后系统将把 App 进程挂起。
且系统必须在30秒内调用 completionHandler,否则进程将被杀死。
此外根据文档,系统会根据后台调用 completionHandler 的时间来决定后台唤起App的频率。
但时间也不能设置太短,比如设置1s,程序刚起来很多请求和操作还没完成就停止也不好,所以这里设置了10s
优化前
参考: juejin.cn/post/695159… juejin.cn/post/684490… juejin.cn/post/684490… juejin.cn/post/684490… mp.weixin.qq.com/s/3-Sbqe9gx… juejin.cn/post/692150… juejin.cn/post/695528… juejin.cn/post/695528… juejin.cn/post/691865… juejin.cn/post/684490… juejin.cn/post/684490… juejin.cn/post/684490…