iOS客户端启动速度优化实践

应用启动时间,直接影响用户对一款应用的判断和使用体验。头条主app自己就包含很是多而且复杂度高的业务模块(如新闻、视频等),也接入了不少第三方的插件,这势必会拖慢应用的启动时间,本着精益求精的态度和对用户体验的追求,咱们但愿在业务扩张的同时最大程度的优化启动时间。html

技术调研ios

先说结论:面试

t(App总启动时间) = t1(main()以前的加载时间) + t2(main()以后的加载时间)。

t1 = 系统dylib(动态连接库)和自身App可执行文件的加载;swift

t2 = main方法执行以后到AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展现。xcode

main()调用以前的加载过程缓存

App开始启动后, 系统首先加载可执行文件(自身App的全部.o文件的集合),而后加载动态连接库dyld,dyld是一个专门用来加载动态连接库的库。 执行从dyld开始,dyld从可执行文件的依赖开始, 递归加载全部的依赖动态连接库。网络

动态连接库包括:iOS 中用到的全部系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。app

其实不管对于系统的动态连接库仍是对于App自己的可执行文件而言,他们都算是image(镜像),而每一个App都是以image(镜像)为单位进行加载的,那么image究竟包括哪些呢?dom

什么是image异步

  1. executable可执行文件 好比.o文件。
  2. dylib 动态连接库 framework就是动态连接库和相应资源包含在一块儿的一个文件夹结构。
  3. bundle 资源文件 只能用dlopen加载,不推荐使用这种方式加载。

除了咱们App自己的可行性文件,系统中全部的framework好比UIKit、Foundation等都是以动态连接库的方式集成进App中的。

iOS开发交流技术群:563513413,无论你是大牛仍是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!

系统使用动态连接有几点好处

代码共用:不少程序都动态连接了这些 lib,但它们在内存和磁盘中中只有一份。

易于维护:因为被依赖的 lib 是程序执行时才连接的,因此这些 lib 很容易作更新,好比libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升级直接换成libSystem.C.dylib 而后再替换替身就好了。

减小可执行文件体积:相比静态连接,动态连接在编译时不须要打进去,因此可执行文件的体积要小不少。

如上图所示,不一样进程之间共用系统dylib的_TEXT区,可是各自维护对应的_DATA区。

全部动态连接库和咱们App中的静态库.a和全部类文件编译后的.o文件最终都是由dyld(the dynamic link editor),Apple的动态连接器来加载到内存中。每一个image都是由一个叫作ImageLoader的类来负责加载(一一对应),那么ImageLoader又是什么呢?

什么是ImageLoader

image 表示一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,因此 ImageLoader 做用是将这些文件加载进内存,且每个文件对应一个ImageLoader实例来负责加载。

两步走:

  • 在程序运行时它先将动态连接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻)。
  • 再从可执行文件 image 递归加载全部符号。

固然全部这些都发生在咱们真正的main函数执行前。

动态连接库加载的具体流程

动态连接库的加载步骤具体分为5步:

  1. load dylibs image 读取库镜像文件
  2. Rebase image
  3. Bind image
  4. Objc setup
  5. initializers

下面对每一步进行分析。

load dylibs image

在每一个动态库的加载过程当中, dyld须要:

  • 分析所依赖的动态库
  • 找到动态库的mach-o文件
  • 打开文件
  • 验证文件
  • 在系统核心注册文件签名
  • 对动态库的每个segment调用mmap()

一般的,一个App须要加载100到400个dylibs, 可是其中的系统库被优化,能够很快的加载。针对这一步骤的优化有:

  1. 减小非系统库的依赖
  2. 合并不是系统库
  3. 使用静态资源,好比把代码加入主程序

rebase/bind

因为ASLR(address space layout randomization)的存在,可执行文件和动态连接库在虚拟内存中的加载地址每次启动都不固定,因此须要这2步来修复镜像中的资源指针,来指向正确的地址。

rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。

rebase步骤先进行,须要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,因此这一步的瓶颈在IO。bind在其后进行,因为要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,因此这一步的瓶颈在于CPU计算。

经过命令行能够查看相关的资源指针:

xcrun dyldinfo -rebase -bind -lazy_bind myApp.App/myApp

优化该阶段的关键在于减小__DATA segment中的指针数量。咱们能够优化的点有:

  1. 减小Objc类数量, 减小selector数量
  2. 减小C++虚函数数量
  3. 转而使用swift stuct(其实本质上就是为了减小符号的数量)

Objc setup

这一步主要工做是:

  1. 注册Objc类 (class registration)
  2. 把category的定义插入方法列表 (category registration)
  3. 保证每个selector惟一 (selctor uniquing)

因为以前2步骤的优化,这一步实际上没有什么可作的。

initializers

以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。

在这里的工做有:

  1. Objc的+load()函数
  2. C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
  3. 非基本类型的C++静态全局变量的建立(一般是类或结构体)(non-trivial initializer) 好比一个全局静态结构体的构建,若是在构造函数中有繁重的工做,那么会拖慢启动速度

Objc的load函数和C++的静态构造函数采用由底向上的方式执行,来保证每一个执行的方法,均可以找到所依赖的动态库。

上图是在自定义的类XXViewController的+load方法断点的调用堆栈,清楚的看到整个调用栈和顺序:

  1. dyld 开始将程序二进制文件初始化
  2. 交由 ImageLoader 读取 image,其中包含了咱们的类、方法等各类符号
  3. 因为 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
  4. runtime 接手后调用 map_images 作解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历全部加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法

至此,可执行文件中和动态库全部的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这以后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)。

整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,动态连接依赖库,并由 runtime 负责加载成 objc 定义的结构,全部初始化工做结束后,dyld 调用真正的 main 函数。

若是程序刚刚被运行过,那么程序的代码会被dyld缓存,所以即便杀掉进程再次重启加载时间也会相对快一点,若是长时间没有启动或者当前dyld的缓存已经被其余应用占据,那么此次启动所花费的时间就要长一点,这就分别是热启动和冷启动的概念,以下图所示:

main()以前的加载时间如何衡量

那么问题就来了,那怎么衡量main()以前也就是time1的耗时呢,苹果官方提供了一种方法,那就是在真机调试的时候勾选dyld_PRINT_STATISTICS选项。

会获得以下形式的输出:

因而可知对于系统级别的动态连接库,由于苹果作了优化,因此耗时并很少,在这个awesome的例子中,自身App中的代码占用了总体时间的94.2%

咱们应用中一次典型的Log以下:

因而可知,最多的用时仍是在image加载和OC类的初始化,共占用总时长的79.3%,精简framework的引入和OC类有优化的空间。

总结一下:对于main()调用以前的耗时咱们能够优化的点有:

  • 减小没必要要的framework,由于动态连接比较耗时
  • check framework应当设为optional和required,若是该framework在当前App支持的全部iOS系统版本都存在,那么就设为required,不然就设为optional,由于optional会有些额外的检查
  • 合并或者删减一些OC类,关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类以下:

  • 删减一些无用的静态变量
  • 删减没有被调用到或者已经废弃的方法。方法见:

http://stackoverflow.com/ques...

https://developer.Apple.com/l...

  • 将没必要须在+load方法中作的事情延迟到+initialize中
  • 尽可能不要用C++虚函数(建立虚函数表有开销)

main()调用以后的加载时间

在main()被调用以后,App的主要工做就是初始化必要的服务,显示首页内容等。而咱们的优化也是围绕如何可以快速展示首页来开展。

App一般在AppDelegate类中的- (BOOL)Application:(UIApplication )Application didFinishLaunchingWithOptions:(NSDictionary )launchOptions方法中建立首页须要展现的view,而后在当前runloop的末尾,主动调用CA::Transaction::commit完成视图的渲染。而视图的渲染主要涉及三个阶段:

  1. 准备阶段 这里主要是图片的解码
  2. 布局阶段 首页全部UIView的- (void)layoutSubViews()运行
  3. 绘制阶段 首页全部UIView的- (void)drawRect:(CGRect)rect运行

再加上启动以后必要服务的启动、必要数据的建立和读取,这些就是咱们能够尝试优化的地方

所以,对于main()函数调用以前咱们能够优化的点有:

  • 不使用xib,直接视用代码加载首页视图
  • NSUserDefaults其实是在Library文件夹下会生产一个plist文件,若是文件太大的话一次能读取到内存中可能很耗时,这个影响须要评估,若是耗时很大的话须要拆分(需考虑老版本覆盖安装兼容问题)
  • 每次用NSLog方式打印会隐式的建立一个Calendar,所以须要删减启动时各业务方打的log,或者仅仅针对内测版输出log
  • 梳理应用启动时发送的全部网络请求,是否能够统一在异步线程请求

实测数据

创建了一个空的HelloWorld工程,只加入了pods中的代码,不包含主端的业务逻辑代码,一次典型的冷启动基本接近2s iPhone6 iOS9.3.5系统测试主要时间在加载动态库,类/方法的初始化还有符号地址绑定阶段。

一次典型的热启动数据以下:能够看到由于系统作了缓存方面的优化,比冷启动快了500ms加上头条主端业务逻辑代码以后一次典型的热启动耗时2.1s。

以上用时均为main()以前的加载耗时。

 main函数以后加载时间优化记录

NSUserDefaults是不是瓶颈

苹果官方文档提到NSUserDefaults加载的时候是整个plist配置文件所有load到内存中,目前头条主端当中NSUserDefaults存储了200多项缓存数据,所以怀疑可能拖慢启动速度,可是测试结果显示并不会。

经过符号断点+[NSUserDefaults standardUserDefaults]肯定最先一次的+load()从执行到结束耗时1.8ms,可见NSUserDefaults的初始化仅耗时1.8ms,并非启动耗时的瓶颈。

如何找到拖慢启动应用时长的瓶颈

为了找到瓶颈,咱们在启动以后的didFinishLauhcning方法开始执行到首页列表页的NewsListViewController的viewDidAppear方法,几乎每一个可能比较耗时的流程进行拆分和统计,获得统计数据以后发现:

主要耗时在首页UI构造和渲染(storyboard加载,tabBar/topBar渲染,开屏广告加载/cell注册/日志模块初始化这几个步骤)。

具体优化点

所以,针对于今日头条这个App咱们能够优化的点以下:

  1. 纯代码方式而不是storyboard加载首页UI。
  2. didFinishLaunching里的函数考虑可否挖掘能够延迟加载或者懒加载,须要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
  3. 对于一些与UI展现无关的业务,如微博认证过时检查、图片最大缓存空间设置等作延迟加载。
  4. 对实现了+load()方法的类进行分析,尽可能将load里的代码延后调用。
  5. 上面统计数据显示展现feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽可能去尝试少作,晚作,不作。

优化结果

以前曾经有一位同事已经作了必定的优化,好比启动以后展现闪屏广告图的同时初始化首页的列表页,当广告展现完成以后列表页也就渲染完成了。通过这一次优化以后的main()以后的启动总时长经过上线以后收集数据的验证达到了预期的效果。

相关文章
相关标签/搜索