iOS应用启动速度优化

背景

  • 项目规模增大,第三方 SDK 增多,app 启动速度受到影响
  • 创新的页面,丰富的功能,如何保障用户体验

iOS 启动过程比较复杂,系统做了很多的加载和初始化工作,本文只涉及可以优化的部分,其他无需关注。

App 运行分析

1、exec()

  • main() 函数是整个程序的入口,在程序启动之前,系统会调用 exec() 函数。
  • 作用是 根据指定的文件名找到可执行文件,并用它来取代进程的内容。

2、Dyld

  • 动态链接器,加载应用所依赖的所有动态链接库。
  • dylib, 既动态库。
  • 获取到需要加载的动态库列表,找到对应的dylib,打开 Mach-O 文件,将其注册到内核。
  • 大部分都是系统 dylib, 系统的 dylib 会被预先计算和缓存起来,所以加载速度快。
  • 想要深究 dylib,可参考源码

3、reabse/binding

  • 在加载所有的动态链接库之后,他们只是处在相互独立的状态,需要将他们绑定起来,这就是 rebase/binding
  • iOS 4.3 之前,会把 dylib 加载到指定地址,所有指针和数据对于代码来说都是固定的, dylib 就无需做 reabse/binding
  • iOS 4.3 后引入了 ASLRdylib 会被加载到随机地址,这个随机的地址跟代码和数据指向的旧地址会有偏差,dylib 需要修正这个偏差,会重复不断的对需要 rebase 的指针加上一个偏移量。

    • Rebasing: 在镜像内部调整指针的指向。
    • Binding:将指针指向镜像外部的内容。

4、Objc setup

这一步的主要工作是:

  • 注册 Objc 类, Objc Runtime 需要维护一张映射类名与类的全局表。当加载一个 dylib 时,其定义的所有的类都需要被注册到这个全局表中。
  • category 中定义的方法插入方法列表。
  • 保证每个 selector 的唯一性。

5、initializer

  • Objcload 函数。
  • C++ 的构造函数属性函数。

优化

pre-main 阶段优化

app 启动有两种类型:

  • Cold launch: app is not in kernel buffer cache.
  • Warm launch: app and data already in mermory.

Cold launcg 耗时是我们需要关注的,在 xcode 8 后,我们可以测量 main 之前的耗时,方法如下: 在 Xcode 中 Edit scheme -> Run -> Arguments 添加环境变量 DYLD_PRINT_STATISTICS,并设置值为 1。然后运行 App,观察控制台输出。(因为dyld会有缓存,所以每次测量时需要重启手机)

打印如下:

1
2
3
4
5
6
7
8
9
Total pre-main time: 220.03 milliseconds (100.0%)
dylib loading time: 153.21 milliseconds (69.6%)
rebase/binding time: 4.31 milliseconds (1.9%)
ObjC setup time: 9.17 milliseconds (4.1%)
initializer time: 53.08 milliseconds (24.1%)
slowest intializers :
libSystem.B.dylib : 3.75 milliseconds (1.7%)
libMainThreadChecker.dylib : 20.14 milliseconds (9.1%)
oa : 47.09 milliseconds (21.4%)

可以看到 pre-main 主要经过 dylib loading -> rebase/binding -> ObjC setup -> initializer
这几个步骤上文已经介绍,我们来看这几个不走哪些使我们能优化的。

1. 加载 dylib

  • 减少不需要的动态库
  • 合并非系统库

2. rebase/binding

  • 对于 Objc 来说就是减少 classsecectorcategory
  • 为了不破坏代码易读性,只能减少不必要的类、代码。

3、Objc setup

4、initializers

  • 减少 +load 方法调用

main -> 首页 优化

经历过 pre-main 阶段,dylib 会调用 main() 函数,main() 函数会调用 UIApplicationMain()。如果在首页展示前我们处理了不必要的业务肯定会影响首页的加载。

这里我们可以借助 instruments工具找出耗时的操作,规避代码上的阻塞写法。为了尽快展示首页,尽量将优先级低的任务后加载。

任务分级

将启动中的所有任务进行梳理和分级,根据任务的优先级来制定优化方案。

  1. 一级:在首页加载前需要初始化的任务,优先级大于首屏展示。如:清理缓存、objection 注册,crashsdk 注册等必要的 sdk 注册。
  2. 二级: 构建 tabbar,首屏数据。
  3. 三级:优先级低的任务根据需要可以建立子线程,在首屏出来后加载耳朵任务, 如:检查更新、微信微博注册等

    我们的didfinishLaunching可以分为这三个状态,方便app在对应的时机执行对应的任务。

代码优化

在整理完app的启动任务后,如果我们代码本身就有问题,同样还是会影响用户体验,在此总结出代码中存在的问题。

  • 文件操作,图片下载等耗时任务不应阻塞主线程;
  • 临时变量过多,建议使用autoreleasepool 以便及时释放;
  • NSDateFormatter 重用;
  • Cell重用;
  • 合理使用单例;
  • 复杂的页面不要使用xib;
  • 避免循环引用;
  • 使用懒加载。

总结

关于app启动优化,可以从几方面入手:

  • 减少不必要的framework;
  • 清理项目中没有使用到的代码、类,避免冗余代码;
  • 慎用+load,第三方库已经使用了够多的+load方法(objection,rn…),在写代码时尽量用+initialize代替+load;
  • 梳理流程,优化代码,启动模块化。耗时操作不要在主线程;
  • 本地缓存。首页的数据离线化,优先展示本地缓存数据,等待网络数据返回之后更新缓存并展示;
  • 懒加载,使用时再去创建;
  • 网络优化,减少接口请求,接口合并,使用ip直连,除去DNS解析耗时,使用protobuf协议,使用http2协议。

转自 干货 | 途牛iOS客户端启动速度优化实践