这是一篇来自 Pol Piella Abadia 的文章,首要介绍了今年 WWDC 中一个十分低调却蛮重要的新特性 mergeable libraries(可兼并库)。主张阅览。

在阅览之前,首要需求了解几个关键词和它们之间的区别,有助于理解下文:

链接 link:声明对一个库的引证关系,声明后才能在代码中运用对应库的接口,也是编译链接时的信息来源

嵌入 embed:将对应的库文件塞到最终的运用包中。静态库会直接塞到运用的二进制文件中,动态库会塞到运用 bundle 的 Framework 文件夹中。

签名 sign:对库进行签名,运转时会对全部库进行签名验证。编译时会对主二进制以及全部库一起签名,这也就导致了后下发的动态库是无法履行的(过不了签名验证)。

合入 merge:区别于嵌入,是 mergeable libraries 特有的一种将库塞入运用包中的方法。

在曩昔,当咱们构建一个库时,需求做一个决定:是让这个库构建成静态库仍是动态库。这个决定是需求深思熟虑的,由于咱们的挑选或许会对运用程序的构建耗时和发动时刻发生连锁反应。

静态库的长处是不会影响运用程序的发动时刻(由于静态库不需求在运转时进行动态查找),但它们会导致运用程序的二进制巨细和编译时刻添加。另一方面,动态库不会影响构建时刻和运用程序的巨细(动由于态库不是运用程序二进制文件的一部分),但它们需求在运转时被找到和加载,所以它们对运用程序的发动时刻有负面影响。

好消息是,从 Xcode 15 开端,咱们不再需求在这两个类型中纠结。咱们现在能够运用 mergeable libraries 。这是一种新式的库,结合了动态和静态库的长处。它针对构建时刻和发动时刻进行了优化,在运用方法上更像是静态库。

mergeable libraries 是在 Meet mergeable libraries WWDC 会议上介绍的,它们在苹果的文档中有自己的页面,假如你对 mergeable libraries 感兴趣,引荐你去阅览。

在这篇文章中,我将向你展示 mergeable libraries模块化代码中的用途,以及怎么装备 Xcode 项目以开端选用它们。

链接动态结构

让咱们从一个简略的比如开端,了解动态结构在 iOS 运用中是怎么被链接的。

假设咱们有一个 iOS 运用,它依靠一个动态结构 Home 。一起,Home 还依靠了别的两个动态库: HomeCore 和 HomeUI 。

【译】了解 mergeable libraries(可合并库)

Home 直接引证了 HomeCore 和 HomeUI,运用的 target 只直接依靠了 Home 结构。因而如上图所示,咱们能够在运用的 target 中链接 Home 模块(并将其嵌入),然后在 Home 的 target 中链接 HomeCore 和 HomeUI 模块(不嵌入它们 — 稍后告知咱们原因)。

在模拟器上构建和运转运用程序没有问题,可是当在设备上运转该运用程序时,咱们得到了一个溃散,操控台日志如下:

dyld: Library not loaded: @rpath/HomeCore.framework/HomeCore
...
dyld: Library not loaded: @rpath/HomeUI.framework/HomeUI
...

嵌入短少的结构

在运用动态结构时,dyld: Library not loaded 的溃散是十分常见的。这种溃散通常发生在有动态结构被链接但没有被嵌入和签名的情况下,就像本例中的 HomeCore 和 HomeUI 。

与静态库的作业方法相反,动态结构不是主库或二进制的一部分。相反,它们是在运转时被查询并调用的。这意味着,当运用程序编译和装置时,不会有任何报错。但当运用程序发动时,动态链接器将在运用程序 bundle 中寻觅 HomeCore 和 HomeUI 结构。咱们这个比如动态链接器肯定是找不到这两个库的,由于咱们还没有嵌入它们。

为了验证这一点,咱们能够经过 otool 查看运用程序二进制文件在运转时将寻觅哪些动态结构:

otool -L ~/Library/Developer/Xcode/DerivedData/MergeableLibraries-<hash>/Build/Products/Release-iphoneos/MergeableLibraries.app/MergeableLibraries

上面的指令打印了以下结果,这表明该运用程序依靠于一个名为 Home 的动态结构和设备上的一堆体系结构:

/Users/polpielladev/Library/Developer/.../Debug-iphoneos/MergeableLibraries.app/MergeableLibraries:
	@rpath/Home.framework/Home (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 2036.0.0)
	/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    ...

假如咱们现在用相同的指令查看 Home 结构,咱们会看到它在全部体系依靠的基础上依靠于别的两个动态结构 HomeCore 和 HomeUI:

/Users/polpielladev/Library/Developer/.../Debug-iphoneos/MergeableLibraries.app/Frameworks/Home.framework/Home:
	@rpath/Home.framework/Home (compatibility version 1.0.0, current version 1.0.0)
	@rpath/HomeUI.framework/HomeUI (compatibility version 1.0.0, current version 1.0.0)
	@rpath/HomeCore.framework/HomeCore (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 2036.0.0, weak)
	/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    ...

什么是 @rpath

正如咱们在 otool 指令的输出中看到的,被查看的二进制文件不知道它们所依靠的动态库的完好途径。相反,它们依靠于一个叫做 @rpath 的特点。这个 @rpath 特点在运转时被解析,通常被设置为相对于运用程序二进制文件的途径。

在 Xcode 中,@rpath 是经过 Runpath Search Paths 设置的,它需求一个链接器在运转时用来定位动态结构的位置列表。

默许情况下,Xcode 将此构建设置为 @executable_path/Frameworks,这意味着链接器将在运用程序二进制文件内的 Frameworks 文件夹中寻觅动态结构。

假如咱们查看运用程序的包文件夹,就能明白溃散发生的原因。HomeCore 和 HomeUI 结构没有出现在 Frameworks 文件夹中:

【译】了解 mergeable libraries(可合并库)

Umbrella frameworks?

既然知道了溃散的原因,咱们接下来就需求嵌入和签名缺失的动态结构,以便在运转时能够找到它们。

咱们的第一直觉或许是直接在 Home target 中嵌入和签名 HomeCore 和 HomeUI 这两个结构。然而,这并不是正确的做法,由于这将将创建一个 Umbrella frameworks(即一个包含其他结构的结构),这是苹果强烈反对的做法(这便是上文咱们没有直接把 HomeCore 和 HomeUI 直接嵌入 Home 的原因)。

Don’t Create Umbrella Frameworks

While it is possible to create umbrella frameworks using Xcode, doing so is unnecessary for most developers and is not recommended. Apple uses umbrella frameworks to mask some of the interdependencies between libraries in the operating system. In nearly all cases, you should be able to include your code in a single, standard framework bundle. Alternatively, if your code was sufficiently modular, you could create multiple frameworks, but in that case, the dependencies between modules would be minimal or nonexistent and should not warrant the creation of an umbrella for them.

苹果在文档中仅仅点名了不主张创建 Umbrella Frameworks (虽然 Xcode 能够),但并没有说明原因。

原因首要是由于假如嵌入的是公开的三方库,很有或许其他库也嵌入了相同的三方库,这不只会形成代码量的添加,还会导致因重复的符号而导致的链接失利,乃至是运转时问题。这边有一个更具体的回答。

正确的办法

咱们需求在运用程序的 target 中链接并嵌入和签名 HomeUI 和 HomeCore ,而不是在 Home 的 target 中嵌入它们。在做出这一改变后,能够看到 HomeCore 和 HomeUI 结构现在也存在于 Frameworks 文件夹中:

【译】了解 mergeable libraries(可合并库)

这是模块化运用程序中十分常见的办法,效果很好,但有一些缺陷:

  1. 结构不再是独立的。咱们不只要链接Home,还必须链接HomeCoreHomeUI
  2. 咱们或许向调用方公开了太多信息。运用 target 只需求知道Home,但它现在也能够拜访内部HomeCoreHomeUI接口,即使它不需求它们。
  3. 嵌入在运用方针中的每个动态模块都需求在运转时加载,这将添加运用的发动时刻。

Mergeable libraries

在引进 Mergeable libraries 之前,解决上述问题的唯一办法是尽或许运用静态库。可是,这或许是一项十分艰巨的使命,乃至是不或许的,尤其是在处理第三方动态依靠项或资源时,有时乃至需求更改整个项目中的依靠途径。

跟着 Xcode 15 中 Mergeable libraries 的引进,这全部都发生了变化。咱们现在能够告知 Xcode 兼并一个动态结构,而不是动态链接它,Xcode 将担任其余的作业。Mergeable libraries 和 Xcode 经过优化,使得整个运用体会规划得像是在运用静态库,一起具有最佳的构建时刻和发动时刻性能。

在 WWDC 的讲座中,Cyndy Mtenga Ishida 共享了以下幻灯片,该幻灯片完美地总结了 Mergeable libraries

【译】了解 mergeable libraries(可合并库)

主动兼并

兼并一个动态结构的最简略办法是在 target 中设置主动兼并。将Create Merged Binary设置为Automatic。然后,Xcode 会将方针的全部直接依靠构建为 Mergeable libraries,并将它们兼并到方针的二进制文件中。

让咱们在Home结构中设置它:

【译】了解 mergeable libraries(可合并库)

现在,咱们能够删去从运用 target 到HomeCoreHomeUI的引证,并保持嵌入和签名Home结构(Home 会持续以传统动态库的方法引进项目)。运用程序现在可在任何设备上成功运转,每个 target 仅嵌入他们需求的内容。

让咱们更进一步,看看咱们是否能够将Home兼并到运用程序二进制文件中。让咱们在App的方针中将Create Merged Binary设置为Automatic

咱们需求在运用程序方针中保存指向Home结构的链接,但咱们现在不再需求嵌入它,让 Xcode 担任将其兼并到运用程序二进制文件中。假如咱们在设备上运转它,全部仍将按预期作业。

Automatic 会默许将全部直接依靠的库都以兼并的方法引进 target,假如你想要更加精密的操控需求兼并的库,能够将Create Merged Binary设置为Manual。此刻,你就需求独自设置每个被依靠的库,Build Mergeable Library设置为YesNo来指定当时这个库是否能够以兼并的方法被引进到项目中。

上面讲的或许有点乱,咱们直接来个实操,手动兼并Home结构中的全部依靠项:

  1. Home结构中将Create Merged Binary生成设置设置为Manual
    【译】了解 mergeable libraries(可合并库)
  2. HomeCoreHomeUI结构中将Build Mergeable Library生成设置设置为Yes
    【译】了解 mergeable libraries(可合并库)
    咱们还能够经过以下方法手动将Home结构兼并到运用程序二进制文件中:
  3. 在运用方针中将Create Merged Binary生成设置设置为Manual
    【译】了解 mergeable libraries(可合并库)
  4. Home结构中将Build Mergeable Library生成设置设置为Yes
    【译】了解 mergeable libraries(可合并库)

Build Mergeable Library是用来操控,当自己被别人引证时,是否能够以兼并的方法引证。

Create Merged Binary 是用来操控,自己在引证其他库时,是默许都尝试用兼并的方法引证,仍是依据每个库自己的装备(Build Mergeable Library)来引证,仍是完全不运用兼并的方法引证。

生成的二进制文件

现在全部依靠项都是可兼并的,并且咱们不再手动嵌入任何结构,咱们能够查看运用包中生成的二进制文件,并和咱们之前的文件做个比较。

调试与发布

首要查看运用的包内容:

【译】了解 mergeable libraries(可合并库)
能够看到,虽然咱们不再需求嵌入动态结构,但它们仍然出现在了 Framework 文件夹中。

这是由于咱们正在运用Debug装备构建运用程序,为了使调试更简略并使增量构建更快,Xcode 会将动态结构从头导出到 bundle 中。

假如咱们在 release 模式下再次构建项目并从头查看运用的包内容,咱们将看到动态结构不再存在:

【译】了解 mergeable libraries(可合并库)

查看动态链接的结构

现在咱们现已为运用程序生成了一个兼并的二进制文件(bundle 中没有动态结构),咱们能够像曾经相同运用otool查看它,以确保没有对兼并的动态结构的引证:

otool -L ~/Library/Developer/Xcode/DerivedData/MergeableLibraries-<hash>/Build/Products/Release-iphoneos/MergeableLibraries.app/MergeableLibraries

上面的指令发生以下输出:

/Users/polpielladev/.../Build/Products/Release-iphoneos/MergeableLibraries.app/MergeableLibraries:
	/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 2036.0.0)
	/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
	/System/Library/Frameworks/DeveloperToolsSupport.framework/DeveloperToolsSupport (compatibility version 1.0.0, current version 21.0.8)
	/System/Library/Frameworks/SwiftUI.framework/SwiftUI (compatibility version 1.0.0, current version 5.0.59)
	/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 7058.3.110, weak)
	/usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 5.9.0)
	/usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 120.100.0, weak)
	/usr/lib/swift/libswiftCoreImage.dylib (compatibility version 1.0.0, current version 2.0.0, weak)
	/usr/lib/swift/libswiftDarwin.dylib (compatibility version 1.0.0, current version 0.0.0, weak)
	/usr/lib/swift/libswiftDataDetection.dylib (compatibility version 1.0.0, current version 750.0.0, weak)
	/usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 32.0.0, weak)
	/usr/lib/swift/libswiftFileProvider.dylib (compatibility version 1.0.0, current version 1492.0.0, weak)
	/usr/lib/swift/libswiftMetal.dylib (compatibility version 1.0.0, current version 341.1.0, weak)
	/usr/lib/swift/libswiftOSLog.dylib (compatibility version 1.0.0, current version 4.0.0, weak)
	/usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 8.0.0, weak)
	/usr/lib/swift/libswiftQuartzCore.dylib (compatibility version 1.0.0, current version 3.0.0, weak)
	/usr/lib/swift/libswiftUniformTypeIdentifiers.dylib (compatibility version 1.0.0, current version 785.0.0, weak)
	/usr/lib/swift/libswiftos.dylib (compatibility version 1.0.0, current version 1040.0.0, weak)
	/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 2036.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1600.135.0)

如咱们所见,只要对体系结构 。这意味着上面提到的三个库都不是以动态库的方法引进工程了。

查看二进制文件的符号

现在让咱们看看运用程序本身中是否嵌入了上面三个库中的符号。

在二进制上运转nm并检索符号列表:

nm -gU ~/Library/Developer/Xcode/DerivedData/MergeableLibraries-<hash>/Build/Products/Release-iphoneos/MergeableLibraries.app/MergeableLibraries

上面的指令生成了这个符号列表,其间咱们能够看到有对HomeCore接口的引证,这是咱们兼并的结构之一:

...
0000000100005378 T _$s8HomeCore0aB3APIC5helloSSvM
00000001000060b8 S _$s8HomeCore0aB3APIC5helloSSvMTq
00000001000052e0 T _$s8HomeCore0aB3APIC5helloSSvg
00000001000060a8 S _$s8HomeCore0aB3APIC5helloSSvgTq
0000000100005e50 S _$s8HomeCore0aB3APIC5helloSSvpMV
0000000100005e58 S _$s8HomeCore0aB3APIC5helloSSvpWvd
00000001000052cc T _$s8HomeCore0aB3APIC5helloSSvpfi
0000000100005328 T _$s8HomeCore0aB3APIC5helloSSvs
00000001000060b0 S _$s8HomeCore0aB3APIC5helloSSvsTq
00000001000053b8 T _$s8HomeCore0aB3APICACycfC
00000001000060c0 S _$s8HomeCore0aB3APICACycfCTq
00000001000053ec T _$s8HomeCore0aB3APICACycfc
0000000100005448 T _$s8HomeCore0aB3APICMa
000000010000c6d8 D _$s8HomeCore0aB3APICMm
0000000100006074 S _$s8HomeCore0aB3APICMn
000000010000c718 D _$s8HomeCore0aB3APICN
0000000100005424 T _$s8HomeCore0aB3APICfD
0000000100005408 T _$s8HomeCore0aB3APICfd
0000000100005e48 S _HomeCoreVersionNumber
0000000100005e18 S _HomeCoreVersionString
0000000100005de0 S _HomeUIVersionNumber
0000000100005db8 S _HomeUIVersionString
0000000100005da0 S _HomeVersionNumber
0000000100005d78 S _HomeVersionString
0000000100000000 T __mh_execute_header
0000000100005154 T _main
...

上面列表中的每个条目都能够运用swift demangle进行解析,以生产咱们能够直接阅览的内容。

例如,上面列表中的符号s8HomeCore0aB3APIC5helloSSvsTq运转swift demangle将回来以下内容:

$s8HomeCore0aB3APIC5helloSSvsTq ---> method descriptor for HomeCore.HomeCoreAPI.hello.setter : Swift.String

实际便是下面的代码:

import Foundation
public class HomeCoreAPI {
    public var hello = "Hello"
    public init() {}
}

因而,咱们能够确认兼并的库中的符号是直接嵌入到二进制文件本身中,就像静态库相同!

剥离导出的符号

运用程序二进制文件中被嵌入额定符号或许会对其最终巨细发生负面影响。Xcode 默许会去除重复的符号来优化最终的二进制巨细,但咱们能够进一步优化。

运用程序二进制文件中嵌入的一些符号为exported,假如运用程序没有任何扩展,咱们能够经过在运用程序 target 的Other Linker Flags中设置-Wl,-no_exported_symbols来安全地去除它们:

【译】了解 mergeable libraries(可合并库)

假如要去除全部不必要的导出符号并仅保存所需的符号,Apple 主张在方针的构建设置中提供导出列表文件。

个人总结

简略讲,mergeable libraries 能够让库一起表现出动态库和静态库的特性。Xcode 默许在 debug 装备下,库会像动态库相同被塞入运用 bundle 的 Framework 文件夹下并动态链接到宿主中,以提高编译速度。而在 release 装备下,库会像静态库相同被打入运用的二进制文件中,以提高运用发动时刻。

不只仅是静态动态“我全都要”的优势,mergeable libraries 还能简化依靠关系,减少宿主对依靠的感知(不过现在咱们根本都适用 CocoaPos 或 SPM 等依靠管理工具,或许感知不强)。

便是不知道具体用起来有没有坑,由于真实项目的依靠或许会十分复杂。我理解 mergeable libraries 仅仅多了支撑把库在动/静直接切换的能力,但条件是你的库本身就支撑动态和静态链接,假如不行仍是没戏。好在 mergeable libraries 不具有传染性,能够一部分依靠库走 mergeable libraries 一部分走动/静态链接。