什么是库,使用库有哪些好处?库就是将代码编译成一个二进制文件,再加头文件。常见的库文件格式有.a
.dylib
.tbd
.framework
.xcframework
。使用库文件可以在不暴露源码的情况供别人使用,开发中将一些不常修改的代码打包成库也可以减少编译的时间。库分为静态库和动态库,在面试中经常会问二者的区别,了解它们的本质就需要亲自探索一番,本文带你一步步探索库的本质。有兴趣的同学建议动手试验一遍,然后再阅读一遍,搞不明白你找我!!
静态库的本质探索
静态库通常是以.a或.framework为后缀的库文件,先准备一个macOS静态库.a文件,我就以AFNetworking为例,通过两个命令file
与ar
来查看一下libAFNetworking.a,从终端打印描述可以看出它是一个文档格式,里面是.o文件。
接下来模拟App链接静态库的过程,使用一个.o文件来链接libAFNetworking.a
- 创建一个test.m文件,然后在test.m文件中引用AFNetworking,test.m代码:
#import <Foundation/Foundation.h>
#import <AFNetworking.h>
int main(){
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSLog(@"testApp----%@", manager);
return 0;
}
-
使用clang将test.m文件生成test.o文件,终端命令
clang -x objective-c -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I./AFNetworking -c test.m -o test.o
- -isysroot是因为使用了Foundation库,需要指定库的位置,使用
find /Applications/Xcode.app/Contents -name SDKs
可以快速进行定位到SDKs文件夹,进入文件夹再使用ls命令列出sdk名称 - -I是指定AFNetworking头文件位置,对应Xcode中的Header search path,在目标文件中有一个重定位符号表,它保存了当前文件里面使用的所有符号,在链接的时候链接器会根据重定位符号表再去重新定位查找具体的符号信息,因此在生成目标文件时我们只需要有一个头文件,能够生成重定位符号即可。
- 关于这些命令的介绍都可以通过
man clang
来查看解释,其他的就不再一一介绍
- -isysroot是因为使用了Foundation库,需要指定库的位置,使用
-
生成可执行文件,终端命令
clang -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -L./AFNetworking -lAFNetworking test.o -o test
- -L后面的是静态库的路径,相当于Xcode配置项里面的Libarary Search Path,
- -l后面是静态库的名称,这里有一个查询规则是会按lib加上-l后面的组合在一起进行查找,因此libAFNetworking.a在这里只需要-lAFNetworking即可。
终端lldb运行测试,在终端输入命令
lldb -file test
或者先lldb
后file test
,进入lldb后使用r
运行。可以看到在终端打印了我们test.m中输出的语名,并且成功打印出了引用的AFNetworking库中类创建的对象将test文件放到任意路径尝试,均可以运行打印出对象,说明最终链接会将所有的.o文件、静态库文件进行合并。
➜ Test lldb
(lldb) file test
Current executable set to '/Users/Peny/Desktop/Test/test' (x86_64).
(lldb) r
Process 78280 launched: '/Users/Peny/Desktop/Test/test' (x86_64)
2021-01-23 02:19:16.032613+0800 test[78280:5833698] testApp----<AFHTTPSessionManager: 0x10040f1c0, baseURL: (null), session: <__NSURLSessionLocal: 0x100410c70>, operationQueue: <NSOperationQueue: 0x10040fef0>{name = 'NSOperationQueue 0x10040fef0'}>
Process 78280 exited with status = 0 (0x00000000)
(lldb)
- 侧面印证:将一个.o文件当成静态库来被链接,如果能成功说明.o等效于一个静态库,具体方式是创建一个TestB.h和TestB.m并定义一个OC方法打印日志,在test.m中调用oc方法。将TestB.m通过clang转成.o然后使用ar命令将.o文件转成.a静态库格式,完整命令
ar -rc libTestB.a TestB.o
,用test.o来链接libTestB.a,最后通过lldb--> file test --> r
,看是否能够成功调用TestB中的方法(本人亲测可以打印,注意要使用MacOS.sdk,不然无法在终端调试哦~)
通过以上测试可以总结出:
- 静态库的本质就是.o文件的合集
- 要成功链接一个静态库有三要素:库名称、头文件和库文件路径。
- 静态库链接之后所有的符号都将合并在一起,源文件就没用了,好处就是能直接运行,但是也会使可执行文件变大,占用__text section空间,苹果对它目前限制500M
动态库探索
常见的动态库格式有.dylib
、.tbd
和.framework
。我们采用与静态库相同的方式去链接一个AFNetworking
的动态库libAFNetworking.dylib
探索尝试,还是两个命令,编写脚本link_dylib.sh:
echo "编译test.m生成test.o==="
clang -x objective-c -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I./AFNetworking -c test.m -o test.o
echo "链接libAFNetworking.dylib==="
clang -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -L./AFNetworking -lAFNetworking test.o -o test
echo "生成test可执行文件"
将脚本放到与AFNetworking
、test.m
同级,添加可执行权限chmod +x link_dylib.sh
,执行脚本./link_dylib.sh
,报错了???
为了排除动态库的问题,我们再使用clang
命令制作一个动态库再进行尝试,使用之前准备的TestB.h
和TestB.m
文件作成一个动态库,将这两个文件放到dylib
文件夹中,在dylib
同级目录下编写build_dylib.sh
脚本
echo "将test.m编译成test.o"
clang -x objective-c -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I./dylib -c test.m -o test.o
pushd ./dylib
echo "将TestB.m编译成TestB.o"
clang -x objective-c -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -c TestB.m -o TestB.o
echo "clang -dynamiclib编译成动态库"
clang -dynamiclib -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk TestB.o -o libTestB.dylib
popd
echo "链接libTestB.dylib生成EXEC"
clang -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -L./dylib -lTestB test.o -o test
执行脚本可以看到test
的exec文件已经生成,再次执行lldb -file test
+ r
,还是报错!!难道是动态库生成的姿势不对?嗯。。。再换一种方式,既然静态库它是一个.o
文件的合集,那是不是也可以将静态库链接成为一个动态库呢?继续折腾~
- 使用clang命令将
TestB.m
编译成TestB.o
文件 - 将
TestB.o
打包成静态库,这次我们使用xcode
官方自带的命令libtool -static
- 直接使用链接器
ld -dylib
来链接 - 生成可执行文件
步骤比较多,编写脚本build_a_dylib.sh
:
MACOS_SDK=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
echo "将test.m编译成test.o"
clang -x objective-c -fobjc-arc -isysroot $MACOS_SDK -I./dylib -c test.m -o test.o
pushd ./dylib
echo "将TestB.m编译成TestB.o"
clang -x objective-c -fobjc-arc -isysroot $MACOS_SDK -c TestB.m -o TestB.o
echo "libtool -static 将.o打包成静态库 libTestB.a"
libtool -static -arch_only x86_64 TestB.o -o libTestB.a
echo "ld -dylib 生成动态库 libTestB.dylib"
ld -dylib -arch x86_64 \
-macosx_version_min 10.15 \
-syslibroot $MACOS_SDK \
-lsystem \
-framework Foundation \
libTestB.a -o libTestB.dylib
popd
echo "链接libTestB.dylib生成EXEC"
clang -fobjc-arc -isysroot $MACOS_SDK -L./dylib -lTestB test.o -o test
添加执行权限chmod +x build_a_dylib.sh
,运行,再次报错!!!
找不到符号
TestB
,但是动态库已经生成了,说明动态库dylib
中没有这个导出符号,可以通过查看导出符号的命令进行验证objdump --macho --exports-trie ./dylib/libTestB.dylib
,那么肯定是我们链接时出现了什么问题?使用man ld
查看链接器,可以看到其中有几个命令:
-all_load Loads all members of static archive libraries.
-ObjC Loads all members of static archive libraries that implement an Objective-C class or category.
-force_load path_to_archive
Loads all members of the specified static archive library. Note: -all_load forces all members of all archives to be loaded. This option allows you to target a specific archive.
-noall_load This is the default. This option is obsolete.
-noall_load
为链接器的一个默认值,猜测当我们从静态库生成动态库的时候,它的符号因为没有被外部使用而被脱掉了才出现上面的错误信息,修改ld
命令添加上-all_load
或者-ObjC
来再试一下,果然完美运行,再次使用objdump查看dylib的导出符号,赫然在列!再来lldb
运行一下吧 。(用脚指头想也知道还是会报错。。。我就试一下~)
果不其然,依然是
Libarary not loaded
and image not found
,实在是烦!!!不过也并非一无所获,至少可以看出动态库与静态库的区别,静态库只是一个.o
文件的集合,而动态库它比静态库多了一个链接的步骤,它是一个最终链接产物,也就是说动态库它不能再进行合并。
探索到了这里,显然还没有结束,明明已经编译链接成功,为什么在运行时会出现这个错误?接下来我们借助一个工具MachOView
来查看可执行文件test
,能报这个库找不到说明Load Commands中肯定是有这个LC_LOAD_DYLIB
,它是不是有什么问题呢?与我们使用cocoapods
引入的动态库对比一下看看
我好像发现了点什么。。。正常运行的项目里面引入的动态库指向的路径为
@rpath/AFNetworking.framework/AFNetworking
,@rpath
是什么暂且不管,后面的路径看起来就是动态库的所有路径啊,第六感告诉我运行报错很可能是因为libTestB.dylib
的路径有问题,查看我们的test
文件目录可以发现我们的test
文件与libTestB.dylib
并不在一个层级大胆推测一下,如果将
libTestB.dylib
文件与test
文件放在同一层级是否就能成功运行,怀着激动的心情将libTestB.dylib
文件放到同一目录下,然后lldb -file test
,在lldb下运行天呐,运行成功了!!!我就说嘛,哈哈,我的第六感是很准的~,也就是说我们的动态库在链接成功之后,其符号仍然保存在库中,在运行时可执行文件会按照LC_LOAD_DYLIB
中的路径去查找动态库中的符号。所以在我们开发中如果再遇到某某动态库Libarary not loaded
时,十有八九就是路径的问题了。
终于搞清楚了之后还要解决引用路径的问题,这里不再码字说明,仅给出研究方向,研究@rpath
、@executable_path
、@load_path
这三者究竟是如何使用的,对于这三者理解之后可以帮助我们解决一些动态库的链接依赖问题,这里给出一些简单的介绍,有兴趣的同学可以自行探索。
-
@rpath
比较灵活,它是一个变量,一个占位,谁使用谁提供它的值,链接器可以通过install_name_tool
的-add_rpath
设置值,可以有多个值 -
@executable_path
代表可以执行程序所在的路径。 -
@loader_path
表示加载它的Mach-O
所在的目录,由它的上层来决定,用它可以解决动态库依赖动态库的复杂场景。
总结动态库的特点:
- 动态库与静态库相比多了一步链接,它是链接的最终产物,动态库不能再次进行合并
- 动态库在编译链接之后并没有将所有符号合并,在运行时会根据
LC_LOAD_DYLIB
中的路径去动态加载
再次建议:要想搞清楚原理一定要亲自动手尝试,不要轻信他人给出的结论,欢迎来怼!