Highlight
Xcode 15 引入 Mergeable Libraries,通过静态链接器在构建时生成元数据,使动态库既能像静态库一样合并到最终二进制中以减小体积、加速启动,又能在开发时保持动态库的构建速度优势。
核心内容
静态库与动态库的权衡
开发者在选择库类型时长期面临两难:
静态库在构建时将代码复制到 App 二进制中。好处是启动快,不需要 dyld 加载。坏处是每次代码变更都要重新链接,迭代开发和调试变慢。
动态库(dylib)在构建时只记录安装路径,代码不复制。好处是构建快,添加或更新库时代价小。坏处是启动时 dyld 必须找到并加载所有依赖,框架越多,内存占用和启动时间越长。
(00:55)
Apple 平台对系统库做了大量优化,但嵌入到 App bundle 中的第三方框架享受不到这些优化。因此历史上需要针对具体应用测量权衡。
Mergeable Libraries 的工作原理
Mergeable Libraries 的核心是在动态库构建时生成额外的元数据(metadata)。
(04:06)
当静态链接器创建动态库时,使用 -make_mergeable 选项记录元数据。这些元数据嵌入在二进制内部,使链接器在后续链接时可以将该库当作类似静态库处理。
合并时使用 -merge_library 或 -merge_framework 选项,链接器利用元数据将库的内容合并到最终输出中。合并后的二进制保持原有的文件类型(可执行文件仍是可执行文件,框架仍是框架)。
(05:10)
合并带来的好处:
- 体积减小:链接器可以跨库去重字符串、符号引用、Objective-C selector、objc_msgSend stub
- 启动加速:dyld 和内核需要加载的框架减少,内存占用降低
- 构建保持快速:Debug 模式下不执行合并,保持动态链接的构建速度
Debug 模式的特殊处理
Debug 模式下,链接器不会真正合并库,而是使用 reexport(重新导出)机制。
(16:41)
Reexport 允许代码实现驻留在一个动态库中,但对外显示为另一个库实现。这意味着 App Extension 或测试目标只需依赖合并后的目标,就能访问所有库的 API,构建速度与纯动态链接相当。
详细内容
自动合并配置
在 Xcode 中启用自动合并只需一个设置:
MERGED_BINARY_TYPE = automatic
(09:02)
在 Xcode 界面中:
- 选中 App Target
- 进入 Build Settings
- 搜索 “Create Merged Binary”
- 将值设为 “Automatic”
自动合并会将所有直接依赖的嵌入框架目标变为 mergeable,系统框架(如 SwiftUI)不受影响。
减少导出符号以优化体积
合并后,库的导出符号会保留在 App 中。App 通常不需要导出符号,这会对体积和构建时间产生负面影响。
(10:04)
在 Other Linker Flags 中添加:
-Wl,-no_exported_symbols
如果 App 需要为 App Extension 保留入口点,使用 Export Symbols File 精确控制保留哪些符号。
手动合并配置
当只有部分框架需要合并时,使用手动模式:
// 在合并目标上
MERGED_BINARY_TYPE = manual
// 在要合并的库目标上
MERGEABLE_LIBRARY = YES
// 在保留的库目标上(默认)
MERGEABLE_LIBRARY = NO
(11:10)
手动合并的典型场景:创建一个 “group library”(如 ForestKit),将多个内部框架合并进去,App 和测试目标都链接这个 group library。
运行时查找 API 的限制
如果代码使用 dlopen 或 NSBundle.bundleForClass 等运行时查找 API,需要注意:
(19:26)
dlopen的路径需要指向合并后的框架目标Bundle(for:)/bundleForClass:在 iOS 12 之前依赖框架二进制来发现 bundle,合并后的框架在运行时没有独立二进制- 最低部署版本需要 iOS 12+ 才能使用 bundle 查找支持
- 如果不依赖这些 API,可以添加
-no_merged_libraries_hook选项避免部署版本限制
分发 Mergeable 库
可以通过 Swift Package Manager 或 Xcode 创建 XCFramework 来分发 mergeable 库。
(23:17)
- Mergeable 元数据大约使 dylib 体积翻倍
- 元数据在 App 构建后会被丢弃,不影响最终 App 体积
- 嵌入 App 时会自动剥离元数据,防止体积膨胀
核心启发
-
将静态库迁移为 Mergeable 动态库
- 做什么:把项目中大量使用的静态库改为动态库并启用 mergeable
- 为什么值得做:保留静态库的启动性能优势,同时获得动态库的构建速度优势
- 怎么开始:将库目标从 Static Library 改为 Framework,设置
MERGEABLE_LIBRARY = YES
-
为大型 App 启用自动合并
- 做什么:在 App Target 上开启 Automatic Merging
- 为什么值得做:一个开关即可减少嵌入框架数量,降低包体积和启动时间
- 怎么开始:在 Build Settings 中搜索 “Create Merged Binary”,设为 Automatic
-
创建 Group Library 简化依赖图
- 做什么:将多个内部框架合并到一个 group framework 中
- 为什么值得做:App Extension 和测试目标只需链接一个框架,减少 dyld 加载开销
- 怎么开始:创建新的 Framework target,启用 manual merging,将子框架设为 mergeable
-
优化导出符号进一步减体积
- 做什么:使用
-no_exported_symbols或精确的导出符号文件 - 为什么值得做:App 通常不需要导出符号,去掉后链接器可以更有效地进行死代码剥离
- 怎么开始:在 Other Linker Flags 中添加
-Wl,-no_exported_symbols
- 做什么:使用
关联 Session
- Link fast: Improve build and launch times — 深入了解链接优化,提升构建和启动速度
- What’s new in Xcode 15 — Xcode 15 的新功能介绍
- Write Swift macros — Swift 宏的编写与使用
- Meet Swift Package Manager for plugins — Swift Package Manager 插件
评论
GitHub Issues · utterances