iOS静态库和动态库

什么是库,使用库有哪些好处?库就是将代码编译成一个二进制文件,再加头文件。常见的库文件格式有.a .dylib .tbd .framework .xcframework。使用库文件可以在不暴露源码的情况供别人使用,开发中将一些不常修改的代码打包成库也可以减少编译的时间。库分为静态库和动态库,在面试中经常会问二者的区别,了解它们的本质就需要亲自探索一番,本文带你一步步探索库的本质。有兴趣的同学建议动手试验一遍,然后再阅读一遍,搞不明白你找我!!

静态库的本质探索

静态库通常是以.a或.framework为后缀的库文件,先准备一个macOS静态库.a文件,我就以AFNetworking为例,通过两个命令filear来查看一下libAFNetworking.a,从终端打印描述可以看出它是一个文档格式,里面是.o文件。

iOS静态库和动态库_第1张图片
image.png

接下来模拟App链接静态库的过程,使用一个.o文件来链接libAFNetworking.a

  • 创建一个test.m文件,然后在test.m文件中引用AFNetworking,test.m代码:
#import 
#import 

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来查看解释,其他的就不再一一介绍
  • 生成可执行文件,终端命令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 或者先lldbfile 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----, operationQueue: {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可执行文件"

将脚本放到与AFNetworkingtest.m同级,添加可执行权限chmod +x link_dylib.sh,执行脚本./link_dylib.sh,报错了???

iOS静态库和动态库_第2张图片
image.png

为了排除动态库的问题,我们再使用clang命令制作一个动态库再进行尝试,使用之前准备的TestB.hTestB.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,运行,再次报错!!!

iOS静态库和动态库_第3张图片
image.png

找不到符号 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运行一下吧 。(用脚指头想也知道还是会报错。。。我就试一下~)

iOS静态库和动态库_第4张图片
image.png

果不其然,依然是 Libarary not loaded and image not found,实在是烦!!!不过也并非一无所获,至少可以看出 动态库与静态库的区别,静态库只是一个.o文件的集合,而动态库它比静态库多了一个链接的步骤,它是一个最终链接产物,也就是说动态库它不能再进行合并

探索到了这里,显然还没有结束,明明已经编译链接成功,为什么在运行时会出现这个错误?接下来我们借助一个工具MachOView来查看可执行文件test,能报这个库找不到说明Load Commands中肯定是有这个LC_LOAD_DYLIB,它是不是有什么问题呢?与我们使用cocoapods引入的动态库对比一下看看

iOS静态库和动态库_第5张图片
image.png

iOS静态库和动态库_第6张图片
image.png

我好像发现了点什么。。。正常运行的项目里面引入的动态库指向的路径为 @rpath/AFNetworking.framework/AFNetworking@rpath是什么暂且不管,后面的路径看起来就是动态库的所有路径啊,第六感告诉我运行报错很可能是因为 libTestB.dylib的路径有问题,查看我们的 test文件目录可以发现我们的 test文件与 libTestB.dylib并不在一个层级
iOS静态库和动态库_第7张图片
image.png

大胆推测一下,如果将 libTestB.dylib文件与 test文件放在同一层级是否就能成功运行,怀着激动的心情将 libTestB.dylib文件放到同一目录下,然后 lldb -file test,在lldb下运行
iOS静态库和动态库_第8张图片
image.png

天呐,运行成功了!!!我就说嘛,哈哈,我的第六感是很准的~,也就是说我们的动态库在链接成功之后,其符号仍然保存在库中,在运行时可执行文件会按照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中的路径去动态加载

再次建议:要想搞清楚原理一定要亲自动手尝试,不要轻信他人给出的结论,欢迎来怼!

你可能感兴趣的:(iOS静态库和动态库)