iOS启动时间优化方案记录

image.png

1. APP启动时间

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

t1 = 系统dylib(动态链接库)和自身App可执行文件的加载; t2 = main方法执行之后到AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。

2. 针对于T2阶段的优化

成本最少,效果明显。

2.1 didFinishLaunchingWithOptions

一般在这个方法里进行初始化操作,并且有些是必须执行的,可以适当的根据功能的不同适当延迟其启动的时机:

  • 1.日志、统计等必须在 APP 一起动就最先配置的事件

  • 2.项目配置、环境配置、用户信息的初始化 、推送、IM等事件

  • 3.其他 SDK 和配置事件

  • 4.可以按需加载的配置,比如分享

优化方案:
第一类:可以仍然放在didFinishLaunchingWithOptions方法里面,
第二类:这个功能要在用户进入APP主体前要加载完,比如放在广告显示的时候
第三类:延迟执行部分业务逻辑和 UI 配置,可以放在第一个页面渲染完成之后,避免首屏加载时大量的本地/网络数据读取
第四类:在使用的时候可以再去加载

3. 针对于T1阶段的优化

3.1 测量时间

通过在工程的scheme中添加环境变量DYLD_PRINT_STATISTICS,设置值为1,App启动加载时Xcode的控制台就会有pre-main各个阶段的详细耗时输出。但是DYLD_PRINT_STATISTICS变量打印时间是iOS10以后才支持的功能,所以需要用iOS10系统及以上的机器来做测试。

Total pre-main time: 1.1 seconds (100.0%)
        dylib loading time: 458.55 milliseconds (38.8%)
      rebase/binding time: 145.48 milliseconds (12.3%)
          ObjC setup time: 28.99 milliseconds (2.4%)
          initializer time: 548.53 milliseconds (46.4%)
          slowest intializers :
            libSystem.B.dylib :   5.85 milliseconds (0.4%)
        libglInterpose.dylib : 376.73 milliseconds (31.8%)
                AFNetworking : 54.63 milliseconds (4.6%)
                    WKWebKit : 48.15 milliseconds (4.0%)

如果想查看更详细的信息,就设置DYLD_PRINT_STATISTICS_DETAILS为1:

total time: 2.4 seconds (100.0%)
  total images loaded:  459 (424 from dyld shared cache)
  total segments mapped: 114, into 10022 pages
  total images loading time: 1.7 seconds (71.8%)
  total load time in ObjC:  12.78 milliseconds (0.5%)
  total debugger pause time: 1.6 seconds (69.3%)
  total dtrace DOF registration time:   0.00 milliseconds (0.0%)
  total rebase fixups:  149,991
  total rebase fixups time:  12.35 milliseconds (0.5%)
  total binding fixups: 72,264
  total binding fixups time:  38.72 milliseconds (1.6%)
  total weak binding fixups time:  36.61 milliseconds (1.5%)
  total redo shared cached bindings time:  27.73 milliseconds (1.1%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load: 574.31 milliseconds (23.9%)
                         libSystem.B.dylib :   7.44 milliseconds (0.3%)
               libBacktraceRecording.dylib :   6.06 milliseconds (0.2%)
                libMainThreadChecker.dylib :  16.50 milliseconds (0.6%)
                      libglInterpose.dylib : 398.79 milliseconds (16.6%)
                       libMTLCapture.dylib :  14.42 milliseconds (0.6%)
                              AFNetworking :  55.91 milliseconds (2.3%)
                                ZWWKWebKit :  52.46 milliseconds (2.1%)
                           Demo :  18.29 milliseconds (0.7%)
total symbol trie searches:    283620
total symbol table binary searches:    0
total images defining weak symbols:  51
total images using weak symbols:  119
3.2 理论理解
3.2.1 Mach-O文件

Mach-O(Mach Object File Format)是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。App 编译生成的二进制可执行文件就是 Mach-O 格式的,iOS 工程所有的类编译后会生成对应的目标文件 .o 文件,而这个可执行文件就是这些 .o 文件的集合。

Mach-O 文件主要由三部分组成:

  • Mach header:描述 Mach-O 的 CPU 架构、文件类型以及加载命令等;
  • Load commands:描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令;
  • Data:Data 中的每个段(segment)的数据都保存在这里,每个段都有一个或多个 Section,它们存放了具体的数据与代码,主要包含这三种类型:
    1. __TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)。
    2. __DATA 包含全局变量,静态变量等。可读写(rw-)。
    3. __LINKEDIT 包含了加载程序的元数据,比如函数的名称和地址。只读(r–-)。
3.2.2 dylib

dylib也是一种 Mach-O 格式的文件,后缀名为 .dylib 的文件就是动态库(也叫动态链接库)。动态库是运行时加载的,可以被多个 App 的进程共用。
如果想知道 TestDemo 中依赖的所有动态库,可以通过下面的指令实现:

otool -L /TestDemo.app/TestDemo
3.2.3 dyld

动态链接器,其本质也是 Mach-O 文件,一个专门用来加载 dylib 文件的库。
dyld 位于 /usr/lib/dyld,可以在 mac 和越狱机中找到。dyld 会将 App 依赖的动态库和 App 文件加载到
内存后执行。

3.2.4 dyld shared cache

是动态库共享缓存,当需要加载的动态库非常多时,相互依赖的符号也更多了,为了节省解析处理符号的时间,OS X 和 iOS 上的动态链接器使用了共享缓存。OS X 的共享缓存位于 /private/var/db/dyld/,iOS 的则在 /System/Library/Caches/com.apple.dyld/。

当加载一个 Mach-O 文件时,dyld 首先会检查是否存在于共享缓存,存在就直接取出使用。每一个进程都会把这个共享缓存映射到了自己的地址空间中。这种方法大大优化了 OS X 和 iOS 上程序的启动时间。

3.2.5 images

images 在这里不是指图片,而是镜像。每个 App 都是以 images 为单位进行加载的。images 类型包括:

  1. executable:应用的二进制可执行文件;
  2. dylib:动态链接库;
  3. bundle:资源文件,属于不能被链接的 dylib,只能在运行时通过 dlopen() 加载。
3.2.6 imageLoader

image表示一个二进制文件,里面是被编译过的符号、代码等,所以ImageLoader作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。
两步走: 在程序运行时它先将动态链接的 image 递归加载, 再从可执行文件 image 递归加载所有符号。

3.2.7 framework

framework 可以是动态库,也是静态库,是一个包含 dylib、bundle 和头文件的文件夹。

3.3 启动过程分析与优化

启动一个应用时,系统会通过fork()方法来新创建一个进程,然后执行镜像通过exec()来替换为另一个可执行程序,然后执行如下操作:

  1. 把可执行文件加载到内存空间,从可执行文件中能够分析出 dyld 的路径;
  2. 把 dyld 加载到内存;
  3. dyld 从可执行文件的依赖开始,递归加载所有的依赖动态链接库 dylib 并进行相应的初始化操作。

结合上面 pre-main 打印的结果,我们可以大致了解整个启动过程如下图所示:

exec() -> Load Executable -> Load Dyld -> Load Dylibs -> Rebase -> Binding ->ObjCSetUp -> Initializers
3.3.1 Load Dylibs

这一步,指的是动态库加载。在此阶段,dyld 会:

  • 分析 App 依赖的所有 dylib;
  • 找到 dylib 对应的 Mach-O 文件;
  • 打开、读取这些 Mach-O 文件,并验证其有效性;
  • 在系统内核中注册代码签名;
  • 对 dylib 的每一个 segment 调用 mmap()。

一般情况下,iOS App 需要加载 100-400 个 dylibs。这些动态库包括系统的,也包括开发者手动引入的。其中大部分 dylib 都是系统库,系统已经做了优化,因此开发者更应关心自己手动集成的内嵌 dylib,加载它们时性能开销较大。

App 中依赖的 dylib 越少越好,Apple 官方建议尽量将内嵌 dylib 的个数维持在6个以内。

优化方案:

  1. 尽量不使用内嵌dylib
  2. 合并已有内嵌dylib
  3. 检查 framework 的 optional 和 required 设置,如果 framework 在当前的 App 支持的 iOS 系统版本中都存在,就设为 required,因为设为 optional 会有额外的检查导致加载变慢;
  4. 使用静态库作为代替;(不过静态库会在编译期被打进可执行文件,造成可执行文件体积增大,两者各有利弊,开发者自行权衡。)
  5. 懒加载 dylib。(但使用 dlopen() 对性能会产生影响,因为 App 启动时是原本是单线程运行,系统会取消加锁,但 dlopen() 开启了多线程,系统不得不加锁,这样不仅会使性能降低,可能还会造成死锁及未知的后果,不是很推荐这种做法。)
3.3.2 Rebase/Binding

指针重定位。

在 dylib 的加载过程中,系统为了安全考虑,引入了 ASLR(Address Space Layout Randomization)技术和代码签名。由于 ASLR 的存在,镜像会在新的随机地址(actual_address)上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide,slide=actual_address-preferred_address),因此 dyld 需要修正这个偏差,指向正确的地址。具体通过这两步实现:

第一步:Rebase,在 image 内部调整指针的指向。将 image 读入内存,并以 page 为单位进行加密验证,保证不会被篡改,性能消耗主要在 IO。

第二步:Binding,符号绑定。将指针指向 image 外部的内容。查询符号表,设置指向镜像外部的指针,性能消耗主要在 CPU 计算。

通过以下命令可以查看 rebase 和 bind 等信息:

xcrun dyldinfo -rebase -bind -lazy_bind TestDemo.app/TestDemo

通过 LC_DYLD_INFO_ONLY 可以查看各种信息的偏移量和大小。如果想要更方便直观地查看,推荐使用 MachOView 工具。

指针数量越少,指针修复的耗时也就越少。所以,优化该阶段的关键就是减少 __DATA 段中的指针数量。

优化方案:

  1. 减少 ObjC 类(class)、方法(selector)、分类(category)的数量,比如合并一些功能,删除无效的类、方法和分类等(可以借助 AppCode 的 Inspect Code 功能进行代码瘦身);
  2. 减少 C++ 虚函数;(虚函数会创建 vtable,这也会在 __DATA 段中创建结构。)
3.3.3 ObjC Setup

完成 Rebase 和 Bind 之后,通知 runtime 去做一些代码运行时需要做的事情:

  • dyld 会注册所有声明过的 ObjC 类;
  • 将分类插入到类的方法列表中;
  • 检查每个 selector 的唯一性。

优化方案:

Rebase/Binding 阶段优化好了,这一步的耗时也会相应减少。

3.3.4 Initializers

Rebase 和 Binding 属于静态调整(fix-up),修改的是 __DATA 段中的内容,而这里则开始动态调整,往堆和栈中写入内容。具体工作有:

  • 调用每个 Objc 类和分类中的 +load 方法;
  • 调用 C/C++ 中的构造器函数(用 attribute((constructor)) 修饰的函数);
  • 创建非基本类型的 C++ 静态全局变量。

优化方案:

  1. 尽量避免在类的 +load 方法中初始化,可以推迟到 +initiailize 中进行;(因为在一个 +load 方法中进行运行时方法替换操作会带来 4ms 的消耗)

  2. 避免使用 atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时再执行。比如用 dispatch_once()、pthread_once() 或 std::once(),相当于在第一次使用时才初始化,推迟了一部分工作耗时。:

  3. 减少非基本类型的 C++ 静态全局变量的个数。(因为这类全局变量通常是类或者结构体,如果在构造函数中有繁重的工作,就会拖慢启动速度)

3.3.5 总结pre-main 阶段可行的优化方案
  • 重新梳理架构,减少不必要的内置动态库数量;

  • 进行代码瘦身,合并或删除无效的ObjC类、Category、方法、C++ 静态全局变量等;

  • 将不必须在 +load 方法中执行的任务延迟到 +initialize 中;

  • 减少 C++ 虚函数。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342