所用版本:
- 处理器: Intel Core i9
- MacOS 12.3.1
- Xcode 13.3.1
- dyld-941.4
- objc4-838
虽然苹果官网发布的正式版才到dyld-852.2
dyld-852.2不过github上可以下到最新非正式版本, 写文章时候最新版本为dyld-941.4
, 估计以后还会更新。dyld3
到dyld4
我认为改动还是比较大。
dyld4的针对于的mach-o
解析器 ( iOS上可执行文件格式是Mach-O格式, 下方也有具体解释) 方面跟dyld3相同,但是引入了 JustInTime 的加载器来优化。
-
dyld3
: 相比dyld2
新增预构建/闭包, 目的是将一些启动数据创建为闭包存到本地,下次启动将不再重新解析数据,而是直接读取闭包内容 -
dyld4
: 采用pre-build + just-in-time
预构建/闭包+实时解析的双解析模式, 将根据缓存有效与否选择合适的模式进行解析, 同时也处理了闭包失效时候需要重建闭包的性能问题。
初看下dyld新旧版本对比, 看一下dyld
加载流程相较之前改变
我这里先带入dyld
以及dyld
做了什么 , 先看个例子
普通的一个OC项目, ViewController
中加一个+ (void)load
方法, main
中加一个函数SAFuc
运行一下, 看下它们走的顺序, 结果如下
运行会发现, 先走的load
方法, 再走SAFuc
, 最后走的main
中的Hello world
这块其实就会有疑问, 不应该先走main
的Hello world
么? 所以我们就要看下应用程序加载流程
动态库/静态库/编译过程
先看下编译过程
的流程图
我们先了解动态库
, 静态库
, 代码编译过程
这几个概念, 方便后面探索
静态库 / 动态库
通常程序都会依赖系统一些库, 库是什么呢? 其实库就是一些可执行的二进制文件
, 能被操作系统加载到内存里面中。库分为两种静态库
, 动态库
静态库
.a
, .lib
等。链接阶段
时静态库会被完整地复制, 一起打包在可执行文件中,被多次使用就有多份冗余拷贝。
-
优点
: 编译完成之后, 链接到目标程序中, 同时打包到可执行文件里面, 不会有外部依赖。 -
缺点
: 静态库会有两份, 所以会导致目标程序体积增大
, 对内存, 性能, 速度消耗很大。并且相同静态库每个app中都会拷贝一份。
动态库
.framework
等。程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入。苹果大部分都是动态库
-
优点
: 不需要拷贝到目标程序,减少App包的体积
-
多个App可以使用同一个动态库,
共享内存, 节约资源
-
由于运行时才会去加载, 那么可以在App不使用时随时对库进行替换或更新,
更新灵活
-
-
缺点
: 动态载入会带来一部分性能损失, 同时动态库也会使得程序依赖于外部环境。一旦动态库没有或消失, 程序会出现问题。
代码编译过程
编译过程-
源文件
: .h, .m, .cpp, .c等文件 -
预编译
: 预先编译文件(源文件), 词法语法分析, 替换宏, 删除注释, 展开头文件, 产生.i文件 -
编译
: 编译文件, 将.i文件转换为汇编语言, 产生.s文件(汇编文件) -
汇编
: 将汇编文件转换为机器代码文件, 产生.o文件 -
链接
: 把之前所有操作的文件链接到程序里面来, 对.o文件中引用其他库的地方进行引用, 生成最后的可执行文件
。动态库与静态库区别其实就是链接的区别。
可执行文件位置: 通常编译后的 程序.cpp显示包内容, 可找到可执行文件
(黑黑的一个文件)
其实可执行文件就是能够运行起来的文件, 我们也可以把它拖到终端中回车, 可发现也能打印出信息。(ios项目需要真机运行, 直接拖入终端回车会报错)
当然我们如果想要查询系统动态库可执行文件, 以CoreFoundation
为例
-
断点
→image list
→CoreFoundation
按路径搜索 可以找到CoreFoundation.frame
-
CoreFoundation.frame
右键选择显示包内容
即可看到CoreFoundation
的可执行文件
dyld
dyld
(the dynamic link editor)是苹果的动态链接器
,是苹果操作系统的重要组成部分,在app被编译打包成可执行文件格式的Mach-O
文件后,交由dyld
负责连接
动静态库,加载程序
- 这里的
image
不是图片是镜像文件
, 库加载进去就是映射, 映射一份到内存, 而这个东西就image
。映射可以理解成, 例如 动态库都存在沙盒路径磁盘里面, 当我们用到相应动态库时候, copy一份(找了一个替身)加载到我们用到程序的内存里面。
探索dyld之前我们要先了解入口, 在load方法处加一个断点,bt
查看下, 当然也可以通过左侧的堆栈信息查看。
旧版本
旧版本dyld入口新版本
dyld4新版本模拟器改动 dyld4新版本真机改动栈结构, 先进后出, 所以要从后往前看。
- 旧版本
dyld
:_dyld_start
开始, 接下来走dyldbootstrap
, 源码入口需要在dyld_start
开始。 - 新版本
dyld
:start
开始接下来走dyld4
中prepare
方法, 源码入口需要在start
中开始探索。
当然我们也可以走下汇编看下
dyld`在哪里,
旧版本
可发现在libdyld.dylib
这里面
既然在
libdyld.dylib
里面, 那我们可以去苹果官方 Source Browser 可下到dyld
源码, 如下图
新版本
而新版本......不得不说官方很严谨。(后面拿真机做例子)
新版本 新版本旧版本
在dyld源码
之后全局搜索dyld_start
(找入口), 入口是汇编写的, 看arm环境
的就行。 能找到接下去走dyldbootstrap
, 这也跟之前bt
打印内容一致
dyldStartup
→__dyld_start
(入口函数)查找时发现,是由dyld
是汇编
实现(.s汇编文件),通过注释发现, 下面会调用call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
,是一个C++方法, 那么我们根据名字dyldbootstrap
, 去寻找他的start
方法。
新版本
新版本我们直接搜索dyldbootstrap
, 肯定是无了
我们先搜索start
, 在同样的dyldStartup
有
其实注释已经告诉我们, 此汇编代码对齐堆栈并跳入C代码:dyld:: start(const KernelArgs* kernArgs)
那么搜索 start(const KernelArgs* kernArgs)
方法看一下, 有
这里的start
方法是dyld的入口点。那么我们接着这里进行探索。 往下看有一个"prepare"准备 方法MainFunc appMain = prepare(state, dyldMA);
,
可看到这个方法是处理相关依赖绑定的方法, 那么进入看下其源码, 看看到底准备些什么内容
prepare底层[ 配置环境/平台/路径/版本等信息 ]
看下gProcessInfo
有struct dyld_all_image_infos* gProcessInfo = &dyld_all_image_infos;
是一个存储dyld所有镜像信息的一个结构体
可看出dyld_all_image_infos
包含信息比较多, mach_header
, dyld_uuid_info
, dyldVersion
等等。
其中mach_header
是Mach-O
的头部,而dyld
加载的文件就是Mach-O
类型的,即Mach-O
类型是可执行文件类型
,由四部分组成:Mach-O头部
、Load Command
、section
、Other Data
,可以通过MachOView
可查看可执行文件信息
回到prepare
方法, 接着往下看
[进行pre-build, 创建mainLoader]
预构建接下来会创建一个mainLoader
主装载器, 如果熟悉dyld3
的小伙伴知道, 旧版本是创建一个ImageLoader
镜像装载器
mainLoader
主装载器, 可以理解成一个容器, 这里面陆续添加 可执行文件
, 动态库
等等, 都装载完成之后经由后续一些处理, 就是我们打开的App。
[ 创建just-in-time ]
just-in-time这是dyld4
一个新特性, dyld4
在保留了dyld3
的 mach-o 解析器
基础上,同时也引入了 just-in-time
的加载器来优化, 这里稍微细说一下。
首先dyld3
出于对启动速度的优化的目的, 增加了预构建(闭包)
。App第一次启动或者App发生变化时会将部分启动数据创建为闭包
存到本地,那么App下次启动将不再重新解析数据,而是直接读取闭包内容。当然前提是应用程序和系统应很少发生变化,但如果这两者经常变化等, 就会导闭包丢失或失效。所以dyld4
采用了 pre-build + just-in-time
的双解析模式,预构建 pre-build
对应的就是 dyld3
中的闭包,just-in-time
可以理解为实时解析
。当然just-in-time
也是可以利用 pre-build
的缓存的,所以性能可控。有了just-in-time
, 目前应用首次启动、系统版本更新、普通启动,dyld4
则可以根据缓存是否有效选择合适的模式进行解析。
[ 装载内容 ]
装载往下看可看到, mainLoader
进行装载, 装载可执行文件, 动态库等等
记录插入信息, 遍历所有dylibs, 一些记录检查操作继续往下走。
[插入缓存]
插入缓存这里是对dyld
缓存的一个处理, 其中state
是prepare
传入进来的参数, 其定义APIs& state = APIs::bootstrap(config, sLocks);
是APIs方法里面的bootstrap
引导程序方法。
接下来是一些其他通知和写入操作, 简单看一下, 之后是下一个重点内容
runAllInitializersForMain
[运行初始化方法]
runAllInitializersForMain前面稍微提过state
定义APIs& state = APIs::bootstrap(config, sLocks);
, 源自DyldAPIs
, 那么我们进入看一下
notifyObjCInit 函数
在执行完初始化之后会执行notifyObjCInit
, 告诉objc 去运行所有 +load
方法, 而此时系统main
还没有执行, 这也就是为什么+ load
方法执行在main
前面的原因。我们看一下notifyObjCInit
内部
其中 _notifyObjCInit
我们看一下。首先可以看到_notifyObjCInit
定义是_dyld_objc_notify_init _notifyObjCInit = nullptr;
因为判断是要_notifyObjCInit
非null才继续后面, 所以我们要搜索下_notifyObjCInit
什么地方赋值
_notifyObjCInit
是 setObjCNotifiers
方法中的第二参数_dyld_objc_notify_init init
继续找
setObjCNotifiers
有_dyld_objc_notify_register
。
_dyld_objc_notify_register
这个方法其实在objc4
源码_objc_init
方法中见过
我们在objc_init
内部调用了dyld_objc_notify_register
方法, 并为其传入参数load_images
(第二个参数 init)。
由load_images
→ call_load_methods
→ call_class_loads
内部也可以看出会 循环调用所有+load
方法,直到不再有。
[link动态库和主程序]
runInitializersBottomUpPlusUpwardLinks回到runAllInitializersForMain
继续看, runInitializersBottomUpPlusUpwardLinks
循环link动态库, 再link可执行文件
[加载主程序入口]
runAllInitializersForMain
准备工作完成之后, 寻找App中main函数
, App正常运行
综上也验证了dyld 打印信息
dyld4打印信息 dyld4应用程序加载流程图
网友评论