App启动过程

栏目: IOS · 发布时间: 5年前

内容简介:iOS开发中,main函数是我们熟知的程序启动入口,但实际上并非真正意义上的入口,因为在我们运行程序,再到main方法被调用之间,程序已经做了许许多多的事情,比如我们熟知的runtime的初始化就发生在main函数调用前,还有程序动态库的加载链接也发生在这阶段。整个过程为:Mach-O文件格式是 OS X 与 iOS 系统上的可执行文件格式,像我们编译过程产生的.O文件,以及程序的可执行文件,动态库等都是Mach-O文件。有以下几种Mach-O

iOS开发中,main函数是我们熟知的程序启动入口,但实际上并非真正意义上的入口,因为在我们运行程序,再到main方法被调用之间,程序已经做了许许多多的事情,比如我们熟知的runtime的初始化就发生在main函数调用前,还有程序动态库的加载链接也发生在这阶段。

整个过程为:

  1. 系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径
  2. 加载dyld(the dynamic link editor,Apple 的动态链接器,系统 kernel 做好启动程序的初始准备后,交给 dyld 负责),dyld去初始化运行环境,开启缓存策略,
  3. dyld加载程序相关动态库,并对这些库进行链接,调用每个依赖库的初始化方法
  4. runtime被初始化
  5. ImageLoader:dyld把Image(包含我们的类、方法等)load进来
  6. runtime对加载进来的Image所有类进行类结构初始化,调用所有的load方法,Category方法也在此时被调用
  7. dyld返回main函数地址,main函数被调用
  8. main函数调用
  9. 执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions
  10. 初始化Window,初始化基础的ViewController结构
  11. 获取数据(Local DB/Network),展示给用户

二、main函数启动之前

Mach-O可执行文件

Mach-O文件格式是 OS X 与 iOS 系统上的可执行文件格式,像我们编译过程产生的.O文件,以及程序的可执行文件,动态库等都是Mach-O文件。有以下几种Mach-O

  • Executable 可执行文件
  • Dylib 动态库
  • Bundle 库:无法被连接的动态库,只能通过dlopen()加载
  • Image :指的是Executable,Dylib或者Bundle的一种,文中会多次使用Image这个名词。
  • Framework 库:动态库和对应的头文件和资源文件的集合

Mach-O的结构如下:

App启动过程
  • Header 头部,包含可以执行的CPU架构,比如x86,arm64
  • Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
  • Data,数据,包含load commands中需要的各个段(segment)的数据,每一个Segment都得大小是Page的整数倍。

更多Mach-O可查看《Mac OS X ABI Mach-O File Format Reference》

有两种方式可以查看一个APP动态调用的系统可执行文件

1、通过machoview,选择APP的可执行文件,可以看到

App启动过程

2、通过otool -L命令行查看

App启动过程

dyld

全程the dynamic loade,Apple 的动态链接器,系统 kernel 做好启动程序的初始准备后,交给 dyld 负责。

2017年,苹果引入了Dyld 3.0,但是只有系统APP采用这个,第三方APP都是采用Dyld 2.0。

Dyld 2.0的加载过程是:

App启动过程
  1. 解析 mach-o 文件,找到其依赖的库,并且递归的找到所有依赖的库,形成一张动态库的依赖图。iOS 上的大部分 app 都依赖 300 到 600 个动态链接库,所以这个步骤包含了较大的工作量。
  2. 匹配 mach-o 文件到自身的地址空间;
  3. 进行符号查找:比如 app 中调用了 printf 方法,就需要去系统库中查找到 printf 的地址,然后将地址拷贝到 app 中的函数指针中;
  4. 绑定和变基:由于 app 需要让地址空间配置随机加载,所以所有的指针都需要加上一个基地址;
  5. 运行初始化程序(Runtime、+load、+initialize),之后运行 main() 函数。

三、优化启动时间

main函数之后

Appdelegate

能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

  • 三方SDK初始化,比如Crash统计; 像分享之类的,可以等到第一次调用再出初始化。
  • 初始化某些基础服务,比如WatchDog,远程参数。
  • 启动相关日志,日志往往涉及到DB操作,一定要放到后台去做
  • 业务方初始化,这个交由每个业务自己去控制初始化时间。

启动业务的优化

建一个类来管理初始化,所有需要初始化的代码都在这里进行,分类初始化:

ViewController

延迟初始化那些不必要的UIViewController。

用Time Profiler找到元凶

Main之前

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是静态分发的。感兴趣的同学可以看看我之前这篇文章:《Swift进阶之内存模型和方法调度》

Initializers

通常,我们会在+load方法中进行method-swizzling,这也是Nshipster推荐的方式。

__atribute__((constructor))

四、Category

category实际上是个结构体,与Class结构体类似

struct _category_t {
	const char *name; // 类的名字,用于寻找类
	struct _class_t *cls; // 编译期为空,在runtime通过name找到类后获得
	const struct _method_list_t *instance_methods; // 实例方法
	const struct _method_list_t *class_methods; // 类方法
	const struct _protocol_list_t *protocols; // 协议
	const struct _prop_list_t *properties; // 关联属性
};
复制代码
  1. runtime的入口 _objc_init 方法里调用 _getObjc2CategoryList 获取category_t 的列表,然后再一一将category添加进去
  2. category是在runtime在把 类的结构已经初始化之后 ,加载进去的,因为内存布局已经确定,所以不能添加实例变量,只能通过添加关联属性的方式来添加“变量”。
  3. runtime 添加方法的时候会放在方法列表的前面 ,也就是说如果之前有相同名字的方法,根据运行时的特性,调用方法时将会调用category的方法,从而达到了“覆盖”的效果

五、iOS 静态库,动态库与 Framework

静态库与动态库的区别

首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别人使用 什么时候我们会用到库呢?一种情况是某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。另外一种情况是,对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。 上面提到库在使用的时候需要 Link,Link 的方式有两种,静态和动态,于是便产生了静态库和动态库。

静态库

静态库即静态链接库(Windows 下的 .lib,Linux 和 Mac 下的 .a)。之所以叫做静态,是因为静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。 静态库的好处很明显,编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。当然其缺点也很明显,就是会使用目标程序的体积增大。

动态库

动态库即动态链接库(Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib/.tbd)。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。 动态库的优点是,不需要拷贝到目标程序中,不会影响目标程序的体积,而且同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。同时,编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。动态库带来的问题主要是,动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux 下喜闻乐见的 lib not found 错误)。

iOS Framework

除了上面提到的 .a 和 .dylib/.tbd 之外,Mac OS/iOS 平台还可以使用 Framework。Framework 实际上是一种打包方式,将库的二进制文件,头文件和有关的资源文件打包到一起,方便管理和分发。

在 iOS 8 之前,iOS 平台不支持使用动态 Framework,开发者可以使用的 Framework 只有苹果自家的 UIKit.Framework,Foundation.Framework 等。这种限制可能是出于安全的考虑(见这里的讨论)。换一个角度讲,因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码,同时动态下载代码又是被苹果明令禁止的,没办法发挥出动态库的优势,实际上动态库也就没有存在的必要了。

由于上面提到的限制,开发者想要在 iOS 平台共享代码,唯一的选择就是打包成静态库 .a 文件,同时附上头文件(例如微信的SDK)。但是这样的打包方式不够方便,使用时也比较麻烦,大家还是希望共享代码都能能像 Framework 一样,直接扔到工程里就可以用。于是人们想出了各种奇技淫巧去让 Xcode Build 出 iOS 可以使用的 Framework,具体做法参考这里和这里,这种方法产生的 Framework 还有 “伪”(Fake) Framework 和 “真”(Real) Framework 的区别。

iOS 8/Xcode 6 推出之后,iOS 平台添加了动态库的支持,同时 Xcode 6 也原生自带了 Framework 支持(动态和静态都可以),上面提到的的奇技淫巧也就没有必要了(新的做法参考 这里 )。为什么 iOS 8 要添加动态库的支持?唯一的理由大概就是 Extension 的出现。Extension 和 App 是两个分开的可执行文件,同时需要共享代码,这种情况下动态库的支持就是必不可少的了。但是这种动态 Framework 和系统的 UIKit.Framework 还是有很大区别。系统的 Framework 不需要拷贝到目标程序中,我们自己做出来的 Framework 哪怕是动态的,最后也还是要拷贝到 App 中(App 和 Extension 的 Bundle 是共享的),因此苹果又把这种 Framework 称为Embedded Framework。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java并发编程实战

Java并发编程实战

Brian Goetz、Tim Peierls、Joshua Bloch、Joseph Bowbeer、David Holmes、Doug Lea / 童云兰 / 机械工业出版社华章公司 / 2012-2 / 69.00元

本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及验证线程安全的规则,如何将小的线程安全类组合成更大的线程安全类,如何利用线程来提高并发应用程序的吞吐量,如何识别可并行执行的任务,如何提高单线程子系统的响应性,如何确保并发程序执行预期任务,如何提高并发代码的性......一起来看看 《Java并发编程实战》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具