iOS基础学习-2-启动时间优化

559 阅读5分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

上一篇文章聊了启动时间的监控和测量 : iOS基础学习-1-启动时间监控,这篇文章聊一下启动过程的时间优化。 上一篇也说了启动过程具体可分三个阶段:

  1. pre-main阶段
  2. main函数之后
  3. 首屏渲染完成之后 接下来我们具体看下各个阶段如何进行时间优化

pre-main阶段

这个阶段做的事情如下:

  1. 加载可执行文件(app的.o文件的集合)
  2. 加载动态连接库 - dylb loading
  3. 进行rebase指针调整和bind符号绑定 - rebase/binding
  4. OC运行时的初始处理(OC相关类的注册,category注册,selector唯一性检查等)- Objc setup
  5. 初始化(执行+load()方法,attribute(constructor)修饰的函数的调用、创建C++静态全局变量) - initializer

可以优化的点有:

  1. dylb loading 阶段,加载动态库
    • 这个阶段会去装载app使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取。对于Apple提供的的系统动态库,做了高度的优化。而对于开发者定义导入的动态库,则需要在花费更多的时间。Apple官方建议尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们。
  2. rebase/binding 阶段,
    • 这个阶段进行rebase指针调整和bind符号绑定,这个阶段可以通过减少App的Objective-C类,分类和Selector的个数。这样做主要是为了加快程序的整个动态链接, 在进行动态库的重定位和绑定(Rebase/binding)过程中减少指针修正的使用,加快程序机器码的生成;
  3. Objc setup 阶段
    • 大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会注册所有声明过的ObjC类,将分类插入到类的方法列表里,再检查每个selector的唯一性。 这个阶段的优化和上个阶段的优化是一样的,上个阶段优化好,这个阶段就不用优化
  4. initializers 阶段
    • 这一阶段,dyld开始运行程序的初始化函数,调用每个Objc类和分类的+load方法,调用C/C++ 中的构造器函数(用attribute((constructor))修饰的函数),和创建非基本类型的C++静态全局变量。Initializers阶段执行完后,dyld开始调用main()函数。
    • 这个阶段的优化有:1. 减少在+load方法中做事情,推迟到initializers方法内执行,2. 减少构造器函数个数,在构造器函数里少做些事情 3. 减少C++静态全局变量的个数

pre - main阶段的优化方案

  1. 尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们。
  2. 减少App的Objective-C类,分类和Selector的个数
  3. 减少在+load方法中做事情,推迟到initializers方法内执行
  4. 减少构造器函数个数,在构造器函数里少做些事情
  5. 减少C++静态全局变量的个数

main 之后

优化方案

  1. 检查启动路径上的方法调用,能滞后的滞后
  2. 二进制重排,减少启动过程的page in
  3. 动态库懒加载,减少启动过程代码加载量
  4. 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.验证

juejin.cn/post/695528…

动态库懒加载,减少启动过程代码加载量

juejin.cn/post/692150…

Background Fetch,减少冷启动次数

1. 设置Background Fetch

info.plist中的UIBackgroundModes中增加fetch

image.png

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/684490…

优化前

image.png


参考: 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…