系统理解 iOS 库与框架

在软件开发中,静态库和动态库在各个方面为我们提供了便利。在计算机专业相关课程中,我们学习过静态库和动态库的一些理论,那么这些理论如何映射到 iOS 开发之中呢?iOS 中有很多相关概念和术语,对此,我并不是非常清晰。为了能够向编译优化的最终目标更近一步,我花了些时间进行了学习,并总结此文以供回顾复习。如果有什么写得不对的地方,欢迎指正。

静态库 VS 动态库

静态库和动态库的共同点在于:它们都是编译好的二进制文件;不同点在于:它们的用法不同。下面两张图可以看出两者在用法上的差异。

对于静态库,在应用程序进行编译链接时,会将静态库中的 被使用的部分 都添加到应用程序的可执行文件,这意味着应用程序的可执行文件大小会随着静态库数量的增加而增大。在运行时,静态库会随着应用程序的可执行文件一起加载到同一代码区。在 iOS 开发中,应用程序的可执行文件就是 ipa 解压后,包内容中与 app 同名的可执行文件。

对于动态库,事实上可以根据其加载时机分为两种:动态链接库动态加载库

  • 动态链接库:当加载目标主程序的可执行文件时,动态库也会被加载到内存中,即 在程序启动时加载。在 Build Phase 中的 Linked Framework and Library 阶段中声明的就是动态链接库,它们随着
  • 动态加载库:当用到相关功能时,使用 dlopen 或其他方式对相关动态库进行加载,即 在程序启动后加载

在 iOS 开发中,在项目设置【General】->【Frameworks, Libraries, and Embedded Content】中,定义了应用程序所依赖的静态库和动态库。

  • 对于 系统动态库,可以将 Embed 属性设置成 Do Not Embed,因为 iOS 系统提供了相关的库,我们无需将它们再嵌入到应用程序的 ipa 包中,如:Foundation.frameworkUIKit.framework
  • 对于 用户动态库,需要将 Embed 属性设置成 Embed,因为链接发生在运行时,链接器需要从应用程序的 ipa 包中加载完整的动态库。
  • 对于 静态库,需要将 Embed 属性设置成 Do Not Embed,因为链接发生在编译时,编译完成后相关代码都已经包含在了应用程序的可执行文件中了,无需在应用程序的 bundle 中再保存一份。

Library VS Framework

在 iOS 开发中,有两个令人难以分清的概念:框架(Framework)、(Library)。下面,我们来捋一捋这两个概念。

库是所有 UNIX 系统共有的,可移植;框架是 OSX/iOS 特有的,不可移植。为了提供对 OSX/iOS 的高级特性的支持,同时又不愿意这些特性被移植到其他平台,苹果提出了 框架 的概念。不过,从本质而言,框架是基于库实现的,可以认为框架是对库进行了封装,是一种特殊形式的库。

1
框架 = 库(静态库/动态库)+ .h(头文件) + bundle(资源包)

从本质而言,库是一个二进制文件。因此,对于 OSX/iOS 和 UNIX 而言,其具体格式也有所不同。在 OSX/iOS 中,库采用 Mach-O 格式进行存储;在 UNIX 中,库采用 ELF 格式进行存储。除此之外,库在两种操作系统中的命名后缀也有所一定的差异。对于 OSX/iOS,静态库的后缀是 .a,动态库的后缀是 .dylib;在 UNIX 中,静态库的后缀是 .a,动态库的后缀是 .so

在 OSX/iOS 中,还有一种后缀为 .tbd 的动态库。.tbd 文件本质上是一个 YAML 文本文件,其描述了需要链接的动态库信息,其主要目的是 减小应用程序的下载大小。当应用程序引用了用户设备中 /usr/lib/ 目录下的 .dylib 动态库时,会自动生成一个 .tbd 文件,其仅仅描述了项目中引用的动态库信息,因此可以减小应用程序的下载大小。具体细节见 传送门

Umbrella Framework

保护伞框架(Umbrella Framework)本质上可以认为是 对普通框架进行了封装,其包含了多个框架并隐藏它们彼此之间复杂的依赖关系。举个例子,Cocoa 框架就是一个保护伞框架,其包含了三个框架:AppKit、CoreData、Foundation。

1
2
3
4
5
6
7
8
9
10
/* 
Cocoa.h
Cocoa Framework
Copyright (c) 2000-2004, Apple Computer, Inc.
All rights reserved.
*/

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <CoreData/CoreData.h>

Framework Bundle Structure

上文提到框架本质上就是对库进行了封装,其包含一个或多个共享库以及相关的支持文件。下面,我们分别来看看普通框架和保护伞框架的目录结构。

首先,我们需要找到框架的保存位置。事实上,框架保存在文件系统中的多个位置:

  • /System/Library/Frameworks:保存苹果提供的框架,如:Foundation.frameworkAVFoundation.framework
  • /Library/Frameworks:保存第三方框架。iOS 上该目录为空。如:OpenVPN.frameworkCarthage.framework
  • ~/Library/Frameworks:保存用户提供的框架(如果有的话)。

此外,应用程序也可能会包含自己的框架,在其 Contents/Frameworks 目录下保存了应用程序专用的框架。其实,这里对应的就是应用程序 ipa 包中的 Frameworks 目录。

通过查看这些路径下的框架结构,我们可以发现大多是普通框架,其目录结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
MyFramework.framework/
Headers -> Versions/Current/Headers
MyFramework -> Versions/Current/MyFramework
Resources -> Versions/Current/Resources
Versions/
A/
MyFramework
Resources/
English.lproj/
InfoPlist.strings
Info.plist
Current -> A

框架使用了与应用程序不同的 bundle 结构。框架基于早期的 bundle 格式,支持同时将多个版本的框架和头文件存放在一个 bundle 中,也就是上述这种结构。这种类型的 bundle 被称为 Versioned Bundle

系统通过目录名称上的 .framework 扩展名来识别框架。框架目录下的各个目录和文件的作用如下:

  • Versions:存放了多个版本的框架,分别用 ABC... 这样的子目录进行保存。通过一个 Current 符号链接来选择使用哪个版本的框架。
  • Resources:基于 Versions/Current 的符号链接,指向当前版本的框架的 Resources 目录。
  • MyFramework:基于 Versions/Current 的符号链接,指向当前版本的框架的 MyFramework 目录。
  • Headers:基于 Version/Current 的符号链接,指向当前版本的框架的 Headers 目录,存放了希望暴露给开发者的公开头文件。

上面介绍了普通框架的结构,而保护伞框架的结构其实跟普通框架的结构基本一致,唯一区别在于:保护伞框架会多一个 Frameworks 目录,用于保存其封装的子框架。如下所示是 Core Servives 保护伞框架的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
CoreServices.framework/
CoreServices -> Versions/Current/CoreServices
CoreServices_debug -> Versions/Current/CoreServices_debug
CoreServices_profile -> Versions/Current/CoreServices_profile
Frameworks -> Versions/Current/Frameworks
Headers -> Versions/Current/Headers
Resources -> Versions/Current/Resources
Versions/
A/
CoreServices
CoreServices_debug
CoreServices_profile
Frameworks/
CarbonCore.framework
CFNetwork.framework
OSServices.framework
SearchKit.framework
WebServicesCore.framework
Headers/
Components.k.h
CoreServices-gcc3.p
CoreServices-gcc3.pp
CoreServices.h
CoreServices.p
CoreServices.pp
CoreServices.r
Resources/
Info-macos.plist
version.plist
Current -> A

除此之外,我们在构建自定义框架时,会发现框架结构下会有一个 Modudles 目录,该目录下默认有一个 module.modulemap 文件。事实上,module.modulemap 是一个模块映射文件,能够使框架支持模块(以及子模块),在编译时也能够利用模块编译的优势进行加速。关于模块相关的内容,可见 传送门

注意:CocoaPods 会将每个 pod 转换为 Umbrella Framework 并添加 module map 使其支持模块。因此,每个 Pod 的 Supporting File 中会有两个对应的文件:PodName-umbrella.h(Umbrella Header)、PodName.modulemap(Module Map)。

CocoaPods

在 Podfile 中有两个选项经常令人迷惑:use_frameworks!use_modular_headers!。那么这两者有什么区别呢?

我们知道在 Xcode 9 之前,不支持 Swift 静态库编译,因此 Swift pod 不得不使用动态库编译,即使用 use_frameworks!。但是,引用了大量动态库会导致应用程序启动时间变长。

好在 Xcode 9 之后开始支持 Swift 静态库编译。为了充分利用该特性,从 CocoaPods 1.5.0 开始,对于 Swift pod,开发者不用必须在 Podfile 中指定 user_frameworks! 以强制使用动态库编译。不过,要注意的是,如果一个 Swift pod 依赖了一个 OC pod,那么我们要为对应的 OC pod 开启 modular headersuse_modular_headers! 就会开启 modular headers)。那么,Swift 引用 OC 时为什么要开启 modular headers事实上,开启 modular headers 的本质就是将 pod 转换为 Modular(也就是支持模块),而 Modular 是可以直接在 Swift 中 import 的,不需要再经过 bridging-header 进行桥接,从而简化了 Swift 引用 OC 的方式

只有支持了模块的框架,才能支持通过模块化头文件(Modular Header)的方式进行导入。Clang 支持模块编译,能够加速编译,减少出错。我们可以通过添加 modulemap 文件使框架支持模块。

简化 Swift 引用 OC 的方式是使用 use_modular_headers! 的一个原因,除此之外,use_modular_headers! 还能够解决一个历史原因。

在 CocoaPods 诞生之初,其致力于封装尽可能多的第三方库。为此,CocoaPods 使用了较为宽松的头文件搜索路径(Header Search Paths),允许 pod 之间的相互引用,无需考虑命名空间,不必采用 #import <NameSpace/fileName.h> 的模块导入方式,允许采用 #import "fileName.h" 的导入方式。

但是,如果给 pod 添加 module map 使其支持模块化,会导致 #import "fileName.h" 无法正常导入。使用 use_modular_headers! 可以强制使用更优的模块导入方式。

在 CocoaPods 1.5.0 中,为了使用模块导入方式。对于 pod 开发者,可以在 pod_target_xxconfig 内设置 'DEFINES_MODULE' => 'YES'。对于 pod 使用者,可以在 Podfile 中添加 use_modular_headers! 指定采用模块导入的方式,也可以通过 :modular_headers => true 配置只让特定的 pod 采用模块导入的方式。

总结

本文首先介绍了静态库和动态库的区别,然后介绍了 iOS 中库与框架的概念及其区别,最后介绍了 CocoaPods 中 use_frameworks!use_modular_headers! 的区别。

参考

  1. iOS静态库与动态库的区别与打包
  2. 初识iOS中的动态库和静态库
  3. 深入剖析iOS动态链接库
  4. iOS 开发中的『库』(一)
  5. iOS 开发中的『库』(二)
  6. 细说iOS静态库和动态库
  7. Creating a Framework for iOS
  8. What is the difference between Embedded Binaries and Linked Frameworks
  9. 通过dlopen使用动态库
  10. Embedding Frameworks In An App
  11. iOS的静态库和动态库
  12. Why Xcode 7 shows .tbd instead of .dylib?
  13. Overview of Dynamic Libraries
  14. “Do Not Embed”, “Embed & Sign”, “Embed Without Signing”. What are they?. What they do?
  15. 《深入解析 MacOSX 与 iOS 操作系统》
  16. CocoaPods 1.5.0 — Swift Static Libraries
  17. 为什么应该用模块取代C/C++中的头文件?
  18. Introduction to Framework Programming Guide
  19. 应用 Swift 静态库的各种坑
  20. iOS - Umbrella Header在framework中的应用
  21. Module System of Swift
  22. cocoapods的静态库和动态库