应用启动时间,直接影响用户对一款应用的判断和使用体验。头条主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异步
除了咱们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实例来负责加载。
两步走:
固然全部这些都发生在咱们真正的main函数执行前。
动态连接库加载的具体流程
动态连接库的加载步骤具体分为5步:
下面对每一步进行分析。
load dylibs image
在每一个动态库的加载过程当中, dyld须要:
一般的,一个App须要加载100到400个dylibs, 可是其中的系统库被优化,能够很快的加载。针对这一步骤的优化有:
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中的指针数量。咱们能够优化的点有:
Objc setup
这一步主要工做是:
因为以前2步骤的优化,这一步实际上没有什么可作的。
initializers
以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。
在这里的工做有:
attribute((constructor)) void DoSomeInitializationWork()
Objc的load函数和C++的静态构造函数采用由底向上的方式执行,来保证每一个执行的方法,均可以找到所依赖的动态库。
上图是在自定义的类XXViewController的+load方法断点的调用堆栈,清楚的看到整个调用栈和顺序:
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()调用以前的耗时咱们能够优化的点有:
http://stackoverflow.com/ques...
https://developer.Apple.com/l...
main()调用以后的加载时间
在main()被调用以后,App的主要工做就是初始化必要的服务,显示首页内容等。而咱们的优化也是围绕如何可以快速展示首页来开展。
App一般在AppDelegate类中的- (BOOL)Application:(UIApplication )Application didFinishLaunchingWithOptions:(NSDictionary )launchOptions
方法中建立首页须要展现的view,而后在当前runloop的末尾,主动调用CA::Transaction::commit
完成视图的渲染。而视图的渲染主要涉及三个阶段:
- (void)layoutSubViews()
运行- (void)drawRect:(CGRect)rect
运行再加上启动以后必要服务的启动、必要数据的建立和读取,这些就是咱们能够尝试优化的地方
所以,对于main()函数调用以前咱们能够优化的点有:
实测数据
创建了一个空的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咱们能够优化的点以下:
didFinishLaunching
里的函数考虑可否挖掘能够延迟加载或者懒加载,须要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。+load()
方法的类进行分析,尽可能将load里的代码延后调用。viewDidLoad
以及viewWillAppear
方法中尽可能去尝试少作,晚作,不作。优化结果
以前曾经有一位同事已经作了必定的优化,好比启动以后展现闪屏广告图的同时初始化首页的列表页,当广告展现完成以后列表页也就渲染完成了。通过这一次优化以后的main()以后的启动总时长经过上线以后收集数据的验证达到了预期的效果。