Highlight
Apple 将 ld64 静态链接器速度提升了一倍,并在 dyld 中引入了 chained fixups 和 page-in linking 技术,让 App 构建更快、启动更快、体积更小。
核心内容
静态链接:从编译到可执行文件
你写的代码需要和其他库一起才能运行。静态链接器(ld)负责把多个 .o 文件和静态库合并成一个可执行文件。静态库(.a)本质上是用 ar 打包的 .o 文件集合,链接器只加载能解析未定义符号的 .o 文件。
这个选择性加载机制很巧妙,但也带来问题:链接器必须按固定顺序串行处理静态库,无法充分利用多核。
(01:57)
ld64 今年快了一倍
Apple 的静态链接器 ld64 在今年进行了大幅优化。通过并行化内容拷贝、LINKEDIT 构建、UUID 计算和代码签名哈希,链接速度提升了一倍。
(07:06)
动态链接:启动时的隐形成本
动态库(dylib)在构建时只记录符号引用和库路径,不复制代码。这减小了 App 体积,但把成本推迟到了启动时:dyld 需要在启动时加载、映射、解析符号、执行 fixup。
(15:47)
Chained fixups:更小的二进制,更快的启动
今年 Apple 引入了 chained fixups 格式。传统 fixup 需要在 LINKEDIT 中记录每个 fixup 位置,而 chained fixups 只在 LINKEDIT 中记录每页的第一个 fixup 位置,其余信息编码在 DATA 段本身的指针中。这大幅减小了 LINKEDIT 体积。
(22:37)
Page-in linking:内核帮你做 fixup
配合 chained fixups,dyld 引入了 page-in linking。启动时不再一次性对所有 dylib 执行 fixup,而是由内核在首次访问 DATA 页时惰性应用 fixup。这减少了启动时间和脏内存,DATA_CONST 页变为 clean page,可以被回收和重建。
(25:07)
详细内容
静态库的选择性加载机制
// main.c
int main() {
foo();
return 0;
}
// foo.c
void foo() {
bar();
}
// bar.c
void bar() {
// implementation
}
void unused_func() {
// never called
}
// baz.c
void baz() {
undef(); // undefined symbol
}
编译后得到 main.o、foo.o、bar.o、baz.o。将 bar.o 和 baz.o 打包成静态库 lib.a。
链接命令:
ld main.o foo.o -l.a -o app
关键点:
- 链接器按命令行顺序处理文件
- 先加载 main.o,发现 main 定义和 foo 未定义
- 再加载 foo.o,解析 foo,但新增 bar 未定义
- 检查静态库 lib.a,发现 bar.o 定义了 bar,加载 bar.o
- baz.o 虽然也在 lib.a 中,但没有未定义符号需要它,所以不被加载
- 这就是静态库的选择性加载,未使用的 .o 文件不会进入最终程序
加速静态链接的四个选项
1. -all_load:并行加载所有静态库内容
# Other Linker Flags
-all_load
适用场景:App 最终会加载静态库中的大部分内容。此选项让链接器并行解析所有静态库,绕过串行限制。但如果多个静态库实现了相同符号,会导致冲突。
2. -dead_strip:移除未使用代码
# Build Settings -> Dead Code Stripping -> YES
# 或在 Other Linker Flags 中添加
-dead_strip
配合 -all_load 使用,补偿因加载全部内容而增加的体积。链接器会移除从 main 不可达的代码和数据。
3. -no_exported_symbols:跳过导出符号表构建
# Other Linker Flags
-no_exported_symbols
主 App 通常不需要导出符号。跳过 exports trie 构建可节省数秒链接时间。先用以下命令检查导出符号数量:
dyld_info -exports /path/to/YourApp
不适用场景:App 加载了链接回主可执行文件的插件,或用 XCTest 以 App 为 host 运行测试。
4. -no_deduplicate:Debug 构建跳过去重
# Other Linker Flags
-no_deduplicate
C++ 模板展开会产生大量相同指令的函数,链接器会去重以减小体积。但去重算法很耗时。Xcode 14 默认在 Debug 配置中传递此选项。如果你使用自定义构建系统,确保 Debug 构建添加此选项。
动态链接的 fixup 机制
App TEXT 段:
call _malloc → 实际变成:
// stub 代码(TEXT 段)
ldr x16, [DATA, #malloc_ptr]
br x16
App DATA 段:
malloc_ptr: 0x0 ← dyld 启动时填入 libSystem 中 malloc 的实际地址
关键点:
- TEXT 段不可变(代码签名要求)
- 调用外部 dylib 函数时,链接器生成 stub 代码
- stub 从 DATA 段加载函数指针并跳转
- dyld 启动时只修改 DATA 段,完成符号绑定
- 所有 fixup 本质上都是 dyld 在 DATA 段设置指针
Chained fixups 格式
传统 fixup:LINKEDIT 记录每个 fixup 的地址位置。
Chained fixups:
- LINKEDIT 只记录每页 DATA 的第一个 fixup 位置
- 后续 fixup 通过指针中的偏移位”链式”链接
- 指针中 packed 了:是否是 bind、下一个 fixup 的偏移、符号索引或内部偏移
要求:
- 部署目标 iOS 13.4+
- Xcode 14 自动为符合条件的 target 生成
Page-in linking 工作流程
启动流程(优化前):
1. 加载主可执行文件
2. 递归加载依赖 dylib
3. 解析所有 bind 符号
4. 对所有 dylib 执行 fixup ← 耗时
5. 运行 initializers
启动流程(优化后):
1. 加载主可执行文件
2. 递归加载依赖 dylib
3. 解析所有 bind 符号
4. fixup 由内核在 page-in 时惰性执行 ← 省时间
5. 运行 initializers
关键点:
- 首次启动时 dyld 缓存绿色步骤的工作
- 后续启动复用缓存
- page-in linking 进一步优化 fixup 步骤
- 仅适用于启动时加载的 dylib,
dlopen的 dylib 仍由 dyld 传统处理
诊断工具
dyld_usage:追踪 dyld 启动行为
dyld_usage -t /Applications/TextEdit.app/Contents/MacOS/TextEdit
输出示例:
launch time: 15ms
fixups: 1ms ← page-in linking 的贡献
static initializers: 12ms ← 现在是大头
dyld_info:检查二进制文件
# 查看 fixup 信息
dyld_info -fixups /path/to/binary
# 查看导出符号
dyld_info -exports /System/Library/Frameworks/Foundation.framework/Foundation
# 查看 dyld cache 中的库
dyld_info -exports Foundation
核心启发
-
评估静态库 vs 动态库的平衡点:ld64 今年快了一倍,你可以承受更多静态库或直接编译进 App 的源文件,减少 dylib 数量以降低启动成本。用
dyld_usage测量启动时间,找到适合你项目的平衡点。 -
Debug 构建启用 -no_deduplicate:如果你使用 C++ 或自定义构建系统,确保 Debug 配置添加
-no_deduplicate,可显著缩短链接时间而不影响 Release 体积优化。 -
检查并启用 chained fixups:将部署目标提升到 iOS 13.4+,Xcode 14 会自动生成 chained fixups 格式。用
dyld_info -fixups验证你的二进制是否已使用新格式,在 iOS 16 上获得 page-in linking 的启动加速。 -
用 -no_exported_symbols 加速大型 App 链接:对主 App target 尝试添加
-no_exported_symbols,先用dyld_info -exports确认导出符号数量。如果数量很大(如数十万),此选项可节省数秒链接时间。 -
减少静态初始化器中的工作:
dyld_usage会显示 static initializers 的耗时。检查代码中全局变量的初始化逻辑,避免在 initializer 中做 I/O、网络请求或复杂计算。
关联 Session
- What’s new in Xcode — Xcode 14 的整体更新,包含更快的链接器
- Diagnose Power and Performance regressions in your app — 使用 Instruments 诊断性能问题
- Improve app size and runtime performance — 进一步优化 App 体积和运行时
- Profile and optimize your game’s memory — 内存分析和优化技巧
评论
GitHub Issues · utterances