iOS App启动过程

Mach-O


  • Header 头部,包含可以执行的CPU架构,比如x86,x86_64,arm7s,arm64
  • Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
  • Data,数据,包含load commands中需要的各个段(segment)的数据,每一个Segment都得大小是Page的整数倍。
    1) TEXT 代码段,只读(通常使用于frameworks,bundles,shared libraries)
    2)
    DATA 数据段,读写
    3) OBJC OC动态语言支持相关的数据
    4)
    IMPORT 仅IA-32平台生成
    5) __LINKEDIT 动态链接器使用的元数据(符号、字符串、入口)

dyld

dyld的全称是dynamic loader,它的作用是加载一个进程所需要的image,dyld是开源的

启动过程

  1. 加载dyld到App进程
  2. 加载动态库(包括所依赖的所有动态库)
  3. Rebase
  4. Bind
  5. 初始化Objective C Runtime
  6. 其它的初始化代码

加载动态库

dyld会首先读取mach-o文件的Header和load commands。
接着就知道了这个可执行文件依赖的动态库。例如加载动态库A到内存,接着检查A所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个App所依赖的动态库在100-400个左右,其中大多数都是系统的动态库,它们会被缓存到dyld shared cache,这样读取的效率会很高。

查看mach-o文件所依赖的动态库,可以通过MachOView的图形化界面(展开Load Command就能看到),也可以通过命令行otool。

1
otool -L demo

Rebase && Bind

有两种主要的技术来保证应用的安全:ASLR和Code Sign。

ASLR的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被映射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。

Code Sign相信大多数开发者都知晓,这里要提一点的是,在进行Code sign的时候,加密哈希不是针对于整个文件,而是针对于每一个Page的。这就保证了在dyld进行加载的时候,可以对每一个page进行独立的验证。

mach-o中有很多符号,有指向当前mach-o的,也有指向其他dylib的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?

mach-o中采用了PIC技术,全称是Position Independ code。当你的程序要调用printf的时候,会先在__DATA段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分:

  • Rebase 修正内部(指向当前mach-o文件)的指针指向
  • Bind 修正外部指针指向

之所以需要Rebase,是因为刚刚提到的ASLR使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在__LINKEDIT中。
可以通过MachOView查看:Dynamic Loader Info -> Rebase Info

1
xcrun dyldinfo -bind demo

Rebase解决了内部的符号引用问题,而外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的。

初始化Objective C Runtime

Objective C是动态语言,所以在执行main函数之前,需要把类的信息注册到一个全局的Table中。同时,Objective C支持Category,在初始化的时候,也会把Category中的方法注册到对应的类中,同时会唯一Selector,这也是为什么当你的Cagegory实现了类中同名的方法后,类中的方法会被覆盖。

另外,由于iOS开发时基于Cocoa Touch的,所以绝大多数的类起始都是系统类,所以大多数的Runtime初始化起始在Rebase和Bind中已经完成。

Initializers

主要包括几部分:

  • +load方法。
  • C/C++静态初始化对象和标记为attribute(constructor)的方法

注:+load方法已经被弃用了,如果你用Swift开发,你会发现根本无法去写这样一个方法,官方的建议是实用initialize。区别就是,load是在类装载的时候执行,而initialize是在类第一次收到message前调用。

上文的讲解是dyld2的加载方式。而最新的是dyld3加载方式略有不同:

dyld2是纯粹的in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。

dyld3则是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情:

  • 分析Mach-o Headers
  • 分析依赖的动态库
  • 查找需要Rebase & Bind之类的符号
  • 把上述结果写入缓存

这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。

启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。

在Xcode中,可以通过设置环境变量来查看App的启动时间,DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS。

优化启动时间

dylibs

启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。

合并动态库.比如公司内部由私有Pod建立了如下动态库:XXTableView, XXHUD, XXLabel,强烈建议合并成一个XXUIKit来提高加载速度。

Rebase & Bind & Objective C Runtime

Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:

  • 减少__DATA段中的指针数量。
  • 合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个删除无用的方法和类。
  • 多用Swift Structs,因为Swfit Structs是静态分发的。

Initializers

通常,我们会在+load方法中进行method-swizzling。

  • 用initialize替代load。不少同学喜欢用method-swizzling来实现AOP去做日志统计等内容,强烈建议改为在initialize进行初始化。
  • 减少atribute((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。
  • 不要创建线程
  • 使用Swfit重写代码。

转自:深入理解iOS App的启动过程

-------------本文结束感谢您的阅读-------------