文章目录
- 编译和链接
-
- 1️⃣核心结论:一句话区分
- 2️⃣编译过程:从源代码到目标文件(.o)
-
- [2.1 预处理(Preprocessing):"替换变量+复制粘贴"](#2.1 预处理(Preprocessing):“替换变量+复制粘贴”)
- [2.2 编译(Compilation):"翻译成机器能懂的语言"](#2.2 编译(Compilation):“翻译成机器能懂的语言”)
- [2.3 汇编(Assembly):"翻译成机器指令"](#2.3 汇编(Assembly):“翻译成机器指令”)
- [2.4 实战:用命令行观察编译过程](#2.4 实战:用命令行观察编译过程)
- 动态库和静态库
-
- 1️⃣关于动态库和静态库核心结论:一句话区分
- 2️⃣底层原理:编译链接过程的差异
-
- [2.1 静态库(.a / .framework):"复制粘贴"到可执行文件](#2.1 静态库(.a / .framework):“复制粘贴”到可执行文件)
- [2.2 动态库(.dylib / .framework / .tbd):"取件券"+ 运行时加载](#2.2 动态库(.dylib / .framework / .tbd):“取件券”+ 运行时加载)
- 3️⃣核心差异对比:体积、内存、更新、依赖
- [4️⃣实战:iOS 中的静态库与动态库](#4️⃣实战:iOS 中的静态库与动态库)
-
- [4.1 静态库的典型应用](#4.1 静态库的典型应用)
- [4.2 动态库的典型应用](#4.2 动态库的典型应用)
- 5️⃣常见误区
-
- [误区 1:动态库一定比静态库"好"](#误区 1:动态库一定比静态库“好”)
- [误区 2:动态库体积一定小](#误区 2:动态库体积一定小)
- [误区 3:iOS 中动态库无法直接使用](#误区 3:iOS 中动态库无法直接使用)
- 6️⃣总结:如何选择?
- DYLD
-
- [1️⃣DYLD 是什么?核心职责](#1️⃣DYLD 是什么?核心职责)
- [2️⃣DYLD 的工作流程(以 iOS App 启动为例)](#2️⃣DYLD 的工作流程(以 iOS App 启动为例))
-
- [2.1 准备阶段:收集依赖信息](#2.1 准备阶段:收集依赖信息)
- [2.2 加载阶段:将动态库读入内存](#2.2 加载阶段:将动态库读入内存)
- [2.3 符号解析:找到函数的"实际地址"](#2.3 符号解析:找到函数的“实际地址”)
- [2.4 链接阶段:缝合程序与动态库](#2.4 链接阶段:缝合程序与动态库)
- 3️⃣DYLD2:经典但逐渐落后的动态链接器
-
- [3.1 DYLD2 的核心问题](#3.1 DYLD2 的核心问题)
-
- [(1)启动速度慢:串行加载 + 全量加载](#(1)启动速度慢:串行加载 + 全量加载)
- (2)符号解析效率低:全局锁竞争
- (3)内存碎片:重复加载相同库
- 4️⃣DYLD3:苹果的"性能革命"动态链接器
-
- [4.1 启动速度优化:并行加载 + 按需加载](#4.1 启动速度优化:并行加载 + 按需加载)
-
- (1)并行加载依赖库
- [(2)惰性加载(Lazy Binding)升级](#(2)惰性加载(Lazy Binding)升级)
- [4.2 内存效率优化:共享缓存 + 惰性卸载](#4.2 内存效率优化:共享缓存 + 惰性卸载)
-
- [(1)共享缓存(`dyld shared cache`)](#(1)共享缓存(
dyld shared cache
)) - [(2)惰性卸载(Lazy Unloading)](#(2)惰性卸载(Lazy Unloading))
- [(1)共享缓存(`dyld shared cache`)](#(1)共享缓存(
- [4.3 新特性支持:适配现代系统](#4.3 新特性支持:适配现代系统)
- [5️⃣DYLD3 的底层技术细节](#5️⃣DYLD3 的底层技术细节)
-
- [5.1 依赖关系图分析(Dependency Graph)](#5.1 依赖关系图分析(Dependency Graph))
- [5.2 符号解析的"三级跳"](#5.2 符号解析的“三级跳”)
- [5.3 内存管理的"智能回收"](#5.3 内存管理的“智能回收”)
- [6️⃣开发者如何适配 DYLD3?](#6️⃣开发者如何适配 DYLD3?)
-
- [6.1 检查当前使用的 DYLD 版本](#6.1 检查当前使用的 DYLD 版本)
- [6.2 适配 DYLD3 的注意事项](#6.2 适配 DYLD3 的注意事项)
-
- (1)避免依赖加载顺序
- (2)优化符号可见性
- (3)减少动态库依赖
- [(4)适配 Swift 代码](#(4)适配 Swift 代码)
- 7️⃣总结
编译和链接
1️⃣核心结论:一句话区分
- 编译:把"设计图纸"(源代码)翻译成"建筑零件"(目标文件),解决单个文件的"语法正确性"和"初步功能实现"。
- 链接:把多个"建筑零件"(目标文件)和"标准建材"(库文件)组装成"完整房子"(可执行文件),解决多文件间的"依赖关系"和"符号解析"。
2️⃣编译过程:从源代码到目标文件(.o)
编译是将 单个源代码文件(.m/.c) 转换为 目标文件(.o,Object File) 的过程,本质是"翻译+初步加工"。它分为 4 个阶段,像"工厂流水线"一样逐步处理。
2.1 预处理(Preprocessing):"替换变量+复制粘贴"
预处理是编译的第一步,主要处理源代码中的 预处理指令 (以 #
开头的行),类似"批量替换"和"文件拼接"。
常见预处理指令:
#import
/#include
:复制头文件内容到当前文件(类似"粘贴")------小心循环引用。#define
:定义宏(如#define MAX(a,b) ((a)>(b)?(a):(b))
),编译前替换代码中的宏调用(类似"批量替换")。#ifdef
/#endif
:条件编译(根据宏是否存在决定是否保留某段代码)。
例子 :
假设 Dog.h
内容为:
objective-c
#define DOG_NAME @"小狗"
@interface Dog : NSObject
- (void)setName:(NSString *)name;
@end
当 Dog.m
中 #import "Dog.h"
时,预处理会将 DOG_NAME
替换为 @"小狗"
,并将 Dog
类的声明复制到 Dog.m
中。
2.2 编译(Compilation):"翻译成机器能懂的语言"
预处理后的代码会被编译器(如 Clang)转换为 汇编代码 (.s
文件),这是"人类能读懂的机器语言"。
关键步骤:
- 语法检查:检查代码是否符合 OC 语法规则(如方法名是否正确、括号是否匹配)。如果报错(如"Expected ';' after expression"),编译失败。
- 语义分析 :检查代码逻辑是否合理(如变量是否声明后使用、方法是否存在)。例如,调用
[dog fly]
但Dog
类没有fly
方法,编译器会警告(但不会报错,因为 OC 是动态语言)。 - 生成汇编 :将 OC 代码转换为 CPU 能识别的汇编指令(如
mov
、call
等)。
2.3 汇编(Assembly):"翻译成机器指令"
汇编器(如 as
)将汇编代码(.s
)转换为 机器指令 (二进制格式),生成 目标文件(.o)。
目标文件(.o)的内容:
- 代码段(Text Section):存储机器指令(如方法的具体实现)。
- 数据段(Data Section) :存储全局变量、静态变量(如
static int count = 0
)。 - 符号表(Symbol Table):记录文件中定义的符号(如函数名、全局变量名)和引用的外部符号(如调用了其他文件的方法)。
- 重定位表(Relocation Table):记录需要外部链接的位置(如调用了其他文件的方法,需要链接时修正地址)。
2.4 实战:用命令行观察编译过程
在 macOS 终端,用 clang
命令手动编译一个 OC 文件,观察中间产物:
objective-c
# 编译 Dog.m 生成 Dog.o(目标文件)
clang -c Dog.m -o Dog.o
# 查看 Dog.o 的符号表(包含定义和引用的符号)
nm Dog.o
# 输出类似:
# U _NSLog
# T _dogSayHello
# 0000000000000000
我们用 "工具包" 和 "共享仓库" 的生活化场景,结合 iOS 开发中的实际案例,彻底讲透 静态库 和 动态库 的区别与核心逻辑。
动态库和静态库
1️⃣关于动态库和静态库核心结论:一句话区分
- 静态库:把"工具包"(代码)直接"塞进"你的工具箱(可执行文件),你的工具箱从此"自给自足",但体积变大。
- 动态库:把"工具包"放在"共享仓库"(系统目录),你的工具箱只留一张"取件券"(引用),需要时去仓库拿,体积小但依赖仓库。
2️⃣底层原理:编译链接过程的差异
2.1 静态库(.a / .framework):"复制粘贴"到可执行文件
静态库的本质是 一组目标文件(.o)的打包集合 (用 ar
工具打包)。在编译链接阶段,编译器会把静态库中所有用到的代码 完整复制 到最终的可执行文件中。静态库通常以 .a(Unix、Linux)或 .lib(Windows)以及MacOS 独有的 .framework为扩展名。
关键步骤:
- 编译源文件生成
.o
目标文件(如Dog.o
)。 - 链接器将
.o
文件和静态库(如libDog.a
)中需要的.o
合并,生成可执行文件(如App
)。 - 可执行文件体积增大(包含静态库的所有代码),但运行时无需额外依赖。
2.2 动态库(.dylib / .framework / .tbd):"取件券"+ 运行时加载
动态库的本质是 独立的二进制文件,存储在系统或应用的特定目录中。编译链接阶段,编译器只记录动态库中用到的函数的"地址线索"(符号引用),不会复制代码到可执行文件中。动态库的格式有:.framework、.dylib、.tbd......
关键步骤:
- 编译源文件生成
.o
目标文件(如Dog.o
)。 - 链接器生成可执行文件时,仅记录动态库(如
libDog.dylib
)的路径和符号引用(如+[Dog bark]
)。 - 运行时,系统根据可执行文件中的"地址线索",从动态库中加载所需代码到内存,供程序调用。
3️⃣核心差异对比:体积、内存、更新、依赖
对比项 | 静态库 | 动态库 |
---|---|---|
体积 | 可执行文件体积大(包含库代码) | 可执行文件体积小(仅存符号引用) |
内存占用 | 每个程序独立复制库代码,内存浪费 | 多个程序共享同一份库代码,内存高效 |
更新维护 | 库更新需重新编译所有依赖它的程序 | 替换动态库文件即可,无需重新编译程序 |
依赖管理 | 无外部依赖(库代码已嵌入) | 强依赖动态库路径(运行时需找到库文件) |
典型场景 | 需独立运行的工具(如命令行程序) | 系统框架(如 UIKit)、高频共享库 |
4️⃣实战:iOS 中的静态库与动态库
4.1 静态库的典型应用
- 自定义静态库 :开发者将常用功能(如网络请求、加密算法)打包为
.a
或.framework
,提供给其他项目直接集成。
优点 :避免重复开发,保护代码隐私(静态库代码嵌入可执行文件,反编译难度更高)。
缺点:每次更新库需重新编译宿主项目。
4.2 动态库的典型应用
- 系统框架(如 UIKit、Foundation) :iOS 系统自带的核心框架均为动态库,存储在
/System/Library/Frameworks
目录。
优点:所有 App 共享同一份框架代码,大幅节省内存;系统升级时自动更新框架(如 iOS 17 升级后,UIKit 动态库同步更新)。 - 第三方动态库(如微信 SDK、支付宝 SDK) :部分 SDK 提供动态库版本(
.framework
或.tbd
),允许 App 直接调用,无需嵌入代码。
5️⃣常见误区
误区 1:动态库一定比静态库"好"
动态库的优势是节省内存和方便更新,但并非所有场景都适用:
- 若需要 离线运行(如无网络环境下的工具类 App),静态库更可靠(无需依赖外部动态库)。
- 若库代码需要 高度定制(如修改底层实现),静态库更灵活(直接修改源码重新编译)。
误区 2:动态库体积一定小
动态库本身的体积可能很大(如 UIKit 框架),但多个 App 共享时,内存总占用会远小于每个 App 都嵌入一份静态库的体积。
误区 3:iOS 中动态库无法直接使用
iOS 支持动态库,但需注意:
- 自定义动态库需通过 Xcode 打包为
.framework
(需设置Install Path
为@executable_path/Frameworks
)。 - 上架 App Store 时,动态库需包含在 App 包内(否则无法加载),因此实际开发中静态库更常见于第三方 SDK。
6️⃣总结:如何选择?
- 选静态库:需要独立运行、保护代码、避免外部依赖。
- 选动态库:需要共享内存、频繁更新、节省资源。
一句话总结:静态库是"一次性买断的工具包",动态库是"共享仓库的取件券",根据需求选择最适合的复用方式。
DYLD
1️⃣DYLD 是什么?核心职责
DYLD(Dynamic Link Editor)是苹果为 macOS/iOS 设计的 动态链接器 ,负责解决程序运行时的 动态依赖 问题。它的核心职责可以概括为三个步骤:
- 加载动态库 :将程序依赖的
.dylib
、.framework
或系统库从磁盘加载到内存。 - 解析符号:找到程序中调用的函数/变量在动态库中的实际内存地址(符号绑定)。
- 链接程序:将程序代码与动态库代码"缝合",确保调用动态库函数时能正确跳转。
2️⃣DYLD 的工作流程(以 iOS App 启动为例)
无论 DYLD2 还是 DYLD3,核心流程都围绕 加载→解析→链接 展开,但具体实现细节差异巨大。以下是通用流程:
2.1 准备阶段:收集依赖信息
App 启动时,内核(kernel
)会读取可执行文件的 头部信息 (Mach-O
头),获取其依赖的动态库列表(如 libSystem.dylib
、UIKit.framework
)。
关键数据结构:
dyld_image_info
:记录每个动态库的路径、加载地址、依赖关系等信息。
2.2 加载阶段:将动态库读入内存
根据依赖列表,DYLD 从磁盘或共享缓存(dyld shared cache
)中加载动态库到内存。
DYLD2 的加载方式:
- 串行加载 :按依赖顺序逐个加载(如先加载
A.dylib
,再加载依赖A
的B.dylib
)。 - 全量加载:即使动态库未被立即使用,也会完整加载到内存(可能导致内存浪费)。
2.3 符号解析:找到函数的"实际地址"
程序中调用的函数(如 [UIView addSubview:]
)在编译时只是符号(如 _objc_msgSend
),DYLD 需要将其映射到动态库中的真实内存地址。
DYLD2 的符号解析:
- 全局锁阻塞 :所有符号解析需竞争同一把全局锁(
dyld lock
),多线程场景下容易成为瓶颈。 - 懒解析(Lazy Binding):部分符号延迟到首次调用时解析(减少启动时的计算量)。
2.4 链接阶段:缝合程序与动态库
将程序的代码段(__TEXT
)与动态库的代码段(__TEXT
)通过 内存地址重定位 关联,确保调用指令(如 call
)能正确跳转到动态库的函数入口。
3️⃣DYLD2:经典但逐渐落后的动态链接器
DYLD2 是苹果早期的动态链接器(随 macOS 10.4/Tiger 引入),在 iOS 13 前是默认实现。它的设计思路是 稳定优先,但在性能和内存效率上存在明显短板。
3.1 DYLD2 的核心问题
(1)启动速度慢:串行加载 + 全量加载
- 串行加载:依赖链越长(如复杂 App 可能有数百个动态库),加载时间越长。例如,一个依赖 100 个动态库的 App,DYLD2 需执行 100 次磁盘读取和内存分配。
- 全量加载:即使动态库仅在后台使用(如统计 SDK),也会在启动时完整加载,占用宝贵的内存资源(尤其是 iOS 设备内存有限)。
(2)符号解析效率低:全局锁竞争
DYLD2 使用全局锁(dyld lock
)保证符号解析的线程安全,但多线程场景下(如 App 启动时同时初始化多个模块),锁竞争会导致大量线程阻塞,延长启动时间。
(3)内存碎片:重复加载相同库
多个进程(如同时运行的微信、支付宝)若依赖同一动态库(如 libSystem.dylib
),DYLD2 会为每个进程单独加载一份,导致内存冗余(同一库在内存中存在多份副本)。
4️⃣DYLD3:苹果的"性能革命"动态链接器
DYLD3 随 iOS 13/macOS 10.15 引入,目标是 大幅提升启动速度、降低内存占用。它针对 DYLD2 的痛点进行了全面重构,核心改进体现在以下方面:
4.1 启动速度优化:并行加载 + 按需加载
(1)并行加载依赖库
DYLD3 通过 依赖关系图分析 (dependency graph
),识别无冲突的动态库(即彼此无依赖的库),并 并发加载 它们。例如,若 A.dylib
和 B.dylib
无依赖关系,DYLD3 可同时加载这两个库,将加载时间从串行的 T1+T2
缩短为 max(T1, T2)
。
技术实现:
- 使用
dispatch_group
或pthread
实现多线程加载。 - 通过
dyld3
私有 API 与内核协作,优化磁盘读取(如预读取相邻磁盘块)。
(2)惰性加载(Lazy Binding)升级
DYLD3 将"懒解析"从"部分符号"扩展到"大部分符号",仅在函数 首次调用时 执行符号解析和地址重定位。例如,一个包含 1000 个函数的动态库,若启动时仅调用其中 10 个,DYLD3 仅解析这 10 个函数的地址,其余 990 个延迟到调用时处理。
4.2 内存效率优化:共享缓存 + 惰性卸载
(1)共享缓存(dyld shared cache
)
DYLD3 引入 全局共享缓存 ,将常用动态库(如 libSystem.dylib
、UIKit.framework
)的解析结果(符号地址、加载路径等)缓存到系统级内存中。多个进程(如微信、支付宝)调用同一库时,直接从共享缓存中读取,避免重复加载和解析。
数据验证:
- 共享缓存可减少 30%-50% 的动态库加载时间(苹果官方测试数据)。
(2)惰性卸载(Lazy Unloading)
DYLD3 允许未使用的动态库在内存紧张时被卸载(回收内存),并在需要时重新加载。例如,一个后台统计库在 App 切到前台时未被使用,DYLD3 可将其卸载,释放内存给前台模块。
4.3 新特性支持:适配现代系统
DYLD3 针对 iOS 13+ 和 macOS 10.15+ 的新特性做了深度优化:
特性 | DYLD3 支持细节 |
---|---|
Swift 动态链接 | 优化 Swift 符号的绑定(如泛型、协议扩展),减少 Swift 代码的启动延迟(比 DYLD2 快 20%+)。 |
arm64e 架构 | 针对苹果自研芯片(如 A12+)优化指令集适配,提升 ARM64e 代码的执行效率。 |
App Sandbox 安全 | 增强对动态库的签名验证(检查 LC_CODE_SIGNATURE ),防止恶意库注入(仅允许加载已签名库)。 |
5️⃣DYLD3 的底层技术细节
5.1 依赖关系图分析(Dependency Graph)
DYLD3 在加载前会构建 依赖关系图(有向无环图,DAG),通过拓扑排序确定加载顺序。例如:
App → A.dylib → B.dylib
App → C.dylib → B.dylib
此时,B.dylib
是 A
和 C
的共同依赖,DYLD3 会先加载 B.dylib
,再并行加载 A
和 C
。
5.2 符号解析的"三级跳"
DYLD3 的符号解析分为三个阶段,逐步细化:
- 预解析(Prebinding):在加载阶段,通过共享缓存快速查找符号的大致地址范围(减少后续搜索时间)。
- 精确解析(Binding) :首次调用符号时,通过
dyld_stub_binder
函数精确计算符号的内存地址(更新__DATA
段的指针)。 - 缓存优化(Caching):将解析结果存入共享缓存,后续调用直接读取缓存(避免重复计算)。
5.3 内存管理的"智能回收"
DYLD3 使用 引用计数 + LRU(最近最少使用) 策略管理动态库内存:
- 每个动态库被加载后,引用计数加 1(进程持有)。
- 当内存紧张时,DYLD3 优先卸载引用计数低且长时间未使用的库(LRU 策略)。
6️⃣开发者如何适配 DYLD3?
6.1 检查当前使用的 DYLD 版本
- 通过日志 :运行应用时添加环境变量
DYLD_PRINT_VERSION=1
,日志会输出dyld: version 3.x
(DYLD3)或dyld: version 2.x
(DYLD2)。 - 通过系统版本:iOS 13+ 和 macOS 10.15+ 默认启用 DYLD3(除非强制指定 DYLD2)。
6.2 适配 DYLD3 的注意事项
(1)避免依赖加载顺序
DYLD3 的并行加载可能改变动态库的加载顺序,若代码依赖特定顺序(如 +load
方法中调用其他库的函数),可能导致崩溃。需确保 +load
方法无外部依赖。
(2)优化符号可见性
DYLD3 对未导出的符号(如 static
函数)解析更严格,需通过 __attribute__((visibility("default")))
显式导出:
objective-c
// 显式导出符号,避免 DYLD3 无法解析
__attribute__((visibility("default")))
void myFunction() {
// ...
}
(3)减少动态库依赖
DYLD3 虽然优化了加载效率,但过多的动态库仍会增加依赖图复杂度。尽量合并功能到少量动态库,或使用静态库(仅当需要离线运行时)。
(4)适配 Swift 代码
DYLD3 对 Swift 的支持更友好,但仍需注意:
- 避免使用
@_transparent
等私有属性(可能影响符号可见性)。 - 确保 Swift 模块的
swiftmodule
目录结构正确(DYLD3 依赖此结构解析符号)。
7️⃣总结
DYLD 从 DYLD2 到 DYLD3 的演进,本质是苹果对 启动速度 和 内存效率 的极致追求。DYLD3 通过并行加载、共享缓存、惰性解析等技术,将动态链接的瓶颈从"启动时间"转移到"运行时效率",为现代 iOS 应用的高性能运行提供了底层保障。
开发者行动建议:
- 优先适配 DYLD3(目标系统为 iOS 13+),利用其性能优势。
- 优化动态库依赖,减少不必要的加载。
- 关注苹果官方文档,及时适配新特性(如 Swift 动态链接优化)。