查看原文
其他

一文了解 Xcode 生成「静态库」和「动态库」 的流程

酷酷的哀殿 老司机技术 2022-08-26

作者:酷酷的哀殿

有位群友分享了一篇 京东零售技术 发表的技术文章 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:点击该按钮后,可以查看每个 子任务 详细信息

以下图为例:

第一行是 RuleNameComplieSwift normal ...

第二行是移动工作目录到到工程所在文件夹:cd /Users/...

第三行是真正的构建 产物(output) 的命令

Demo

首先,我们需要准备一个用于测试的 Demo。

它们的依赖关系如下所示:

构建概览

当我们通过 Run Button[7] 触发构建构建时,Build System 会以逆序的方式完成 构建(Build)

  • 先构建 libDepA.aDepB.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 属于动态库,它的构建过程会比较复杂:

image-7002123550

在进行后面的分析前,我们需要先了解一个小知识:

小知识 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_SYSTEMXcode`Build Settings`[11]Versioning System 对应:

image-232802931

默认情况下,当 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_MODULEDefines Module 相对应

image-233942225

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 的作用是 Driverclang 只负责分析输入的参数并组装成真正执行的命令。

后续会专门写文章进行讲解

ld64 与 llvm 有关系吗?

ld64 的源码同样可以从苹果开源获取:

https://opensource.apple.com/tarballs/ld64/

通过本节内容,我们可以得到第二个结论:

结论二:动态库不依赖 llvm

总结

通过分析 Report navigator[15] 页面的详细 构建(Build) 日志,我们可以得到下面两个结论:

  • 只有在 动态库 的链接过程会依赖 clang Driver
  • 真正执行生成 静态库动态库 的任务的是 libtoolld64

推荐阅读

✨ iOS 性能监控:Runloop 卡顿监控的坑

✨ iOS 崩溃排查技巧:如何获取系统库源码

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。

参考资料

[1]

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

[12]

Apple Generic: https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6

[13]

Module: https://clang.llvm.org/docs/Modules.html#module-map-language

[14]

Report navigator: https://help.apple.com/xcode/mac/11.4/#/dev21d56ecd4

[15]

Report navigator: https://help.apple.com/xcode/mac/11.4/#/dev21d56ecd4


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存