常见的库文件格式
.a : 静态库
.dylib : 传统意义动态库
.framework : 可动可静
.xcframework: 针对不同架构的搞一起。
xcframework,18年苹果出的新的库的格式,我们平常开发中对胖库各种架构都支持的sdk ,上线的时候需要把比如模拟器的架构剔除,为了节省空间,可是当想用模拟器运行的时候又会发现此架构被剔除了,所以这种库的出现会省去此操作,想要链接什么架构就链接什么架构。
这篇文章主要介绍 .a 和.framework
什么时候会用到库
1、某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。
2、对于某些不会进行大改动的代码,我们想减少编译时间,就可以把它打包成库,因为库是已经编译好的二进制,编译的时候只需要link一下,不会浪费编译时间。
操作准备
先看一份代码
#import <Foundation/Foundation.h>
#import <AFNetworking.h>
int main(){
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSLog(@"testApp----%@", manager);
return 0;
}
- 上面代码就是一个简单的test.m文件。
- 可以看到 此代码引用了 AFNetworking 并用到了里面的一个方法,这样就构成了一个场景,我们的主工程用到了一个第三方库。
在看一下这个目录
- 这里可以看到我们引用的是一个AFNetworking的静态库代码(因为我们看到了.a)
- 还有它的头文件
file 命令去查看一下它具体的格式
lipo -info 可以去查看它支持的一个架构
此时 就给我们链接一个静态库需要的所有信息。
那这个静态库.a 到底是什么?是什么格式?
上面 通过 file 命令我们知道它是一个 archive 文档格式的,
我们还知道它是一个.o 文件的合集 那么我们可以通过 ar命令 来查看一下
- 由此 可以验证出 .a 它就是一个.o文件的合集
正式链接
上边我们已经准备好调节,下面我们就正式的将我们的.m与AFN进行一个链接
- 首先我们知道 编译的一个过程是将.m 生成目标文件.o 之后再进行一个链接.最后生成可执行文件,或者 动态库。
1、将test .m文件编译为目标文件
这里就需要借助我们平常熟悉的东西 Clang 大家都知道,clang是前端编译器。那么我们看一下它的自描述
- clang 是一个 c 、c++ 和 oc 的编译器
- clang是一个C, c++和Objective-C编译器,包含了 预
处理、解析、优化、代码生成、汇编和链接。
根据传入的高级模式设置,Clang将停止
在做一个完整的链接之前 - clang可执行文件实际上是一个小的驱动程序,它控制其他程序的整体执行
诸如编译器、汇编器和链接器等工具。通常,您不需要与
驱动程序,但您可以使用它来运行其他工具。
2.开始编译为.o
clang -x objective-c \
-target x86_64-apple-macos11.0\
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-I ./AFNetworking \
-c test.m -o test.o
- clang -x :来指定编译的语言.
- -target : 指定编译的架构
- -fobjc-arc : 指定为 arc环境
- -isysroot : 这就是用到的系统库所在的位置 (例如我们.m 里面引用了<Foundation/Foundation.h>)
- -I :指定里面引用的第三方库头文件的路径 header serach path
- -c :输出
- \ 换行输入 为了好看
通过上面一系列操作 遍输出了.o文件
- 在上篇文章我们有讲过.o 里边有重定位符号表,重定位符号里保存的是 当前用到的符号. 那它的作用是什么?其实就是当我们进行链接的时候,通过这个重定位符号表再次重定位,生成具体的一个符号信息。
- 这也就是为什么我们生成目标文件只需要一个头文件的地址就可以了,因为再生成目标文件的时候,只需要告诉clang 哪个地方需要进行重定位就好了。
- 进行链接 静态库 生成可执行文件
上面我们已经完成了目标文件的生成,下面我们通过连接器生成可执行文件,同样的我们知道我们的clang也是包含我们链接器的一个接口的。
$ clang -target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-L./AFNetworking \
-lAFNetworking \
test.o -o test
ld: in ./AFNetworking/libAFNetworking.a(AFHTTPSessionManager.o), building for macOS, but linking in object file built for iOS Simulator, for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
- -target :指定架构
- -fobjc-arc:指定 为arc
- -isysroot : 系统库所在的位置 如NSLog
- -L :第三方静态库的位置(这里是libAFNetworking.a的地址,向我们xcode Library Search Paths)
链接的过程就需要把重定位符号表的符号进行重定位,也就是说需要符号的真实的地址,向我们用到的AFHTTPSessionManager 它真实的地址在哪? 是不是保存在静态库.a 里面的.o文件里去了,也就是在.a的重定位符号表里, 我们生成的.o也有重定位符号表,当生成可执行文件的时候会将其融合生成一个完整的符号表。因为在可执行文件中只有一个符号表。
- -l:上面我们告诉了连接器我的静态库放在哪里那接下来就需要告诉它我连接哪个库文件(注意:这里说的是库文件并没有说静态库) 这里是提供了一个 -l 参数 这里我们写-lAFNetworking
那这里为什么这么写-lAFNetworking ?因为它是有一个查找规则的,它会先查找lib+<library_name>的动态库,找不到再去找lib+<library_name>的静态库 ,在找不到就会报错。
- test.o -o test : 输入为可执行文件。
上面为啥报错 因为是AFNetworking 架构的问题,它是模拟器架构,我们这是电脑环境。
重要
通过上面的操作我们知道了:链接成功一个库的三要素:
1、-l<directory> (大i)指定目录寻找头文件
xcode: Header Search Paths
2、-L<dir> 指定库文件路径
xcode: :Library Search Paths
3、-l<library_name>(小L)指定链接库文件名称(.a.dylib库文件)
xcode: Other link flags 配置的 如-lAFNetworking
静态库的合并
我们知道静态库是.o文件的合集,那么我们是不是可以将多个.a文件合并为一个.a当然是可以的
看一下
可以通过 ar命令 来进行 操作
ar
- ar——创建和维护library 文档格式。
- ar实用程序创建并维护合并到存档中的文件组。一旦创建了存档,就可以添加和删除新文件
可以提取、删除或替换现有文件。
随意找两个.a 我们进行一个合并 下面我就搞俩.a
首先查看一下 下面两个.a 中的内容
- 由此可见的确是.o的一个合集
下面我们将两个 .a进行一个合并 这时我们就又用到一个命令 libtool
同样的看一下自描述
libtool-创建库
ranlib-添加或更新存档库的目录libtool命令获取指定的输入对象文件,并创建一个库,用于链接编辑器ld(1)。库的名称由output指定(指向-o标志的参数)。输入对象文件可以是包含对象文件(“通用”文件、归档文件、对象文件)的任何正确格式。Libtool不会将任何非对象输入文件放入输出库(与ranlib不同,ranlib允许在其操作的归档文件中进行此操作)。
当从相同CPU类型和不同CPU子类型的对象生成“通用”文件时,libtool和ranlib最多为每个CPU类型创建一个库,而不是在通用文件中为每个CPU类型和CPU子类型的唯一配对创建一个单独的库。因此,每个库的结果CPU子类型是该CPU类型的\u ALL CPU子类型。此策略强烈鼓励库的实现者创建一个库,该库选择在运行时而不是在链接时运行的最佳代码。Libtool可以创建动态链接的共享库(使用动态),也可以创建静态链接(存档)库(使用-static)。
好了知道大概描述了我们开始实操
➜ 静态库合并 libtool \
-static \
-o \
libABTest.a \
libA_LibTest.a \
libB_LibTest.a
查看合并的 libABTest.a 的.o合集
➜ 静态库合并 ar -t libABTest.a
__.SYMDEF SORTED
A_LibTest.o
A_Manager.o
A_Tool.o
B_Manager.o
B_LibTest.o
B_Tool.o
- 此时可以看到 两个库的.o文件 被合并在了一起。
- 此时库.o已经合并 那是不是就是一个处理头文件的问题了?
Framework
Mac OS/iOS平台还可以使用Framework. Framework实际上是一种打包方式,将库的二进制文件,头文件和有关的资源打包在一起,方便管理和分发。
Framework 和系统的UIKit.Framework 还是有很大的区别。系统的Framework 不需要拷贝到目标程序中,我们自己做出来的Framework 哪怕是动态的,最后也还是要拷贝到App中(App 和 Extension的Bundle 是共享的),因此苹果又把这种Framework称为 Embedded Framework.
Embedded Framework:
开发中使用的动态库会被放入到ipa下的framework目录下,基于沙盒运行。
不同的App使用相同的动态库,并不会只在系统中存在一份。而是会在多 个App中各自打包、签名、加载一份。
- 从上面的描述想必大家也 明白了Framework其实就是 即将库本身的格式还有头文件 签名 以资源文件 放在了一起。
手动创建Framework
先看一下 AFNetworking的franmework的目录
我们按照它的格式手动来创建一个
1、我们以 A_libTest 静态库为例 在我们项目中 来链接它 并执行。
2、新建文件夹并改为A_LibTest .framework 将上面编译出来的libA_LibTest.a 放进 A_LibTest .framework 中 并新建立 heards 文件夹 将头文件 扔进去,按照Framework的格式我们将lib 还有.a 都去掉
- 通过👆步骤 我们已经创建好了一个Framework 下面我们进行连接看一下
看一下准备的test.m 它引入了 Framework中的一个头文件A_Manager.h 并且main函数里使用了 此类的一个方法 -(void)A_Manager_TestFunction
下面我们来进行编译一下
clang -x objective-c \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-I./Frameworks/A_LibTest.framework/Headers \
-c test.m -o test.o
- 可以看到被我们编译为了.o
下面开始 链接 按照我们上面讲的三要素 1 、指定目录寻找头文件(-大i)2、指定库文件路径 -L 3、指定链接库文件名称 那么那是 .a 或者是 .dylib 下面看一下Framework的手动链接
clang -target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-F./Frameworks \
-framework A_libTest \
test.o -o test
- 首先 可以看到 前面命令是一样的
- -F 指定Framework 所在的目录 当前我们的在./Frameworks
-
-framework 指定要链接哪个Framework 当前我们链接的是 A_libTest
- 可以看到我们已经链接成功并成功的输出了可执行文件
下面我们运行一下看能否调用成功
重要
通过上面的操作我们知道了:链接成功一个Framework:
1、-l<directory> (大i)指定目录寻找头文件
xcode: Header Search Paths
2、-F<directory> 指定目录寻找framewok
xcode: :framework search path
3、-framework<framework_name> 指定链接的framework名称
xcode: Other link flags 配置的 如-framework AFNetworking
通过shell 脚本来 操作
环境准备
开始编写脚本build.sh
首先 将 test.m 编译成中间代码
echo "-----开始编译test.m"
clang -x objective-c \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-I./StaticLibrary \
-c test.m -o test.o
然后进入到StaticLibrary 中将 A_Manager 编译成中间代码
echo "-----开始进入StaticLibrary目录"
pushd ./StaticLibrary
echo "-----开始编译A_Manager.m 为目标文件"
clang -x objective-c \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-I./Frameworks/A_LibTest.framework/Headers \
-c A_Manager.m -o A_Manager.o
将 A_Manager.o 打包为.a
echo "-----开始将目标文件打包为 libA_Manager.a"
ar -rc libA_Manager.a A_Manager.o
退出当前目录 将test.o 链接 libA_Manager.a 并生成可执行文件EXE
echo "-----开始退出StaticLibrary目录"
popd
echo "-----将test.o 链接 libA_Manager.a 并生成可执行文件EXE"
clang -target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-L./StaticLibrary \
-lA_Manager \
test.o -o test
开始运行./build.sh
此时我们可以看到 每一步的操作用脚本来实现是如此简单
如遇到:zsh: permission denied: ./build.sh
解决: chomd +x ./build.sh
运行test
成功被打印
dead_strip
通过如下命令我们查看一下 上面👆我们编译出来的 mach-O 看它的__Text section
objdump --macho -d test
- 可以看到包含静态库的符号 在连接的时候会把静态库的符号放在一起。保存在可执行文件中。
我们将main函数里使用到的方法去掉
- 这里不妨停几秒想一下 我们引入了头文件 但是并没有使用它里面的任何代码,那么生成的可执行文件test有没有包含 A_Manager.m中的代码?
运行我们的脚本 ./build.sh 再次查看 mach-O
- 可以看到只有一个main 函数 这其实就是编译器默认的将没有用到的符号给我们脱掉了。
这里我们想一个点 分类是在运行时创建的还是 在编译时创建的?肯定时运行时了。
所以分类会出现问题?
通过例子来展示
通过 workspace 来管理 两个 project 一个是主项目 一个是 我们的静态库Framework
-
主工程调用了静态库A 的一个 A_Test方法
-
静态库 A_Test方法调用了 分类 A+Test 的方法 testCategoryMethod方法
- A+Test 的方法打印
此时不妨想一想 会调用成功吗?
- 不成功又为啥?
这就是因为由于分类是运行时加载的,所以 编译器默认将它符合进行了死代码剥离。
如何解决
-all_load : 全部加载不要给我脱
-ObjC : 只保留OC的其他的该脱脱
-force_load:指定 哪些静态库你需要脱
dead strip 两种方案的区别
方案一
-noall_load /-all_load /-ObjC /-force_load<file> :在链接静态库的时候进行死代码删除。
方案二
linking
dead Code Stripping : xxx ,它是链接器给我们提供了一种优化方式。
通过实操我们直观的看一下他们的区别
沿用上面代码
- 写一个全局符号 并在main函数中调用
- 还写了一个本地符号
-
通过上篇文章我们写的脚本编译一下
- 此时我们想一下 我编译出来的可执行文件有没有包含 libA_Manager.a这个静态库的代码?
没有?为什么?因为我们没有使用静态库的代码所以它默认是-noall_load
我们来查看一下 我们编译出来的 test mach-O
obdump --macho -d test
- 可以看到 并没有 静态库的代码
下面我们修改.sh 给连接器传入 -all_load
- 再次编译并运行
再次查看我们的 test mach-O
- 此时发现我们没有用到的静态库的函数也被存在了mach-o中,所以说方案一的参数设定对静态库来说是非常有用的。
下面我们看一下 ld的优化方案 dead strip
- 翻译过来就是 移除代码或方法,没有被入口点或导出符号用到的。就会被删除
我们再回过头来看一下我们的test.m
我们在.sh 中拼接上 -dead_strip 再次编译
在此查看一下符号表
- 此时我们看到 明明 test.m我写了一个全局符号,但是我并没有用到,此时就会被干掉。
下面我们将其在main函数中使用,再次编译并查看符号表
- 此时发现 现在它出现在了符号表中
从这里我们就可以分析出 dead_strip和我们的n-noall_load all_load 等 它并不是一个东西
它只是我们的链接器给我提供的一种优化方式,它是有一定规则的
1、没有被我们入口点使用就被干掉,
2、没有被导出符号用到就会干掉
下面我在将 all_load 添加上
编译并 再次查看符号表
- 此时发现 除了OC的其他的全部被脱掉了 为啥?因为OC是动态语言现在给干掉它不敢啊。
拓:还可以通过-Xlinker -why_live -Xlinker _xxxx 来查看这个符号为什么没有被干掉
所以现在我们知道 dead strip 和 all_load /noall_load 是两码事。