库与打包
库
通常来说,库这个概念有狭义与广义之分。
狭义的库
一般是指来自C语言的概念,根据编译时的动态链接或静态链接,分为所谓的动态库和静态库。注意,狭义的库是一种源码编译后生成的二进制文件。
广义的库
程序员一般把别人写的且封装好的代码称作库,也就是所谓的第三方库。
iOS/MacOS 中的库
在苹果系统中,静态库使用.a后缀名,动态库使用.dylib或.tbd后缀名。
静态库与静态链接:

动态库与动态链接:

静态库示例
创建Person.h:
#import <Foundation/Foundation.h>
@interface Person : NSObject
-(void)greet;
@end
创建Person.m:
#import "Person.h"
@implementation Person
-(void)greet{
NSLog(@"Hello");
}
@end
创建main.m:
#import "Person.h"
int main(){
Person *p = [[Person alloc]init];
[p greet];
return 0;
}
首先编译生成静态库:
clang -target x86_64-apple-darwin20.3.0 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -c Person.m -o Person.o
打包静态库,执行以下命令后生成libPerson.a:
ar rs libPerson.a Person.o
链接静态库,生成可执行文件:
clang -target x86_64-apple-darwin20.3.0 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I. -L. -lPerson main.m -o test
运行可执行文件测试:
./test
clang编译参数:
-x:指定编译语言的类型,如:-x objective-c-isysroot:设置使用的SDK路径-F<directory>:设置搜索framework的路径-framework <framework_name>:指定链接的framework名称,例如:-framework AFNetworking
动态库示例
仍以上面代码为例,使用命令编译生成动态库:
clang -target x86_64-apple-darwin20.3.0 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I. -shared -fPIC Person.m -o libPerson.dylib
依赖动态库生成 可执行文件:
clang -target x86_64-apple-darwin20.3.0 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I. -L. -lPerson main.m -o test
注意,clang还有另一个参数-dynamiclib可以用来替代-shared生成动态库。关于创建动态库,可查看官方文档: Creating Dynamic Libraries
结合xcrun工具:
它是 Xcode 的命令行工具。可以通过执行xcode-select --install命令进行安装。安装完成后可以执行xcrun -h查看帮助文档。
常用的一些参数组合
xcrun --sdk iphoneos --find clang # 查看目标编译器的路径
xcrun --sdk iphoneos --show-sdk-path # 查看指定SDK的路径
xcrun --sdk macosx --show-sdk-path
使用Xcode
MacOS示例
- 创建一个库工程


在 Type 选项中设置是动态库还是静态库
- 工程创建完成后,添加库代码。注意,默认生成的库名称为
lib+工程名。我们可以设置生成的库名称,在【Build Settings】中搜索packaging

Executable Prefix选项为库名前缀,这个最好不要修改,按照规范来。修改Product Name项的值,这就是我们希望生成的库名称。
导出头文件。由于库是一个二进制文件,使用者无法知道该库包含哪些类和方法,因此必须配一个头文件,告知使用者有哪些类和方法可以用。在【Build Phases】中按以下步骤导出头文件

点击1步骤中的
+创建一个 Copy Files 项(如果没有),点击3步骤中的+选择要导出的头文件。使用CMD + B 快捷键后生成了一个动态库libPerson.dylib,在同目录下还包含一个头文件Person.h
使用生成的动态库:
首先创建一个普通的MacOS 命令行工程,调用库中的类。这里以前面的main.m作为测试示例。
接下来有两种方式依赖库,第一种最简单,直接将生成的库与头文件拖到工程目录下即可使用。另一种则需要添加依赖配置,但是这种方式可以帮助我们理解Xcode编译构建过程。
设置头文件搜索路径。这一步就对应于我们命令编译时的
-I参数:
这里有两个选项可以配置,分别是Header Search Paths和User Header Search Paths,他们的区别在于你包含头文件的方式,前者使用尖括号包含头文件,后者使用双引号包含。
设置库搜索路径。这一步对应命令行编译参数
-L:
链接所依赖的库。这一步对应命令行编译参数
-l:
如图,搜索Other Linker Flags,然后添加
-lPerson,指定链接的库名称,与命令行编译一样。
iOS示例
在iOS开发中,我们可以像MacOS一样去使用静态库,但却无法使用真正的动态库。

可以看到,在iOS标签下,只能创建静态库工程。
在 iOS 8 之前,苹果不支持App开发者在iOS上使用动态库,开发者要进行模块化,只能是打包成静态库.a 文件,同时附上头文件。但是这种方式打包不够方便,使用时也比较麻烦,没有Framework的便捷性。但是这时候的Framework也只支持打包成静态Framework。
在iOS开发中制作静态库时,需要注意目标CPU架构。如下图,找到Build Active Architecture Only选项:

首先要区分Debug版和Release版,Debug的该选项默认为YES,这表示仅生成当前活跃的目标CPU架构。所谓活跃的,就是黄色矩形框中的设置项,上图中的是arm64,表明生成的库只包含arm64指令集。如果该选项设置为No,并且黄色框中选择任意一个模拟器,则编译会生成一个胖版的库文件。所谓胖版库文件,就是包含多种指令集的库。我们可以使用lipo -info 命令来查看一个二进制库文件支持的CPU架构。也可以使用lipo -create libx.a libxxx.a -output 目标架构.a命令手动合并一个胖版的库。

注意,我们可以在【Build Settings】下的Architectures中指定目标CPU架构。
iOS版本与CPU架构表:

注意:iPhone4s是2011年10月4日发布的,距今已经十年了,现在的开发者完全不用考虑对其兼容。因此,发布的CPU架构中无需支持armv7,包括armv7s也无需支持。
Framework
Framework是指 Cocoa/Cocoa Touch 程序中使用的一种包,实际上就是一个有着特定结构的文件夹。它可以将源码文件、头文件、资源文件(nib/xib、图片、本地化文本)、库文件等集中打包在一起,方便开发者使用。因此,如果有人把Framework称作库,那么此时指的是广义的库。
iOS 8/Xcode 6 推出之后,添加了对动态库的支持,Xcode 6 支持动态 Framework。但是提供给开发者使用的动态库并不是真正的动态库。并且动态 Framework 也和系统的 UIKit.Framework 有很大区别。系统的 Framework 不需要拷贝到目标程序中,是真正的动态链接,而开发者打包的动态 Framework,还是要拷贝到 App 中,因此苹果又把这种 Framework 称为 Embedded Framework。
作为开发者,我们有两种方式来制作Framework,一种是直接打包已经编译好的二进制,另一种是通过源文件来创建Framework。
注意,Framework中可以打包静态库,也可以打包动态库,因此可以分为动态Framework和静态Framework。

静态Framework
手撸一个Framework
我们已经知道了Framework就是一个特定结构的文件夹,那么完全可以手撸一个来验证。
创建文件夹MyExample.framework,cd进去再创建Headers文件夹。这里仍然以前面的Person.h、Person.m作为示例,先编译出一个静态库:
# 编译生成.o目标文件
clang -arch x86_64 -mios-simulator-version-min=10.0 -isysroot "$(xcrun -sdk iphonesimulator --show-sdk-path)" -fobjc-arc -I. -c Person.m -o Person.o
# 将目标文件打包为.a静态库
ar rs libPerson.a Person.o
# 合并生成Framework的同名二进制文件
lipo -create libPerson.a -output MyExample
现在,我们将生成的Framework同名二进制MyExample拷贝到之前创建好的文件夹中,同时将需要暴露的头文件拷贝至Headers文件夹:

这样,我们就得到了一个Framework包,将MyExample.framework这个包拖到Xcode工程中,就可以使用它了。
Xcode 创建Framework

如图,选择Framework模板创建Framework工程。然后我们可以把一个.a静态库以及头文件直接拖到工程中,CMD + B编译后就会生成静态Framework。但是需要注意设置Framework的类型,Dynamic 为动态,Static为静态,默认是动态类型的。

另外还有注意头文件的导出,Public中添加需要暴露的头文件,Project中为私有的头文件。

当然,如果我们有源码,可以直接将源码添加到工程中,从源码编译生成静态Framework。
打包动态库创建动态Framework
一般来说,都是直接利用源代码来创建动态Framework,但实际上也可以把一个编译好的动态库制作成动态Framework。问题在于,Xcode中并没有创建iOS动态库的模版选项。
这时候,有两种解决方法:
在Xcode中选择MacOS平台,创建一个适用MacOS的动态库工程,但是需要修改该工程的一些配置项
将
Base SDK由 macOS 改为 iOS
Code Signing Identity由 Apple Development 改为 iOS Developer
添加Development Team

如果最后要将该动态库打包为动态Framework,那么还需要设置
@rpath,否则在运行时动态库无法加载
不用Xcode,直接用命令行编译一个动态库
clang -arch x86_64 -mios-simulator-version-min=10.0 -isysroot "$(xcrun -sdk iphonesimulator --show-sdk-path)" -shared -fPIC -fobjc-arc -I. Person.m -o libPerson.dylib
iOS工具链是根据SDK区分开的,分为iphonesimulator、iphoneos、macos和tvos。对于每个SDK,你都需要进行一次编译。你可以把iphonesimulator和iphoneos的输出合并成一个Fat库,但这会增加打包大小。
上述命令中的-arch用于指定目标CPU的架构,-mios-simulator-version-min是设置运行设备的最低系统版本,此处为模拟器设备,如果是真机,则指定-miphoneos-version-min
注意,得到动态库后,直接拖入Framework中,然后打包出来的动态Framework包是无法使用的,我们需要将生成的Framework包中的二进制进行替换。
另外,在使用动态Framework时,还有非常重要的一点,就是需要把Framework打包到APP中,添加Embed配置:

动态Framework的缺陷
在实际iOS开发中,并不建议使用动态库或者动态Framework。我们知道,动态库最大用处有两点,第一是可以在运行时通过dlopen动态加载,从而实现热更新;第二点是可以在多个程序间共享代码,减小打包体积。然而在iOS上这两点都无法做到,几乎与使用静态库没有太大区别。
iOS上的dlopen是修改过的,增加了签名验证,当App从服务器远程下载动态库加载时,直接会被拒绝。因为我们的App上传应用商店的时候会重新进行签名,而我们服务器的动态库是没有经过这个签名的。另外,iOS上实行沙盒环境,每个App都在一个沙盒中,数据无法共享,所以我们打包的动态库只能自己用,无法实现共享。

既然如此,苹果还在iOS上提供动态Framework机制有什么用呢?
在iOS8之前苹果不允许第三方框架使用动态库实现,但是iOS8之后添加了App Extesion,Extesion 需要与主App共享代码,且后来加入的Swift语言机制也需要动态库,于是苹果提出了Embedded Framework概念,这种动态库允许App和App Extension共享代码,但是这份动态库的生命被限定在一个App进程内。简单点可以理解为被阉割的动态库。
当然,如果你的App不走应用商店上架,而是用企业证书,那么动态库就能自己签名,实现动态加载了。
总结
库与Framework的区别
- 库分为动态库与静态库,Framework也分为动态Framework和静态Framework
- 库是一个纯二进制文件,是编译后生成的指令
- Framework是一个可以包含二进制产物、源码和资源的文件夹:
.a + .h + sourceFile = .framework - 使用Framework的优势在于,可以更方便的管理头文件和一些依赖的资源文件,以及库的版本。
一些工具
file:该命令可以用来查看一个Framework是动态的还是静态的lipo:加-info参数查看二进制的目标架构,也可以加-create参数合并多种架构的库,生成一个fat库lipo -create libAsimulator.a libBdevice.a -output ./ABLibotool:加-l参数,可以查看二进制的依赖,即@rpath、@executable_path、@loader_path路径。关于这几个路径可查看这里。其中关注的重点是@rpath,它是run path的缩写,对应于Xcode配置中的Runpath Search Paths。它是一个或者多个路径的列表,类似于系统环境变量$PATH。当我们遇到dyld: Library not loaded这种错误时,可以通过设置@rpath解决。install_name_tool:可以用来修改二进制的依赖install_name_tool -add_rpath /usr/local/lib/. a.out install_name_tool -change @rpath/mylib.dylib @rpath/framework/mylib.dylib XXLib install_name_tool -id @rpath/my/path mylib该命令的使用公式:
install_name_tool [-change old new] ... [-rpath old new] ... [-add_rpath new] ... [-delete_rpath old] ... [-id path] inputnm:用于查看二进制中的符号表。OC 的链接器并不会为每个方法建立符号表,而是仅仅为类建立符号表。这样的话,如果静态库中定义了已存在的一个类的分类,链接器就会以为这个类已经存在,不会把分类和核心类的代码合起来。这样的话,在最后的可执行文件中,就会缺少分类里的代码,发生找不到符号的错误。
当静态库中同时存在类和分类时,可以在【Build Settings】->【Linking 】->【Other Linker Flags】中添加参数
-ObjC解决问题,但如果静态库中只有分类而没有对应的类时, 添加-ObjC无效,这时候可以添加-all_load或者-force_load。-all_load 会让链接器把所有找到的目标文件都拷贝到可执行文件中,因此,你若使用了不止一个静态库文件,添加该参数很可能会遇到 ld: duplicate symbol 错误,因为不同的库文件里面可能会存在相同的目标文件,所以一般推荐使用 -force_load 参数。
-force_load 所做的事情跟 -all_load 一样,但该参数需要指定依赖的库文件路径,如此,你只是完全加载了一个库文件,不会影响其它库文件。
-force_load ./ALib.a
公众号“编程之路从0到1”
