静态库、Framework 的链接与合并

常见的库文件格式

.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 并用到了里面的一个方法,这样就构成了一个场景,我们的主工程用到了一个第三方库。

在看一下这个目录


截屏2021-03-06 上午11.28.25.png
  • 这里可以看到我们引用的是一个AFNetworking的静态库代码(因为我们看到了.a)
  • 还有它的头文件

file 命令去查看一下它具体的格式


截屏2021-03-06 上午11.33.57.png

lipo -info 可以去查看它支持的一个架构


截屏2021-03-06 上午11.36.26.png

此时 就给我们链接一个静态库需要的所有信息。

那这个静态库.a 到底是什么?是什么格式?
上面 通过 file 命令我们知道它是一个 archive 文档格式的,
我们还知道它是一个.o 文件的合集 那么我们可以通过 ar命令 来查看一下


截屏2021-03-06 上午11.48.11.png
  • 由此 可以验证出 .a 它就是一个.o文件的合集

正式链接

上边我们已经准备好调节,下面我们就正式的将我们的.m与AFN进行一个链接

  • 首先我们知道 编译的一个过程是将.m 生成目标文件.o 之后再进行一个链接.最后生成可执行文件,或者 动态库。

1、将test .m文件编译为目标文件
这里就需要借助我们平常熟悉的东西 Clang 大家都知道,clang是前端编译器。那么我们看一下它的自描述


截屏2021-03-06 上午11.57.21.png
  • 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文件


截屏2021-03-06 下午12.53.03.png
  • 在上篇文章我们有讲过.o 里边有重定位符号表,重定位符号里保存的是 当前用到的符号. 那它的作用是什么?其实就是当我们进行链接的时候,通过这个重定位符号表再次重定位,生成具体的一个符号信息。
  • 这也就是为什么我们生成目标文件只需要一个头文件的地址就可以了,因为再生成目标文件的时候,只需要告诉clang 哪个地方需要进行重定位就好了。
  1. 进行链接 静态库 生成可执行文件
    上面我们已经完成了目标文件的生成,下面我们通过连接器生成可执行文件,同样的我们知道我们的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

屏幕快照 2021-03-10 下午8.35.26.png
  • ar——创建和维护library 文档格式。
  • ar实用程序创建并维护合并到存档中的文件组。一旦创建了存档,就可以添加和删除新文件
    可以提取、删除或替换现有文件。

随意找两个.a 我们进行一个合并 下面我就搞俩.a


屏幕快照 2021-03-10 下午8.48.25.png

首先查看一下 下面两个.a 中的内容


屏幕快照 2021-03-10 下午8.52.15.png
  • 由此可见的确是.o的一个合集

下面我们将两个 .a进行一个合并 这时我们就又用到一个命令 libtool
同样的看一下自描述


屏幕快照 2021-03-10 下午9.04.55.png
  • 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中各自打包、签名、加载一份。

屏幕快照 2021-03-10 下午9.45.32.png
  • 从上面的描述想必大家也 明白了Framework其实就是 即将库本身的格式还有头文件 签名 以资源文件 放在了一起。

手动创建Framework

先看一下 AFNetworking的franmework的目录


屏幕快照 2021-03-10 下午10.17.47.png

我们按照它的格式手动来创建一个

1、我们以 A_libTest 静态库为例 在我们项目中 来链接它 并执行。


屏幕快照 2021-03-10 下午10.05.37.png

2、新建文件夹并改为A_LibTest .framework 将上面编译出来的libA_LibTest.a 放进 A_LibTest .framework 中 并新建立 heards 文件夹 将头文件 扔进去,按照Framework的格式我们将lib 还有.a 都去掉


屏幕快照 2021-03-10 下午10.23.01.png
屏幕快照 2021-03-10 下午10.21.33.png

屏幕快照 2021-03-10 下午10.33.57.png
  • 通过👆步骤 我们已经创建好了一个Framework 下面我们进行连接看一下

看一下准备的test.m 它引入了 Framework中的一个头文件A_Manager.h 并且main函数里使用了 此类的一个方法 -(void)A_Manager_TestFunction

屏幕快照 2021-03-10 下午10.32.46.png

屏幕快照 2021-03-10 下午10.33.14.png

下面我们来进行编译一下

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
屏幕快照 2021-03-10 下午10.44.18.png
  • 可以看到被我们编译为了.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


    屏幕快照 2021-03-10 下午11.15.37.png
  • 可以看到我们已经链接成功并成功的输出了可执行文件

下面我们运行一下看能否调用成功

屏幕快照 2021-03-10 下午11.20.04.png

重要

通过上面的操作我们知道了:链接成功一个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 脚本来 操作

环境准备
屏幕快照 2021-03-18 下午9.51.56.png
屏幕快照 2021-03-18 下午9.52.30.png
屏幕快照 2021-03-18 下午9.53.55.png

开始编写脚本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


屏幕快照 2021-03-18 下午10.11.10.png
屏幕快照 2021-03-18 下午10.12.34.png

此时我们可以看到 每一步的操作用脚本来实现是如此简单

如遇到:zsh: permission denied: ./build.sh
解决: chomd +x ./build.sh

运行test


屏幕快照 2021-03-18 下午10.16.19.png

成功被打印

dead_strip

通过如下命令我们查看一下 上面👆我们编译出来的 mach-O 看它的__Text section

objdump --macho -d test 
屏幕快照 2021-03-18 下午11.06.15.png
  • 可以看到包含静态库的符号 在连接的时候会把静态库的符号放在一起。保存在可执行文件中。

我们将main函数里使用到的方法去掉


屏幕快照 2021-03-18 下午11.09.36.png
  • 这里不妨停几秒想一下 我们引入了头文件 但是并没有使用它里面的任何代码,那么生成的可执行文件test有没有包含 A_Manager.m中的代码?

运行我们的脚本 ./build.sh 再次查看 mach-O


屏幕快照 2021-03-18 下午11.12.00.png
  • 可以看到只有一个main 函数 这其实就是编译器默认的将没有用到的符号给我们脱掉了。

这里我们想一个点 分类是在运行时创建的还是 在编译时创建的?肯定时运行时了。
所以分类会出现问题?

通过例子来展示

通过 workspace 来管理 两个 project 一个是主项目 一个是 我们的静态库Framework


屏幕快照 2021-03-19 上午12.06.57.png
  • 主工程调用了静态库A 的一个 A_Test方法


    屏幕快照 2021-03-19 上午12.07.40.png
  • 静态库 A_Test方法调用了 分类 A+Test 的方法 testCategoryMethod方法


    屏幕快照 2021-03-19 上午12.08.10.png
  • A+Test 的方法打印

此时不妨想一想 会调用成功吗?


屏幕快照 2021-03-19 上午12.13.36.png
  • 不成功又为啥?

这就是因为由于分类是运行时加载的,所以 编译器默认将它符合进行了死代码剥离。

如何解决

-all_load : 全部加载不要给我脱
-ObjC : 只保留OC的其他的该脱脱
-force_load:指定 哪些静态库你需要脱

dead strip 两种方案的区别

方案一

-noall_load /-all_load /-ObjC /-force_load<file> :在链接静态库的时候进行死代码删除。

方案二

linking
dead Code Stripping : xxx ,它是链接器给我们提供了一种优化方式。

通过实操我们直观的看一下他们的区别
沿用上面代码

屏幕快照 2021-03-22 下午10.47.38.png
  • 写一个全局符号 并在main函数中调用
  • 还写了一个本地符号
  • 通过上篇文章我们写的脚本编译一下


    屏幕快照 2021-03-22 下午10.51.50.png
  • 此时我们想一下 我编译出来的可执行文件有没有包含 libA_Manager.a这个静态库的代码?
    没有?为什么?因为我们没有使用静态库的代码所以它默认是-noall_load
    我们来查看一下 我们编译出来的 test mach-O
obdump --macho -d test 
屏幕快照 2021-03-22 下午10.59.37.png
  • 可以看到 并没有 静态库的代码

下面我们修改.sh 给连接器传入 -all_load


屏幕快照 2021-03-22 下午11.03.20.png
  • 再次编译并运行

再次查看我们的 test mach-O


屏幕快照 2021-03-22 下午11.05.58.png
  • 此时发现我们没有用到的静态库的函数也被存在了mach-o中,所以说方案一的参数设定对静态库来说是非常有用的。

下面我们看一下 ld的优化方案 dead strip

屏幕快照 2021-03-22 下午11.14.29.png
  • 翻译过来就是 移除代码或方法,没有被入口点或导出符号用到的。就会被删除

我们再回过头来看一下我们的test.m

屏幕快照 2021-03-22 下午11.17.38.png

我们在.sh 中拼接上 -dead_strip 再次编译


屏幕快照 2021-03-22 下午11.19.50.png

在此查看一下符号表


屏幕快照 2021-03-22 下午11.26.30.png
  • 此时我们看到 明明 test.m我写了一个全局符号,但是我并没有用到,此时就会被干掉。

下面我们将其在main函数中使用,再次编译并查看符号表


屏幕快照 2021-03-22 下午11.29.15.png
  • 此时发现 现在它出现在了符号表中

从这里我们就可以分析出 dead_strip和我们的n-noall_load all_load 等 它并不是一个东西
它只是我们的链接器给我提供的一种优化方式,它是有一定规则的
1、没有被我们入口点使用就被干掉,
2、没有被导出符号用到就会干掉

下面我在将 all_load 添加上


屏幕快照 2021-03-22 下午11.38.42.png

编译并 再次查看符号表

屏幕快照 2021-03-22 下午11.39.44.png
  • 此时发现 除了OC的其他的全部被脱掉了 为啥?因为OC是动态语言现在给干掉它不敢啊。

拓:还可以通过-Xlinker -why_live -Xlinker _xxxx 来查看这个符号为什么没有被干掉


屏幕快照 2021-03-22 下午11.48.10.png

屏幕快照 2021-03-22 下午11.48.02.png

所以现在我们知道 dead strip 和 all_load /noall_load 是两码事。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 什么是库? 库(Library):就是⼀段编译好的⼆进制代码,加上头⽂件就可以供别⼈使⽤。 常⽤库⽂件格式:.a、...
    帅驼驼阅读 534评论 0 7
  • 常见库文件格式:.a,.dylib,.framework,.xcframework,.tdb 什么是库(Libra...
    HotPotCat阅读 1,157评论 0 3
  • 常用库文件格式 .a:静态库 .dylib:动态库 .framework:动静结合的库 .xcframework:...
    Mjs阅读 1,029评论 0 1
  • iOS 开发进阶 文章汇总[https://www.jianshu.com/p/c40b31400816] 目录 ...
    differ_iOSER阅读 1,787评论 4 16
  • 什么是库,使用库有哪些好处?库就是将代码编译成一个二进制文件,再加头文件。常见的库文件格式有.a .dylib ...
    崔希羽阅读 994评论 0 1