文章目录
1.1 动态库和静态库的认识
1.1.1 介绍
从本质上,库是一种可执行代码的二进制形式。可以被操作系统载入内存执行。库分为两种:静态库(.a .lib)和动态库(framework .so .dll)。
.a是纯二进制文件,.framework中除了有二进制文件还有资源文件,.a文件不能直接使用,至少需要.h文件的配合。而.framework可以直接使用。
.a + .h + sourceFile = .framework
所谓静态和动态是指链接过程,动态和静态是相对于编译器和运行期的。
静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要载入静态库。
而动态库是在程序编译时会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。
1.1.2 静态库
静态库即静态链接库(Windows 下的 .lib,Linux 和 Mac 下的 .a)。之所以叫做静态,是因为静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。
静态库的好处很明显,编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。
当然其缺点也很明显,就是会使用目标程序的体积增大。
1.1.3 动态库
动态库在内存中只有一个,操作系统也只会加载一次到内存中。只是针对不同的进程进行各自的映射
代码段在内存中的权限都是只读的,所以多个程序虽然使用同一个动态库,但是并不会修改源代码
动态库即动态链接库(Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib/.tbd)。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。
动态库的优点是,不需要拷贝到目标程序中,不会影响目标程序的体积,而且同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。同时,编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。
动态库带来的问题主要是,动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux 下喜闻乐见的 lib not found 错误)。
1.2 Mach-O的简单认识
程序要想运行起来,其可执行文件格式就需要被操作系统所理解,对于iOS来说,Mach-O是其可执行文件的格式。
其是一种用于可执行文件、目标代码、动态库、内核转储的文件格式。
Mach-O有三种文件类型 Executable、Dylan、Bundle
Executable
Executable是app的二进制主文件,我们可以在Xcode项目中的products文件中找到它。
Dylib
Dylib是动态库,动态库分为动态连接库和动态加载库。
动态连接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随之被加载到内存中,随着程序启动而启动。
动态加载库:当需要的时候再使用dlopen等通过代码或者命令的方式加载,程序启动后而加载。
Bundle
Bundle是一种特殊的Dylib,我们无法对其进行链接。所能做的就是在runtime运行时通过dlopen来加载它,它可以在macOS上用于插件
Image和Framework
Image(镜像文件)包含了上述的三种类型
Framework可以理解为动态库。
1.3 dyld的简单认识
dyld是动态链接器。
在 iOS / macOS 系统中,仅有很少的进程只需内核就可以完成加载,基本上所有的进程都是动态链接的
Mach-O镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容填充,这个填充工作就是由dyld来完成的。
对于链接生成的Mach-O可执行文件,在程序启动时通过dyld进行链接载入。
App启动过程主要研究的就是dyld的执行过程。
1.4 编译过程
- 预处理:载入.h.m.cpp等文件进行预处理产生.i文件。也叫预编译,可以替换掉所有的宏;处理条件预编译指令,比如#if;删除所有注释;展开头文件。
- 编译:将.i文件转换成汇编语言,产生.s文件。进行一些语法分析、词法分析、语义分析;还会进行一些优化,比如真值判断,假值判断。
- 汇编:将汇编语言转换成机器可以执行的指令,产生.o文件。每一条汇编语言都对应一条机器指令。
- 链接:对.o文件中引用的其他库的地方进行引用,生成可执行文件Mach-O。dyld就在此处起作用。
1.5 App启动过程
通过打印调用栈,我们清晰的看到App启动的起点是_dyld_start
查看一下dyld的源码
在dyldStartup的文件中我们找到如下
- 开始进行初始化链接载入等操作:
call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
- 清空栈和跳转到主程序的start(不重要):
clean up stack and jump to "start" in main executable
- 开始执行main函数
LC_MAIN case, set up stack for call to main()
看一下call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
函数
我们本质就要进入这个文件的start函数进行查看
其核心就是执行了一个dyld::_main()函数
看一下dyld::main的源码主要做了什么事呢?
大体说明:
- 设置运行环境
- 调用
instantiateFromLoadedImage
函数加载可执行文件到内存中 - 调用link函数
详细步骤:
第一步: 环境变量配置。根据环境变量设置相应的值以及获取当前运行架构。我们如果自己人为设置环境变量,在此就可以被获取到。
第二步:检查是否开启了共享缓存。以及共享缓存是否映射到共享区域,例如UIKit\CoreFoundation等。苹果的动态库都放在了缓存里,叫动态库共享缓存,从iOS 3.1开始,为了提高性能,绝大部分的系统动态库文件都打包存放到了一个缓存文件中(dyld shared cache)
第三步:主程序的初始化。调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象。
第四步:插入动态库。DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载。
第五步:链接主程序。
第六步:链接动态库。
第七步:弱符号绑定。
第八步:执行初始化方法。
第九步:寻找主程序入口,即main函数。
总结
- 主要的就是动态库的插入和链接、主程序的初始化和链接、初始化方法
- 该函数最终返回引用程序main函数的地址,最后Dyld去调用它。
- 主程序的初始化只做了一件事,就是创建一个ImageLoader对象,后面就是通过这个对象来加载二进制文件到内存中。
总结
知识点总结:
- 源文件->预处理->编译->汇编->链接->可执行文件Mach-O
- dyld是动态链接器,它的作用时机在于App启动,将可执行文件Mach-O进行动态链接,链接动态库,进行主程序的加载以及方法的初始化。(类、分类、协议的加载,还有Cxx函数和load()的加载)
- App启动时dyld链接器用来加载程序
- 加载过程中重点在于load的加载和类、分类、协议、方法的加载
- load的加载和类、分类、协议、方法的加载都是在objc中的,但是他们的实际调用时机是在dyld中,因为是通过map_images和load_images回调函数的注册来做到的。
启动过程总结:
启动过程,会先有一个启动动画,为了防止一个应用占用过多的系统资源加载不出来启动动画,iOS有一个看门狗机制。如果我们的应用程序对一些特定的UI事件(比如启动、挂起、恢复、结束)响应不及时,Watchdog 会把我们的应用程序干掉,并生成一份响应的 crash 报告。错误编码是8badf00d
看门狗解决方案:
- 异步网络请求。看门狗问题很大程度会出现在同步网络调用而阻塞主线程的情况。
- 在非主线程中使用同步网络请求
- 通过 RunLoop 来操控一切,一旦超过既定的超时时间,就提示用户重试或者暂时先跳过网络请求。
App在启动时会先通过dyld动态链接器进行库文件(比如经典的runtime库或者UIKit)与汇编后的.o文件进行动态链接,以及进行主程序的加载,加载是通过objc库注册的map_images和load_images回调函数来进行加载类信息的。而在加载过程中会进行类、分类、协议的加载,load_images里执行了load方法。
最后 dyld 会调用 main() 函数。main() 会调用 UIApplicationMain(),程序启动。
UIApplication对象是应用程序的象征,每一个应用都有自己的UIApplication对象,而且是单例的。通过[UIApplication sharedApplication]可以获得这个单例对象,一个iOS程序启动后创建的第一个对象就是UIApplication对象。然后创建UIApplication的delegate对象 —–(您的)AppDelegate ,开启一个runloop,每当监听到对应的系统事件时,就会通知AppDelegate。
后面就是一个viewcontroller的生命周期了。差不多就这些。