打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
iOS启动原理(一)

背景


iOS的启动过程一直比较神秘,这方面的资料也不是太多,大多数的资料都来自2016年WWDC的一篇视频,本文的大部分内容来自于视频,算是视频的一个归纳总结再加上自己的一点点感悟吧。


启动的过程

dyld是App的启动器,启动的大部分事情都由dyld完成,iOS的启动大致分为几个部分:


  1. 内核将App的执行文件加载到随机地址空间(加载到随机地址主要是因为ASLR技术)

  2. 内核将dyld的执行文件加载到随机地址空间

  3. 内核执行dyld文件

  4. dyld启动App

    1. dyld加载所有App所依赖的dylibs(动态库)

    2. 执行rebasing/binding修复地址

    3. Objc Setup

    4. initialize

  5. dyld调用App中的main(),将主动权交还给App


手机内核只负责将App的执行文件和dyld加载到内存中,然后所有的启动工作都交给了dyld。


dyld加载App依赖的dylibs


dyld拿到App的执行文件后,首先从文件的header中解析出App依赖的dylib列表,找到每一个依赖的dylib。打开并读取dylib文件的起始位置,验证签名,确保dylib没有被篡改。验证签名后,对dylib中的每个segment调用mmap()


segment


一般每个Mach-O文件都会有三个segment:

__TEXT: 一般处于文件的头部位置,包含Mach header,被执行的代码,和只读常量,只读可执行(r-x)。由于不会被更改,所以读到内存中后可复用
__DATA: 包含各种变量,可读写(rw-),由于可以被更改,所以不可复用
__LINKEDIT: 包含函数名称和对应的地址,只读(r--)


mmap()


文件读入内存并不用一次性读入整个文件,它可以使用分页映射(mmap())的方式进行读取。也就是用到哪个segment,再将哪个segment读入内存,实现文件读入的懒加载。


同时同一个Mach-O文件中的segment也可以映射到多个进程,实现进程之间的内存共享。__TEXT__LINKEDIT段都是只读的,不会有进程对它进行修改,它们是可以让所有进程共享的,大家都使用同一份内容。然而__DATA段却不是这样,__DATA是可读写的,当某一个进程需要对它进行修改时,需要先copy一份出来,映射到新的 RAM 页上。让这个进程拥有自己独立的内存拷贝,进行修改。这就是Copy-On-Write技术,简称COW。


由于__TEXT__LINKEDIT段可以进程间共享,只需要在第一次使用的时候进行IO操作,后续即可直接使用,所以App在第一次启动时会比较费时,因为所有的segment读取都需要进行IO操作。后续启动,会快很多,很多segment已经映射到内存中,会被缓存起来,二次启动直接使用,不需要进行IO操作,这就有了iOS中冷启动和热启动的概念:


  1. 冷启动:新安装App或者手机重启后,第一次启动。手机需要加载所有的segment

  2. 热启动:启动过App后,再次启动。内存中缓存的segment可以直接复用。


执行rebasing/binding修复地址


由于App和每个dylib加载到的都是随机地址空间,代码中原来的函数地址跟真实的函数地址会有差异。修复这个差异的过程就是rebasing和binding。其中rebasing主要做的是image内部的修复,binding主要做的是image间的修复。


Rebasing


对于Image内部的函数,假设它的原地址是A,对应当前地址空间下的新地址是B。那么它所有的函数指针都需要加上地址差(B-A)。所有的Rebasing过程就是从__LINKEDIT取出函数指针,修改函数指针,存入__DATA中,供函数调用。(原始的函数指针存在__LINKEDIT中,修改后的数据存在__DATA中)

之前说到,加载文件使用的是mmap技术,__LINKEDITDATA段是在第一次使用时才会执行IO操作,加载到内存中。所以Rebasing阶段,耗时主要是在IO操作上。


Binding


image间的函数指针,实际是被符号名称绑定的,为了找到对应的函数实现,dyld需要去符号表中根据符号名称查找,找到后将地址存到__DATA中对应函数指针中。由于IO操作在rebasing阶段已经在做了,所以binding阶段主要耗时在符号表查找的这个过程,这个过程的主要瓶颈在CPU计算上。


Objc Setup


Objc是一门动态语言,为了维持它的动态性,在启动时,需要将类的名称和类的方法都注册起来。Objc Setup阶段,主要是做Class的注册,Method的注册和Category的注册。


一个好的设计模式,一般都推崇写很多类,每个类尽量简单,写很多Category,每个Category都只包含独立模块的方法。但是从启动速度的角度来说,尽量减少类,Category和方法,才会让Objc Setup阶段耗时更少。


initialize


当所有的Class和method都注册过后,系统需要做一些初始化的工作,对于Objective-C而言,主要是需要调用各个类的+load方法,所以项目中应该尽量避免使用+load方法,正常的初始化工作,可以在initialize中实现。StackOverflow上有详细的关于+loadinitialize的对比


End


当上面所有阶段执行完成之后,dyld会调用main()函数,将主动权交还给App。之后才会调用到didFinishLaunch中的代码。


上面介绍的启动时间主要是main()函数之前的启动时间,正常这个时间控制在400ms以内就可以算一个启动速度优异的App了。正常我们关注更多的可能是main()函数后didFinishLaunch中代码的执行时间。但是对用户而言,main()函数之前的时间也是启动的一部分。往往这部分时间也不短,所以不能掉以轻心哦~


作者:小笨狼
链接:https://www.jianshu.com/p/4fe773d6da4c#comments
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
如何优化 App 的启动时间
iOS底层原理(九)性能优化
iOS App冷启动治理:来自美团外卖的实践
今日头条iOS客户端启动速度优化
iOS逆向工程
Mac os x 加载 dylib
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服