一文了解 Xcode 生成「静态库」和「动态库」 的流程
作者:酷酷的哀殿
有位群友分享了一篇 京东零售技术 发表的技术文章 iOS 链接原理解析与应用实践。
原文提到,在 iOS App 开发中,程序的链接是由 Xcode 中自带的 LLVM 来帮助我们完成的,程序员们也因此更注重业务逻辑的编写。
恰好,笔者最近也曾经小范围分享过一篇涉及编译流程的文章。两篇文章对 链接 的描述存在一些差异。现将该文的部分内容略作调整摘抄如下。
希望能够通过本文解答 iOS APP 的链接是由谁完成的。
Xcode 名词讲解
对 Xcode 名词熟悉的同学可以直接跳过本部分。
构建(Build):从多个源码及相关的依赖库组件为单一的 构建产物(Product) 的过程
任务(Task): 我们经常执行的 构建(Build)、 测试(Test)、静态分析(Analyze)、归档(Archive) 都被 Xcode 当做 任务(Task) 对待
工具栏(Tool Bar):通过 工具栏(Tool Bar),可以完成 构建并运行 APP、查看构建进度 等工作
运行按钮(Run Button) (▶️):开发者触发构建的入口之一,通过该按钮,可以依次执行 构建(Build) 和 运行(Run)
Report navigator button:点击后,可以切换到 报告导航器(Report navigator[1])
报告导航器(Report navigator[2]) :可以查看 任务报告(task reports[3])、调试会话日志(debugging session logs[4])、和 机器人报告(bot reports[5])
Xcode 执行 构建(Build) 时,报告导航器(Report navigator[6]) 的右侧会显示一系列的 子 tasks,通常情况下,每个 子 tasks 会有一个 产物(output)
Open Transcript button:点击该按钮后,可以查看每个 子任务 详细信息
以下图为例:
第一行是 RuleName
:ComplieSwift normal ...
第二行是移动工作目录到到工程所在文件夹:cd /Users/...
第三行是真正的构建 产物(output) 的命令
Demo
首先,我们需要准备一个用于测试的 Demo。
它们的依赖关系如下所示:
构建概览
当我们通过 Run Button[7] 触发构建构建时,Build System
会以逆序的方式完成 构建(Build):
先构建
libDepA.a
和DepB.framework
最后构建
HostDemo
构建详解
静态库构建流程
对于 DepA
,完整的 构建(Build) 流程如下图所示:
本节会重点讲解与 链接 相关的步骤。
为什么需要创建链接辅助文件?
本例中,Xcode 构建系统 会创建下面的 链接辅助文件 辅助链接
链接辅助文件 的路径:
.../HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepA.build/Objects-normal/arm64/DepA.LinkFileList
链接辅助文件的内容:
.../HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepA.build/Objects-normal/arm64/DepA.o
假如链接时,Xcode 构建系统 是将所有的 .o
路径传给链接工具,会存在 命令参数过长 的问题。
为了解决上面的问题,Xcode 构建系统 会先 创建链接辅助文件,链接辅助文件 会存储需要链接的文件,然后只需要传递一个固定长度的参数给链接工具,链接工具会读取该文件的内容,并将所有的 .o
文件链接到一起
Xcode 构建系统如何链接静态库?
如下所示,通过 Report navigator[8] 的,我们可以看到 Xcode 构建系统 会调用 libtool
工具进行静态库构建:
Libtool /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos/libDepA.a normal (in target 'DepA' from project 'HostDemo')
cd /Users/HostDemo
export IPHONEOS_DEPLOYMENT_TARGET\=11.1
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/libtool \
-static \
-arch_only arm64 \
-D \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.2.sdk \
-L/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos \
-filelist /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepA.build/Objects-normal/arm64/DepA.LinkFileList \
-dependency_info /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepA.build/Objects-normal/arm64/DepA_libtool_dependency_info.dat \
-o /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos/libDepA.a
ps. -filelist
的参数是前面提到的 链接辅助文件 路径
libtool 与 llvm 有关系吗?
通过 -V
参数,我们可以发现 libtool
属于 苹果公司的 cctools
项目,版本号是 973.4
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/libtool -V
Apple Inc. version cctools-973.4
cctools
项目的源码可以通过下面的地址获取:
https://opensource.apple.com/tarballs/cctools/
cctools
通常会在 Xcode 发布一段时间后再公布,所以,我们只能获取到比较旧的版本 cctools-949.0.1.tar.gz[9]
通过本节内容,我们可以得到第一个结论:
结论一:静态库不依赖 llvm
。
动态库构建流程
DepB
属于动态库,它的构建过程会比较复杂:
在进行后面的分析前,我们需要先了解一个小知识:
小知识 1:当我们通过 **template**[10] 创建动态库时,Xcode 构建系统 实际上是读取 动态库配置文件
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/Project Templates/Base/Framework Base.xctemplate/TemplateInfo.plist
完成的。
OK,我们回到正题。
下面,我们重点讲解与静态库链接过程的差异点。
为什么存在 创建并编译版本号文件 步骤?
小知识 1 提到的 动态库配置文件 会存在如下的配置:
<key>Project</key>
<dict>
<key>SharedSettings</key>
<dict>
<key>VERSIONING_SYSTEM</key>
<string>apple-generic</string>
<key>CURRENT_PROJECT_VERSION</key>
<string>1</string>
<key>VERSION_INFO_PREFIX</key>
<string></string>
</dict>
</dict>
而 VERSIONING_SYSTEM
和 Xcode
的 `Build Settings`[11] 的 Versioning System
对应:
默认情况下,当 VERSIONING_SYSTEM
= Apple Generic[12] 存在时, Xcode 构建系统 会自动创建文件 $(PRODUCT_NAME)_vers.c
,并添加部分与版本号相关的代码。
以 DepB
为例,Xcode 构建系统 会自动创建文 DepB_vers.c
,并添加下面的代码:
extern const unsigned char DepBVersionString[];
extern const double DepBVersionNumber;
const unsigned char DepBVersionString[] __attribute__ ((used)) = "@(#)PROGRAM:DepB PROJECT:HostDemo-1.23" "\n";
const double DepBVersionNumber __attribute__ ((used)) = (double)1.23;
小知识 2:在上一篇 iOS 崩溃排查技巧:如何获取系统库源码 提到的版本号信息就是依赖
Apple Generic
生成的,我们后续也会对该过程进行详细的讲解
为什么需要 module.modulemap 文件 ?
与 VERSIONING_SYSTEM
类似, 动态库配置文件 会存在一个与 Module
相关的配置:
<key>DEFINES_MODULE</key>
<string>YES</string>
DEFINES_MODULE
与 Defines Module
相对应
当 DEFINES_MODULE
= YES
时,Xcode 构建系统 会自动创建文件 module.modulemap
framework module DepB {
umbrella header "DepB.h"
export *
module * { export * }
}
小知识 3:`Module`[13] 是 编译器 llvm 用于解决头文件引用导致重复编译等问题的方案。
Xcode 构建系统如何链接动态库?
如下所示,通过 Report navigator[14] ,我们可以看到 Xcode 构建工具 会调用 clang
进行链接操作:
Ld /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos/DepB.framework/DepB normal (in target 'DepB' from project 'HostDemo')
cd /Users/HostDemo
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang \
-target arm64-apple-ios11.1 \
-dynamiclib \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.2.sdk \
-L/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos \
-F/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos \
-filelist /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepB.build/Objects-normal/arm64/DepB.LinkFileList \
-install_name @rpath/DepB.framework/DepB \
-Xlinker \
-rpath \
-Xlinker @executable_path/Frameworks \
-Xlinker \
-rpath \
-Xlinker @loader_path/Frameworks \
-dead_strip \
-Xlinker \
-object_path_lto \
-Xlinker /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepB.build/Objects-normal/arm64/DepB_lto.o \
-Xlinker \
-export_dynamic \
-Xlinker \
-no_deduplicate \
-fembed-bitcode-marker \
-fobjc-arc \
-fobjc-link-runtime \
-Xlinker \
-no_adhoc_codesign \
-compatibility_version 1 \
-current_version 1 \
-Xlinker \
-dependency_info \
-Xlinker /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepB.build/Objects-normal/arm64/DepB_dependency_info.dat \
-o /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos/DepB.framework/DepB
那么,我们可以得到 clang
负责链接动态库的结论吗?
答案当然是否定的。
真正负责动态库的链接工具 -- ld64
通过添加 -v
参数,我们可以可以得到真正被执行的命令:
➜ clang -target arm64-apple-ios11.1 -dynamiclib ... -filelist .../DepB.LinkFileList ... -o .../DepB.framework/DepB -v
复制上面的命令并在 终端 执行,我们可以得到以下内容:
Apple clang version 12.0.0 (clang-1200.0.32.27)
Target: arm64-apple-ios11.1
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld" -demangle -lto_library /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib -dynamic -dylib -dylib_compatibility_version 1 -dylib_current_version 1 -arch arm64 -dylib_install_name @rpath/DepB.framework/DepB -dead_strip -platform_version ios 11.1.0 14.2 -bitcode_bundle -bitcode_process_mode marker -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.2.sdk -o /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos/DepB.framework/DepB -L/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos -filelist /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepB.build/Objects-normal/arm64/DepB.LinkFileList -rpath @executable_path/Frameworks -rpath @loader_path/Frameworks -object_path_lto /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepB.build/Objects-normal/arm64/DepB_lto.o -export_dynamic -no_deduplicate -no_adhoc_codesign -dependency_info /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Intermediates.noindex/HostDemo.build/Debug-iphoneos/DepB.build/Objects-normal/arm64/DepB_dependency_info.dat -framework Foundation -lobjc -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/12.0.0/lib/darwin/libclang_rt.ios.a -F/var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/T/Deri/HostDemo-dfpsksztypjutkfvwedupiwcqrks/Build/Products/Debug-iphoneos
精简后的版本:
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld" \
-o ../DepB.framework/DepB \
-filelist ../DepB.LinkFileList -rpath @executable_path/Frameworks
我们可以看到最后被调用的是 ld
。
与之前的操作类似,通过添加 -v
参数的方式可以得到 ld
属于 Project:ld64
,版本号是 609.7
➜ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld -v
@(#)PROGRAM:ld PROJECT:ld64-609.7
BUILD 18:10:07 Oct 19 2020
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em
LTO support using: LLVM version 12.0.0, (clang-1200.0.32.27) (static support for 27, runtime is 27)
TAPI support using: Apple TAPI version 12.0.0 (tapi-1200.0.23.4)
小知识 4:本节中,
clang
的作用是 Driver,clang
只负责分析输入的参数并组装成真正执行的命令。后续会专门写文章进行讲解
ld64 与 llvm 有关系吗?
ld64
的源码同样可以从苹果开源获取:
https://opensource.apple.com/tarballs/ld64/
通过本节内容,我们可以得到第二个结论:
结论二:动态库不依赖 llvm。
总结
通过分析 Report navigator[15] 页面的详细 构建(Build) 日志,我们可以得到下面两个结论:
只有在 动态库
的链接过程会依赖clang Driver
。真正执行生成 静态库 与 动态库 的任务的是 libtool
和ld64
。
推荐阅读
关注我们
我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。
关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。
参考资料
Report navigator: https://help.apple.com/xcode/mac/11.4/#/dev21d56ecd4
[2]Report navigator: https://help.apple.com/xcode/mac/11.4/#/dev21d56ecd4
[3]task reports: https://help.apple.com/xcode/mac/11.4/#/dev7e8b473cc
[4]debugging session logs: https://help.apple.com/xcode/mac/11.4/#/deva96503d0a
[5]bot reports: https://help.apple.com/xcode/mac/11.4/#/dev5501bae0f
[6]Report navigator: https://help.apple.com/xcode/mac/11.4/#/dev21d56ecd4
[7]Run Button: https://help.apple.com/xcode/mac/11.4/#/devdc0193470
[8]Report navigator: https://help.apple.com/xcode/mac/11.4/#/dev21d56ecd4
[9]cctools-949.0.1.tar.gz: https://opensource.apple.com/tarballs/cctools/cctools-949.0.1.tar.gz
[10]template: https://help.apple.com/xcode/mac/11.4/#/dev07db0e578
[11]Build Settings
: https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6
Apple Generic: https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6
[13]Module
: https://clang.llvm.org/docs/Modules.html#module-map-language
Report navigator: https://help.apple.com/xcode/mac/11.4/#/dev21d56ecd4
[15]Report navigator: https://help.apple.com/xcode/mac/11.4/#/dev21d56ecd4