优化 App 的启动时间

 转:http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/


参考 文章:

https://icetime17.github.io/2018/01/01/2018-01/APP启动优化的一次实践/

APP启动优化的一次实践

https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA

iOS App 启动性能优化

https://mp.weixin.qq.com/s/zeWfmAi0YnoQowcPpFhHUA

iOS启动时间优化

https://mp.weixin.qq.com/s/wBZFv_-l7MDtTdofIxS13A

今日头条iOS客户端启动速度优化

https://developer.apple.com/videos/play/wwdc2016/406/

http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/

优化 App 的启动时间  (最好)

http://www.zoomfeng.com/blog/launch-time.html

iOS启动时间优化



打印 启动时间 :

Total pre-main time:  94.33 milliseconds (100.0%)

 dylib loading time:  61.87 milliseconds (65.5%)

 rebase/binding time:   3.09 milliseconds (3.2%)

 ObjC setup time:  10.78 milliseconds (11.4%)

 initializer time:  18.50 milliseconds (19.6%)

 slowest intializers :

 libSystem.B.dylib :   3.59 milliseconds (3.8%)

 libBacktraceRecording.dylib :   3.65 milliseconds (3.8%)

 GTFreeWifi :   7.09 milliseconds (7.5%)



启动时间线:

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers



一、了解

0、Mach-O 镜像 加载

所以在多个进程加载Mach-O 镜像时__TEXT 和__LINKEDIT 因为只读,都是可以共享内存的。而__DATA 因为可读写,就会产生dirty page。当dyld 执行结束后,__LINKEDIT 就没用了,对应的内存页会被回收。

1、安全

ASLR(Address Space Layout Randomization):地址空间布局随机化,镜像会在随机的地址上加载。这其实是一二十年前的旧技术了。

代码签名:可能我们认为Xcode 会把整个文件都做加密hash 并用做数字签名。其实为了在运行时验证Mach-O 文件的签名,并不是每次重复读入整个文件,而是把每页内容都生成一个单独的加密散列值,并存储在__LINKEDIT 中。这使得文件每页的内容都能及时被校验确并保不被篡改。

2、从exec() 到main()

exec() 是一个系统调用。系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(因为使用ASLR)。并将起始位置到0x000000 这段范围的进程权限都标记为不可读写不可执行。如果是32 位进程,这个范围至少是4KB;对于64 位进程则至少是4GB。NULL指针引用和指针截断误差都是会被它捕获。

3、dyld 加载dylib 文件

Unix 的前二十年很安逸,因为那时还没有发明动态链接库。有了动态链接库后,一个用于加载链接库的帮助程序被创建。在苹果的平台里是dyld,其他Unix 系统也有ld.so。 当内核完成映射进程的工作后会将名字为dyld 的Mach-O 文件映射到进程中的随机地址,它将PC 寄存器设为dyld 的地址并运行。dyld 在应用进程中运行的工作是加载应用依赖的所有动态链接库,准备好运行所需的一切,它拥有的权限跟应用一样。


二、加载Dylib

从主执行文件的header 获取到需要加载的所依赖动态库列表,而header 早就被内核映射过。然后它需要找到每个dylib,然后打开文件读取文件起始位置,确保它是Mach-O 文件。接着会找到代码签名并将其注册到内核。然后在dylib 文件的每个segment 上调用mmap()。应用所依赖的dylib 文件可能会再依赖其他dylib,所以dyld 所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载100 到400 个dylib 文件,但大部分都是系统dylib,它们会被预先计算和缓存起来,加载速度很快。

1、Fix-ups

在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个dylib 的调用另一个dylib。这时需要加很多间接层。

现代code-gen 被叫做动态PIC(Position Independent Code),意味着代码可以被加载到间接的地址上。当调用发生时,code-gen 实际上会在__DATA 段中创建一个指向被调用者的指针,然后加载指针并跳转过去。

所以dyld 做的事情就是修正(fix-up)指针和数据。Fix-up 有两种类型,rebasing 和binding。

2、Rebasing 和Binding

Rebasing:在镜像内部调整指针的指向

Binding:将指针指向镜像外部的内容

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

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

通过这个命令可以查看所有的Fix-up。rebase,bind,weak_bind,lazy_bind 都存储在__LINKEDIT 段中,并可通过LC_DYLD_INFO_ONLY 查看各种信息的偏移量和大小。

 建议用MachOView 查看更加方便直观。

 从dyld 源码层面简要介绍下Rebasing 和Binding 的流程。

ImageLoader是一个用于加载可执行文件的基类,它负责链接镜像,但不关心具体文件格式,因为这些都交给子类去实现。每个可执行文件都会对应一个ImageLoader 实例。ImageLoaderMachO 是用于加载Mach-O 格式文件的ImageLoader 子类,而ImageLoaderMachOClassic 和ImageLoaderMachOCompressed都继承于ImageLoaderMachO,分别用于加载那些__LINKEDIT 段为传统格式和压缩格式的Mach-O 文件。

 因为dylib 之间有依赖关系,所以ImageLoader 中的好多操作都是沿着依赖链递归操作的,Rebasing 和Binding 也不例外,分别对应着recursiveRebase() 和recursiveBind() 这两个方法。因为是递归,所以会自底向上地分别调用doRebase() 和doBind() 方法,这样被依赖的dylib 总是先于依赖它的dylib 执行Rebasing 和Binding。传入doRebase() 和doBind() 的参数包含一个LinkContext 上下文,存储了可执行文件的一堆状态和相关的函数。

 在Rebasing 和Binding 前会判断是否已经Prebinding。如果已经进行过预绑定(Prebinding),那就不需要Rebasing 和Binding 这些Fix-up 流程了,因为已经在预先绑定的地址加载好了。

ImageLoaderMachO实例不使用预绑定会有5个原因:

1.       Mach-O Header 中MH_PREBOUND 标志位为0

2.       镜像加载地址有偏移(这个后面会讲到)

3.       依赖的库有变化

4.       镜像使用flat-namespace,预绑定的一部分会被忽略

5.       LinkContext 的环境变量禁止了预绑定


ImageLoaderMachO中doRebase() 做的事情大致如下:


1.       如果使用预绑定,fgImagesWithUsedPrebinding计数加一,并return;否则进入第二步

2.       如果MH_PREBOUND 标志位为1(也就是可以预绑定但没使用),且镜像在共享内存中,重置上下文中所有的lazy pointer。(如果镜像在共享内存中,稍后会在Binding 过程中绑定,所以无需重置)

3.       如果镜像加载地址偏移量为0,则无需Rebasing,直接return;否则进入第四步

4.       调用rebase() 方法,这才是真正做Rebasing 工作的方法。如果开启TEXT_RELOC_SUPPORT 宏,会允许rebase() 方法对__TEXT 段做写操作来对其进行Fix-up。所以其实__TEXT 只读属性并不是绝对的。

ImageLoaderMachOClassic和ImageLoaderMachOCompressed 分别实现了自己的doRebase() 方法。实现逻辑大同小异,同样会判断是否使用预绑定,并在真正的Binding 工作时判断TEXT_RELOC_SUPPORT 宏来决定是否对__TEXT 段做写操作。最后都会调用setupLazyPointerHandler在镜像中设置dyld 的entry point,放在最后调用是为了让主可执行文件设置好__dyld 或__program_vars。


3、Rebasing

 在过去,会把dylib 加载到指定地址,所有指针和数据对于代码来说都是对的,dyld 就无需做任何fix-up 了。如今用了ASLR 后会将dylib 加载到新的随机地址(actual_address),这个随机的地址跟代码和数据指向的旧地址(preferred_address)会有偏差,dyld 需要修正这个偏差(slide),做法就是将dylib 内部的指针地址都加上这个偏移量,偏移量的计算方法如下:

Slide = actual_address - preferred_address

 然后就是重复不断地对__DATA 段中需要rebase 的指针加上这个偏移量。这就又涉及到page fault 和COW。这可能会产生I/O 瓶颈,但因为rebase 的顺序是按地址排列的,所以从内核的角度来看这是个有次序的任务,它会预先读入数据,减少I/O 消耗。

4、Binding

Binding 是处理那些指向dylib 外部的指针,它们实际上被符号(symbol)名称绑定,也就是个字符串。之前提到__LINKEDIT 段中也存储了需要bind 的指针,以及指针需要指向的符号。dyld 需要找到symbol 对应的实现,这需要很多计算,去符号表里查找。找到后会将内容存储到__DATA 段中的那个指针中。Binding 看起来计算量比Rebasing 更大,但其实需要的I/O 操作很少,因为之前Rebasing 已经替Binding 做过了。

5、ObjC Runtime

 Objective-C 中有很多数据结构都是靠Rebasing 和Binding 来修正(fix-up)的,比如Class 中指向超类的指针和指向方法的指针。

 ObjC 是个动态语言,可以用类的名字来实例化一个类的对象。这意味着ObjC Runtime 需要维护一张映射类名与类的全局表。当加载一个dylib 时,其定义的所有的类都需要被注册到这个全局表中。

 C++ 中有个问题叫做易碎的基类(fragile base class)。ObjC 就没有这个问题,因为会在加载时通过fix-up 动态类中改变实例变量的偏移量。

在ObjC 中可以通过定义类别(Category)的方式改变一个类的方法。有时你想要添加方法的类在另一个dylib 中,而不在你的镜像中(也就是对系统或别人的类动刀),这时也需要做些fix-up。

ObjC 中的selector 必须是唯一的。

6、Initializers

 C++ 会为静态创建的对象生成初始化器。而在ObjC 中有个叫+load 的方法,然而它被废弃了,现在建议使用+initialize。

对比详见:http://stackoverflow.com/questions/13326435/nsobject-load-and-initialize-what-do-they-do

 现在有了主执行文件,一堆dylib,其依赖关系构成了一张巨大的有向图,那么执行初始化器的顺序是什么?自顶向上!按照依赖关系,先加载叶子节点,然后逐步向上加载中间节点,直至最后加载根节点。这种加载顺序确保了安全性,加载某个dylib 前,其所依赖的其余dylib 文件肯定已经被预先加载。

 最后dyld 会调用main() 函数。main() 会调用UIApplicationMain()。




三、优化启动时间

可以针对App启动前的每个步骤进行相应的优化工作。

1、加载Dylib

 之前提到过加载系统的dylib 很快,因为有优化。但加载内嵌(embedded)的dylib 文件很占时间,所以尽可能把多个内嵌dylib 合并成一个来加载,或者使用static archive。使用dlopen() 来在运行时懒加载是不建议的,这么做可能会带来一些问题,并且总的开销更大。

2、Rebase/Binding

 之前提过Rebaing 消耗了大量时间在I/O 上,而在之后的Binding 就不怎么需要I/O 了,而是将时间耗费在计算上。所以这两个步骤的耗时是混在一起的。

 之前说过可以从查看__DATA 段中需要修正(fix-up)的指针,所以减少指针数量才会减少这部分工作的耗时。对于ObjC 来说就是减少Class,selector 和category 这些元数据的数量。从编码原则和设计模式之类的理论都会鼓励大家多写精致短小的类和方法,并将每部分方法独立出一个类别,其实这会增加启动时间。对于C++ 来说需要减少虚方法,因为虚方法会创建vtable,这也会在__DATA 段中创建结构。虽然C++ 虚方法对启动耗时的增加要比ObjC 元数据要少,但依然不可忽视。最后推荐使用Swift 结构体,它需要fix-up 的内容较少。

3、ObjC Setup

针对这步所能事情很少,几乎都靠Rebasing 和Binding 步骤中减少所需fix-up 内容。因为前面的工作也会使得这步耗时减少。

4、Initializer

显式初始化

 使用+initialize 来替代+load,如果 一个类中+initialize 和 +load 代码同时存在,则这种优化无效果。

 不要使用__atribute__((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。比如使用dispatch_once(),pthread_once() 或std::once()。也就是在第一次使用时才初始化,推迟了一部分工作耗时。

隐式初始化

对于带有复杂(non-trivial)构造器的C++ 静态变量:

1.       在调用的地方使用初始化器。

2.       只用简单值类型赋值(POD:Plain Old Data),这样静态链接器会预先计算__DATA 中的数据,无需再进行fix-up 工作。

3.       使用编译器warning 标志-Wglobal-constructors 来发现隐式初始化代码。

4.       使用Swift 重写代码,因为Swift 已经预先处理好了,强力推荐。

  不要在初始化方法中调用dlopen(),对性能有影响。因为dyld 在App 开始前运行,由于此时是单线程运行所以系统会取消加锁,但dlopen() 开启了多线程,系统不得不加锁,这就严重影响了性能,还可能会造成死锁以及产生未知的后果。所以也不要在初始化器中创建线程。

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

推荐阅读更多精彩内容

  • 这是一篇 WWDC 2016 Session 406 的学习笔记,从原理到实践讲述了如何优化 App 的启动时间。...
    MTDeveloper阅读 736评论 0 1
  • 这是一篇 WWDC 2016 Session 406 的学习笔记,从原理到实践讲述了如何优化 App 的启动时间。...
    茗涙阅读 1,844评论 0 3
  • 这是一篇 WWDC 2016 Session 406 的学习笔记,从原理到实践讲述了如何优化 App 的启动时间。...
    请叫我周小帅阅读 506评论 0 2
  • 背景 一个项目做的时间长了,启动流程往往容易杂乱,库也用的越来越多,APP的启动时间也会慢慢变长。本次将针对iOS...
    酱油瓶2阅读 3,416评论 0 12
  • App 运行理论 理论速成Mach-O 术语Mach-O 是针对不同运行时可执行文件的文件类型。文件类型:Exec...
    未明一二阅读 514评论 1 3