记一次iOS启动时间优化

启动时间

启动时间是这里指从用户点击 APP 那一刻开始到用户看到第一个界面这中间的时间。以程序的入口main函数为分界点,我们将启动时间分为 pre-main 时间(t1)和 main 函数到第一个界面渲染完成时间(t2)这两个部分。

t1时间:是指main函数执行之前,系统加载可执行文件(.o文件集合),然后加载动态链接库dylb(dyld是一个专门用来加载动态链接库的库),dyld从可执行文件的依赖开始, 递归加载所有可执行文件的依赖动态链接库。

t2时间:是从main函数开始到- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。

pre-main Time

首先从缩短t1的时间开始优化,我们可以通过配置Edit scheme ——> Arguments ——> Environment Variables 添加 DYLD_PRINT_STATISTICS 设置Value为1。运行后,系统会打印出pre-main的时间。
记一次iOS启动时间优化_第1张图片

运行结果如下图:
记一次iOS启动时间优化_第2张图片
可以看出pre-main总耗时在914.04毫秒,将近1秒钟。从图中可以看到pre-mian分成几个步骤。我们可以从每个阶段进行优化,达到最终优化 效果。

dylib loading time

dylib loading time:动态库加载时间,可以在项目编译完成后在Finder找到xxx.app里面的同名xxx文件执行 otool命令。

$ otool -L xxx

终端会输出所有link的动态链接库(包括一些隐藏的lib),动态链接库包括:iOS 中用到的所有系统 framework,加载OCruntime的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。

加载dylib的顺序

加载dylib

分析每个dylib(大部分是iOS系统的),找到其Mach-O文件,

打开并读取验证有效性,找到代码签名注册到内核,

最后对dylib的每个segment调用mmap()。

系统其实对系统库的加载已经做过优化,针对这一步骤的优化我们可以做以下尝试:

  • 减少非系统库的依赖。
  • 合并已有的dylib和使用静态库(static archives),减少dylib的使用个数。

rebase/binding time

dylib加载完成之后,它们处于相互独立的状态,需要绑定起来。

在dylib的加载过程中,系统为了安全考虑,引入了ASLR(Address Space Layout Randomization)技术和代码签名。
由于ASLR的存在,镜像(Image,包括可执行文件、dylib和bundle)会在随机的地址上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。
Rebase在前,Bind在后,Rebase做的是将镜像读入内存,修正镜像内部的指针,性能消耗主要在IO。
Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。

优化该阶段的关键在于减少__DATA segment中的指针数量。我们可以优化的点有:

  • 减少Objc类数量, 减少selector数量。
  • 减少C++虚函数数量
  • 转而使用swift stuct(其实本质上就是为了减少符号的数量)

ObjC setup time

大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会注册所有声明过的ObjC类,将分类插入到类的方法列表里。这个步骤的优化和前面的优化点差不多,所以我们无需在多做优化。

initializer time

到了这一阶段,dyld开始运行程序的初始化函数,调用每个Objc类和分类的+load方法,调用C/C++ 中的构造器函数(用attribute((constructor))修饰的函数),和创建非基本类型的C++静态全局变量。Initializers阶段执行完后,dyld开始调用main()函数。
所以我们针对这一步骤的优化点是:

  • 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize
  • 减少构造器函数个数,在构造器函数里少做些事情
  • 减少C++静态全局变量的个数

main time

在这一阶段的优化在于减少didFinishLaunchingWithOptions的工作量,所以我们的重点在于给didFinishLaunchingWithOptions’减负’:

  • 我们知道在使用很多第三方库的时候,很多时候第三方库都推荐在didFinishLaunchingWithOptions完成初始化。但其实我们找到可以延迟加载的库,做延迟加载处理。比如推迟到最先显示的控制器上。
  • 梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
  • 避免在首页控制器的viewDidLoad和viewWillAppear做太多事情。

实际优化

在能够优化的措施中,

pre-main 阶段

  • 删除无用的dylib,检查Xcode中Linked Frameworks and Libraries中的无效dylib,或者删除自认为不用的库,然后编译测试是否成功。可以对项目熟悉的人来说工作量还不算多。
  • 使用LSUnusedResources排查项目中未使用的类,合并项目中的重复分类。
  • 全局搜索+load方法,将多个类中+load的实现延迟到+initiailize中。

main 阶段

  • 主要是减少didFinishLaunchingWithOptions中方法调用,延迟加载三方库。

启动时间的优化如果过早的话其实并没有什么明显的区别,而且如果业务上有广告的展示需求,那么启动时间优化其实只不过是让用户提早看到广告而已。手动滑稽。

引用

  • iOS 程序 main 函数之前发生了什么
  • 阿里数据iOS端启动速度优化的一些经验
  • 今日头条iOS客户端启动速度优化

你可能感兴趣的:(iOS,启动优化)