【iOS】编译和链接、动静态库及dyld的简单学习

文章目录

  • 编译和链接
    • 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 实战:用命令行观察编译过程)
  • 动态库和静态库
  • 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:经典但逐渐落后的动态链接器
    • 4️⃣DYLD3:苹果的"性能革命"动态链接器
      • [4.1 启动速度优化:并行加载 + 按需加载](#4.1 启动速度优化:并行加载 + 按需加载)
      • [4.2 内存效率优化:共享缓存 + 惰性卸载](#4.2 内存效率优化:共享缓存 + 惰性卸载)
        • [(1)共享缓存(`dyld shared cache`)](#(1)共享缓存(dyld shared cache))
        • [(2)惰性卸载(Lazy Unloading)](#(2)惰性卸载(Lazy Unloading))
      • [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?)
    • 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 能识别的汇编指令(如 movcall 等)。

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为扩展名。

关键步骤

  1. 编译源文件生成 .o 目标文件(如 Dog.o)。
  2. 链接器将 .o 文件和静态库(如 libDog.a)中需要的 .o 合并,生成可执行文件(如 App)。
  3. 可执行文件体积增大(包含静态库的所有代码),但运行时无需额外依赖。

2.2 动态库(.dylib / .framework / .tbd):"取件券"+ 运行时加载

动态库的本质是 独立的二进制文件,存储在系统或应用的特定目录中。编译链接阶段,编译器只记录动态库中用到的函数的"地址线索"(符号引用),不会复制代码到可执行文件中。动态库的格式有:.framework、.dylib、.tbd......

关键步骤

  1. 编译源文件生成 .o 目标文件(如 Dog.o)。
  2. 链接器生成可执行文件时,仅记录动态库(如 libDog.dylib)的路径和符号引用(如 +[Dog bark])。
  3. 运行时,系统根据可执行文件中的"地址线索",从动态库中加载所需代码到内存,供程序调用。

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 设计的 动态链接器 ,负责解决程序运行时的 动态依赖 问题。它的核心职责可以概括为三个步骤:

  1. 加载动态库 :将程序依赖的 .dylib.framework 或系统库从磁盘加载到内存。
  2. 解析符号:找到程序中调用的函数/变量在动态库中的实际内存地址(符号绑定)。
  3. 链接程序:将程序代码与动态库代码"缝合",确保调用动态库函数时能正确跳转。

2️⃣DYLD 的工作流程(以 iOS App 启动为例)

无论 DYLD2 还是 DYLD3,核心流程都围绕 加载→解析→链接 展开,但具体实现细节差异巨大。以下是通用流程:

2.1 准备阶段:收集依赖信息

App 启动时,内核(kernel)会读取可执行文件的 头部信息Mach-O 头),获取其依赖的动态库列表(如 libSystem.dylibUIKit.framework)。

关键数据结构

  • dyld_image_info:记录每个动态库的路径、加载地址、依赖关系等信息。

2.2 加载阶段:将动态库读入内存

根据依赖列表,DYLD 从磁盘或共享缓存(dyld shared cache)中加载动态库到内存。

DYLD2 的加载方式

  • 串行加载 :按依赖顺序逐个加载(如先加载 A.dylib,再加载依赖 AB.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.dylibB.dylib 无依赖关系,DYLD3 可同时加载这两个库,将加载时间从串行的 T1+T2 缩短为 max(T1, T2)

技术实现

  • 使用 dispatch_grouppthread 实现多线程加载。
  • 通过 dyld3 私有 API 与内核协作,优化磁盘读取(如预读取相邻磁盘块)。
(2)惰性加载(Lazy Binding)升级

DYLD3 将"懒解析"从"部分符号"扩展到"大部分符号",仅在函数 首次调用时 执行符号解析和地址重定位。例如,一个包含 1000 个函数的动态库,若启动时仅调用其中 10 个,DYLD3 仅解析这 10 个函数的地址,其余 990 个延迟到调用时处理。

4.2 内存效率优化:共享缓存 + 惰性卸载

(1)共享缓存(dyld shared cache

DYLD3 引入 全局共享缓存 ,将常用动态库(如 libSystem.dylibUIKit.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.dylibAC 的共同依赖,DYLD3 会先加载 B.dylib,再并行加载 AC

5.2 符号解析的"三级跳"

DYLD3 的符号解析分为三个阶段,逐步细化:

  1. 预解析(Prebinding):在加载阶段,通过共享缓存快速查找符号的大致地址范围(减少后续搜索时间)。
  2. 精确解析(Binding) :首次调用符号时,通过 dyld_stub_binder 函数精确计算符号的内存地址(更新 __DATA 段的指针)。
  3. 缓存优化(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 动态链接优化)。
相关推荐
摇滚侠21 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 属性优先级 行内写法 变量选择 笔记42
java·spring boot·笔记
摇滚侠21 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 总结 热部署 常用配置 笔记44
java·spring boot·笔记
小白要努力sgy1 天前
待学习--中间件
学习·中间件
rechol1 天前
汇编与底层编程笔记
汇编·arm开发·笔记
~无忧花开~1 天前
CSS学习笔记(五):CSS媒体查询入门指南
开发语言·前端·css·学习·媒体
吴鹰飞侠1 天前
AJAX的学习
前端·学习·ajax
阿里超级工程师1 天前
ios云打包证书申请不需要苹果电脑也是可以的
ios·证书·云打包
我的xiaodoujiao1 天前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 19--测试框架Pytest基础 3--前后置操作应用
python·学习·测试工具·pytest
lzj_pxxw1 天前
嵌入式开发技巧:舍弃标志位,用宏定义函数实现程序单次运行
笔记·stm32·单片机·嵌入式硬件·学习
江苏世纪龙科技1 天前
【世纪龙科技】新能源汽车动力电池拆装与检测虚拟实训软件介绍
学习