# iOS 动态库与静态库全面解析

23 阅读11分钟

iOS 动态库与静态库全面解析

一、基本概念

静态库 (Static Library)

同一个库,动态库 vs 静态库,谁更大?绝大多数情况下,动态库会让包体积更大。 原因如下:

核心差异:能不能 Strip 未使用代码,静态库 — 链接器只拉入你实际用到的 .o,没用到的直接丢弃: 静态库是在编译链接阶段被完整拷贝到可执行文件中的代码集合。链接完成后,静态库文件本身不再被需要。

文件格式:

  • .a — 传统静态库(archive 文件,本质是 .o 目标文件的打包)
  • .framework — 可以是静态 framework(Xcode 从 iOS 8 起支持)

动态库 (Dynamic Library)

动态库在运行时由动态链接器(dyld)加载到进程地址空间中,不会被拷贝到可执行文件里,而是以独立文件形式存在于 App Bundle 中。

文件格式:

  • .dylib — 传统动态库(系统库使用,第三方不可提交 App Store)
  • .tbd — 动态库的文本描述文件(text-based stub),Xcode 链接系统库时使用
  • .framework — 可以是动态 framework(iOS 8+ 支持嵌入式动态 framework)

注意.framework 本身只是一种打包格式(目录结构),它既可以是静态的也可以是动态的,取决于内部二进制的 Mach-O 类型。


二、编译链接原理

静态库的链接过程

源代码 (.m/.swift)
       ↓ 编译器 (clang/swiftc)
目标文件 (.o)
       ↓ 归档工具 (ar)
静态库 (.a)
       ↓ 链接器 (ld) 将用到的 .o 拷贝进最终二进制
可执行文件 (Mach-O executable)

关键点:

  • 链接器做符号解析,只将被引用到的 .o 文件链接进来(粒度是 .o,不是函数)
  • 使用 -ObjC flag 时会链接所有包含 ObjC 类的 .o(解决 Category 不生效的问题)
  • 使用 -all_load 会强制链接所有 .o
  • 使用 -force_load <path> 可以对特定静态库强制全部链接
  • 静态库的代码最终融合进主二进制,运行时已不存在"库"的概念

动态库的链接过程

源代码 (.m/.swift)
       ↓ 编译 + 链接
动态库 (.dylib / .framework)
       ↓ 嵌入 App Bundle 的 Frameworks/ 目录
       ↓ 运行时 dyld 加载
进程地址空间

关键点:

  • 编译时只做符号检查,不拷贝代码,主二进制只记录"我依赖了哪个动态库"
  • dyld 在 App 启动时(或按需 dlopen)将动态库映射到进程地址空间
  • 动态库有独立的 install_name,指示 dyld 去哪里找它
  • 嵌入式 framework 的 install_name 通常是 @rpath/XXX.framework/XXX
  • 动态库在运行时保持独立,拥有自己的符号表和地址空间

核心区别图示

┌─────────────────────────────────────────────────────┐
│                    编译期                             │
│                                                     │
│  静态库:代码被拷贝 ──────→ 合并到主二进制               │
│  动态库:只记录依赖关系 ──→ 主二进制仅保存引用            │
│                                                     │
├─────────────────────────────────────────────────────┤
│                    运行期                             │
│                                                     │
│  静态库:不存在了,代码已在主二进制中                     │
│  动态库:dyld 加载 → rebase → bind → 映射到进程空间     │
│                                                     │
└─────────────────────────────────────────────────────┘

三、Mach-O 文件结构

无论静态库还是动态库,最终都与 Mach-O 格式密切相关。

Mach-O 文件结构:
┌──────────────────────┐
│      Header          │  ← 魔数、CPU 类型、文件类型
│                      │     MH_EXECUTE (可执行文件)
│                      │     MH_DYLIB   (动态库)
│                      │     MH_OBJECT  (目标文件,静态库内的 .o)
├──────────────────────┤
│   Load Commands      │  ← 描述 segment 布局、依赖的动态库列表、入口点等
│                      │     LC_LOAD_DYLIB 记录依赖的动态库
│                      │     LC_RPATH 指定运行时搜索路径
├──────────────────────┤
│   __TEXT Segment      │  ← 只读:机器码、字符串常量、Swift metadata
├──────────────────────┤
│   __DATA Segment      │  ← 可读写:全局变量、ObjC 元数据、GOT (全局偏移表)
├──────────────────────┤
│   __LINKEDIT Segment  │  ← 符号表、字符串表、代码签名信息
└──────────────────────┘

静态库(.a)的本质:不是 Mach-O 文件,而是多个 .o(Mach-O Object)的归档包。链接器从中提取需要的 .o 合并到最终的 Mach-O 可执行文件中。

动态库的本质:是一个完整的 Mach-O 文件(类型为 MH_DYLIB),有自己的 Header、Load Commands、Segments,运行时被 dyld 独立加载。


四、全面对比

维度静态库动态库
链接时机编译期,链接器完成运行期,dyld 完成
代码位置拷贝进主 Mach-O独立文件,位于 .app/Frameworks/
主二进制大小更大(包含库代码)更小(只记录依赖引用)
App Bundle 总大小通常更小(Strip 掉未用代码)可能更大(整个库都打包)
启动速度快,无额外加载开销慢,dyld 需要 load → rebase → bind
内存每个引用者各有一份拷贝系统库多进程共享;嵌入式库不共享
符号可见性合并到主二进制的全局符号表保持独立符号表,符号隔离
符号冲突风险高,容易 duplicate symbols低,各库符号空间独立
ObjC Category-ObjC flag 才能加载自动加载
链接时优化 (LTO)支持,编译器可跨库优化不支持,库边界是优化屏障
增量编译改库需重新链接整个 App改库只需重编该库
代码签名无需单独签名每个动态库需独立签名
Xcode 配置Do Not EmbedEmbed & Sign

五、优缺点详解

静态库的优点

  1. 启动速度快 — 不增加 dyld 加载数量,pre-main 阶段零额外开销
  2. 链接时优化 (LTO) — 编译器可以跨静态库边界做死代码消除、函数内联、常量折叠
  3. 包体积可控 — 链接器只拉入被引用的 .o,未用代码不会进入最终二进制
  4. 部署简单 — 最终只有一个 Mach-O,不需要管 Embed & Sign
  5. 无运行时依赖 — 不会出现 dylib not found / image not found 崩溃

静态库的缺点

  1. 代码重复 — 若主 App 和 Extension 都静态链接同一个库,代码各存一份
  2. 符号冲突 — 多个静态库包含同名符号时报 duplicate symbol 错误
  3. 编译链接耗时 — 主二进制越大,链接阶段越慢;改库后需重新链接整个 App
  4. 无法独立更新 — 库的任何改动都要重新编译发版

动态库的优点

  1. 系统库共享内存 — UIKit、Foundation 等系统动态库被所有 App 共享,节省内存
  2. 符号隔离 — 各动态库有独立命名空间,同名符号不冲突
  3. 增量编译友好 — 修改动态库只需重编该库,不影响主二进制链接
  4. 跨 Target 共享 — 主 App 和 Extension 可共用同一份动态 framework,避免代码重复
  5. 热替换理论可行 — 替换 .framework 文件即可更新逻辑(App Store 不允许,仅企业包/调试可用)

动态库的缺点

  1. 启动变慢 — 每个动态库在 pre-main 阶段都增加 dyld 加载耗时
  2. 包体积膨胀 — 动态库无法 Strip 未使用符号,整个库的代码都会打入 Bundle
  3. 签名复杂 — 每个动态 framework 需独立代码签名
  4. 沙盒限制 — iOS 不允许 dlopen App Bundle 外的动态库
  5. 运行时崩溃风险 — 库缺失或版本不匹配,启动时直接 crash
  6. 无法跨库 LTO — 编译器优化止步于动态库边界

六、启动性能原理 (dyld)

dyld 加载全流程

App 进程创建
  ↓
1. Load dylibs           递归加载主二进制依赖的所有动态库(及其传递依赖)
  ↓                       每个库:mmap 到虚拟内存 → 验证签名 → 注册
2. Rebase                 ASLR (地址空间布局随机化) 导致实际加载地址与编译地址不同
  ↓                       遍历所有内部指针,加上随机偏移量 (slide)
3. Bind                   解析跨库的外部符号引用
  ↓                       lazy binding: 首次调用时才解析(大部分函数)
  ↓                       non-lazy binding: 启动时立即解析(ObjC 元数据、C++ 虚表)
4. ObjC Runtime Setup     注册所有 ObjC 类到 runtime
  ↓                       插入 Category 的方法到类的方法列表
  ↓                       确保 selector 唯一性
5. Initializers           执行 +load 方法
  ↓                       执行 C/C++ __attribute__((constructor))
  ↓                       执行 Swift 全局变量的初始化器
  ↓
main() 被调用

每一步为什么耗时

阶段耗时原因与动态库数量的关系
Load磁盘 I/O + 签名验证线性正相关,库越多越慢
Rebase遍历 __DATA 段所有内部指针与库的数据段大小相关
Bind符号查找(哈希表查询)与跨库符号引用数量相关
ObjC Setup类注册 + Category 合并与 ObjC 类/Category 总数相关
Initializers执行用户代码+load 和 constructor 数量相关

dyld 2 vs dyld 3

特性dyld 2 (iOS 12 及以前)dyld 3 (iOS 13+)
解析时机每次启动都在进程内完整解析首次解析后缓存为 launch closure
安全性在 App 进程内解析(可被攻击)解析移到进程外守护进程
缓存closure 缓存后,后续启动跳过解析
冷启动首次略慢(多了写缓存),后续显著加速
热启动中等直接读取 closure,非常快

性能数据参考

动态库数量大致额外 pre-main 耗时 (iPhone 8 级别)
1-5 个~5-20ms
10-20 个~50-150ms
50+ 个~300ms+
100+ 个可能超过 400ms watchdog 阈值 (冷启动)

Apple 官方建议:嵌入式动态 framework 控制在 6 个以内。

静态库为什么不影响启动

静态库的代码在编译期已经合并进主二进制:

  • 不增加 Load dylibs 的数量
  • 不增加跨库 Bind 的符号数量
  • ObjC 类直接注册在主二进制中,无额外开销
  • 唯一的影响:主二进制变大 → mmap 主二进制的时间微增(可忽略)

七、符号解析原理

静态链接的符号解析

主程序引用 _doSomething (未定义符号 U)
         ↓
链接器在静态库中搜索
         ↓
找到 MyModule.o 中定义了 _doSomething (符号类型 T)
         ↓
将整个 MyModule.o 拷贝进主二进制
         ↓
符号变为已定义 (resolved)
  • 粒度是 .o 文件:即使只用了 .o 中的一个函数,整个 .o 都会被链接
  • 这就是为什么 SDK 开发者会把每个函数/类放在单独的 .m 文件中,以减少无用代码

动态链接的符号解析

主程序引用 _doSomething (标记为 external, lazy)
         ↓
编译时:链接器确认动态库中存在该符号 → 通过
         ↓
运行时:首次调用 _doSomething
         ↓
dyld 在动态库的符号表中查找 → 写入 GOT/lazy pointer
         ↓
后续调用直接走 GOT,无需再次查找
  • Lazy Binding:大部分函数调用使用,首次调用时才解析,分散了启动开销
  • Non-Lazy Binding:ObjC 类引用、__DATA 段指针等在启动时立即解析

符号冲突对比

静态库:同名符号 → duplicate symbol 编译错误(严格)

ld: duplicate symbol '_MyFunction' in:
    libA.a(module.o)
    libB.a(module.o)

动态库:同名符号 → 运行时 "先加载者胜"(flat namespace)或各自独立(two-level namespace,iOS 默认)

Two-Level Namespace (iOS 默认):
  调用 libA 的 _MyFunction → 解析到 libA 内部
  调用 libB 的 _MyFunction → 解析到 libB 内部
  不会混淆

八、内存与体积影响

内存模型对比

┌─────────── 静态库场景 ──────────────┐
│                                    │
│  App 进程内存:                      │
│  ┌──────────────────┐              │
│  │ 主二进制 (__TEXT)   │ ← 含库A代码  │
│  │ 主二进制 (__DATA)   │ ← 含库A数据  │
│  └──────────────────┘              │
│                                    │
│  Extension 进程内存:                │
│  ┌──────────────────┐              │
│  │ Extension (__TEXT) │ ← 又一份库A  │
│  │ Extension (__DATA) │ ← 又一份库A  │
│  └──────────────────┘              │
│                                    │
│  → 库A代码存在两份 (磁盘 + 内存)       │
└────────────────────────────────────┘

┌─────────── 动态库场景 ──────────────┐
│                                    │
│  App 进程内存:                      │
│  ┌──────────────────┐              │
│  │ 主二进制           │              │
│  │ 库A.framework     │ ←──┐ __TEXT  │
│  └──────────────────┘    │ 页共享   │
│                          │         │
│  Extension 进程内存:      │         │
│  ┌──────────────────┐    │         │
│  │ Extension         │    │         │
│  │ 库A.framework     │ ←──┘ 同一物理页│
│  └──────────────────┘              │
│                                    │
│  → __TEXT 段可跨进程共享物理内存页      │
│  → __DATA 段每个进程各自 copy-on-write│
└────────────────────────────────────┘

体积影响对比

因素静态库动态库
未使用代码链接器丢弃未引用的 .o整个库都打进 Bundle
LTO 死代码消除支持,可消除未使用的函数不支持跨库消除
多 Target 场景代码重复(每个 Target 一份)代码只存一份
Strip链接后可全局 Strip只能 Strip 库自身的调试符号
压缩 (App Thinning)主二进制参与整体压缩每个 framework 独立压缩

九、总结

维度胜出方说明
启动速度静态库不增加 dyld 加载开销
包体积 (单 Target)静态库死代码消除 + LTO 优化
包体积 (多 Target)动态库代码共享避免重复
编译速度动态库增量编译不影响主二进制
符号安全动态库Two-Level Namespace 隔离
运行时稳定性静态库无 image not found 风险
部署复杂度静态库无需管签名和 Embed
代码优化程度静态库支持跨库 LTO

核心原则:除非有明确的跨 Target 代码共享需求(如 App Extension),否则优先选择静态库。iOS 嵌入式动态库不具备系统级共享优势,带来的启动开销往往得不偿失。