什么事动态库?
与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。动态库的格式有:
.framework
、.dylib
、.tbd
。缺点:会导致一些性能损失。但是可以优化,比如延迟绑定 (Lazy Binding) 技术。
.framework
和.dylib
在之前的文章里都有介绍,这里就不多做赘述。那么什么事tbd
格式呢?
tbd
:全称text-based stub libraries
,本质上就是一个YAML描述的文本文件。
它的作用是用于记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息等等。用于避免在真机开发过程中直接使用传统的dylib
。
对于真机来说,由于动态库都是在设备上的,在Xcode上使用基于tbd
格式的伪framework
可以大大减少Xcode的大小。
接下来我们一起来探索一下动态库
动态库原理
首先看一下我们的测试环境:
build
里面的指令我们在9、iOS强化 --- 静态库里面都有讲过,不同的是将TestExample.o --->TestExample.a
换成了 TestExample.o --->TestExample.dylib
:
echo "编译test.m ---> test.o"
clang -target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-I./dylib \
-c test.m -o test.o
pushd ./dylib
echo "编译TestExample.m ---> TestExample.o"
clang -target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-c TestExample.m -o TestExample.o
echo "编译TestExample.o ---> libTestExample.dylib"
# -dynamiclib: 动态库
clang -dynamiclib \
-target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
TestExample.o -o libTestExample.dylib
popd
echo "链接libTestExample.dylit --- test EXEC"
clang -target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-L./dylib \
-lTestExample \
test.o -o test
我们第一次执行脚本的时候,同样会遇到build.sh
的权限问题;同样的,我们赋予权限就可以了:
chmod +x ./build.sh
执行完脚本是这个样子的:
- 接下来我们运行一下
test
:
为什么会报这样一个错误呢?
这里我们就要弄明白动态库
到底是一个什么东西:
1、动态库
是编译链接的最终产物(是.o
文件链接后的产物)。
2、之前我们讲过静态库
是.o
文件的合集,那么静态库
就能够链接成动态库
。
(这里我们先把上面的问题记录一下,接着往下走)
我们上面是直接将.o
链接成.dylib
,上面我们也说了静态库
可以链接成动态库
。那么接下来,我们就在上面的"编译TestExample.o ---> libTestExample.dylib"
这个一步改一下,改成下面的指令:
# Xcode ---> 静态库
libtool -static -arch_only x86_64 TestExample.o -o libTestExample.a
echo "编译libTestExample.a ---> libTestExample.dylib"
# -dynamiclib: 动态库
# dylib 最终链接的产物
ld -dylib -arch x86_64 \
-macosx_version_min 11.1 \ # 设置支持的最小版本
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-lsystem -framewoek Foundation \
-all_load \
libTestExample.a -o libTestExample.dylib
注意上面的-all_load
,这一点我们再9、iOS强化 --- 静态库静态库的最后讲过,这里因为dylib
并没有使用.a
文件里面的函数,所有如果不单独设置,默认是-noall_load
。
运行build.sh
:
执行
test
:我们发现,
test
依然报错。
那么
dyld: Library not loaded
这个错误的是怎么产生的呢?
首先我们要明确一点,我们的动态库
是通过dyld
在运行时动态加载的。
那么我们在编译的时候只是告诉了test
符号,但是在运行过程中,dyld
动态加载动态库
,此时去找符号的真实的地址,发现找不到。
动态库Framework
下面我们通过Framework
来讲解一下,来解决一下上面的问题:
-
Framework
本质上就是对静态库
或者动态库
的一层包装。
首先我们创建如下的文件格式(这一点想必大家在静态库这一节里面已经非常熟悉了):
同样的我们使用脚本来编译和链接我们的代码:
echo "编译TestExample.m ---> TestExample.o"
clang -target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-I./Headers \
-c TestExample.m -o TestExample.o
echo "编译TestExample.o ---> TestExample.dylib"
# -dynamiclib: 动态库
# dylib 最终链接的产物
ld -dylib -arch x86_64 \
-macosx_version_min 11.1 \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-lsystem -framework Foundation \
TestExample.o -o TestExample
# 这里我们就不再外部去修改文件的后缀和文件名了,我们直接生成TestExample动态库
执行结果:
这样我们的
framework
就构建起来了,接下来我们再来编译链接我们的test
,脚本:
echo "编译test.m ---> test.o"
clang -target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-I./Frameworks/TestExample.framework/Headers \
-c test.m -o test.o
echo "链接test.o ---> test"
clang -target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-F./Frameworks \
-framework TestExample \
test.o -o test
- 其实这个时候运行
test
还是会报和上面一样的错误。
现在这个错误已经让人非常的头疼了,那为什么会产出这样一个错误呢?究竟我们要怎么做才能解决这个错误呢?
这就要从dyld
加载动态库说起了,首先我们来看下面这张图:
当我们的
dyld
去加载一个Mach-O
的时候,Mach-O
里面有一个Load Command
叫做LC_LOAD_DYLIB
,里面保存了使用到的动态库的路径。
我们都知道,动态库
是运行时加载的,其实就是通过LC_LOAD_DYLIB
找到动态库
的路径,然后去加载的。那么我们就来看一下我们刚刚生成的
test
可执行文件里面的LC_LOAD_DYLIB
:
otool -l test | grep 'DYLIB' -A 5
// -A 向下打印
// -B 向上打印
// 5 五行
- 那么我们怎么去告诉可执行文件,
动态库的路径
呢?
这里大家要明确一点,动态库的路径
肯定是需要动态库自己去告诉可执行文件的。
这就需要我们在生成动态库
的时候,有一个专门的字段来保存动态库的路径
。也就是说。
我们查看一下这个Load Command
(LC_ID_DYLIB
):
此时这个路径是不对的。说明我们在生成动态库的时候,这个路径给错了。
下面我们就来修改一下动态库
的路径。
先介绍一个搜索指令:
otool -l test | grep 'rpath' -A 5 -i
/// 这条指令是大小写敏感的,如果想要大小写不敏感,就在末尾加一个 "-i"
方法一:install_name_tool
通过 install_name_tool
的id
指令,从外部修改LC_ID_DYLIB
。
接下来我们再来看一下
test
里面的LC_LOAD_DYLIB
:此时再运行
test
就不会报错了。方法二:在生成动态库的时候,就将地址写进入
大家看到上面的方法是在生成动态库
之后,才去修改动态库地址。
其实我们可以在生成的过程中,就去修改。
install_name
是连接器(ld)的一个参数,我们来看一下:
install_name
就是用来设置LC_ID_DYLIB
的值的。
这个时候我们来引入另一个知识点:
@rpath
上面我们在给LC_ID_DYLIB
,设置值的时候,传入的是一个绝对路径,这就有一个不好的地方。那就是我们动态库
不能在其他的地方使用了。
ⅰ:@rpath
:Runpath search Paths
,dyld
搜索路径,运行时@rpath
指示dyld
按顺序搜索路径列表,以找到动态库。
ⅱ:@rpath
保存一个或多个路径的变量。@rpath
是由可执行文件提供的,也就是说:。
ⅰ:@executable_path
:表示可执行文件所在的目录,解析问可执行程序的绝对路径。
ⅱ:@loader_path
:表示被加载的Mach-O
所在的目录,每
次加载时都可能被设置为不同的路径,由上层指定。这次我们不使用
install_name_tool
1、首先我们在TestExampleBuild.sh
文件中的TestExample.o
链接生成TestExample.dylib
的时候加上这样一条指令(这里是直接通过ld
链接器操作的,所以不需要Xlinker
;当然也可以使用clang
,写法跟build.sh
里面的指令一样):
-install_name @rpath/TestExample.framework/TestExample \
ld -dylib -arch x86_64 \
-macosx_version_min 11.1 \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-lsystem -framework Foundation \
-install_name @rpath/TestExample.framework/TestExample \
TestExample.o -o TestExample
2、接着在build.sh
,最后生成test
可执行文件的时候,加上这样一条指令:
-Xlinker -rpath -Xlinker @executable_path/Frameworks \
echo "链接test.o ---> test"
clang -target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \
-F./Frameworks \
-framework TestExample \
-Xlinker -rpath -Xlinker @executable_path/Frameworks \
test.o -o test
同样的执行脚本之后,test
还是可以运行成功的。
-
注意:
LC_RPATH
可以有多条,所以使用的时候需要注意。
多个动态库嵌套
-
多个动态库
嵌套的原理,跟使用单个动态库
一样。因为本身动态库就是编译连接的最终产物。比如中间动态库
需要给下一级动态库
设置rpath
的时候,跟上面的build.sh
一样。
⚠️ ⚠️⚠️ :注意,此时中间动态库给下一级动态库提供rpath
的时候,使用的是@loader_path
:
-Xlinker -rpath -Xlinker @loader_path/Frameworks \
同时,中间动态库
处理引入自己的头文件
之外,还要引入下一级动态库的头文件
:
-I./Headers \
-I./Frameworks/TestExampleLog.framework/Headers \
下面讲一下多个动态库
的另一个问题:
比如:
动态库
TestExample
里面嵌套者SubTestExample
,如果说test
想要使用SubTestExample
里面的函数,这个时候应该怎么办?因为
TestExample
里面的符号对于test
是暴露的;SubTestExample
里面的符号对于TestExample
是暴露的;但是,
SubTestExample
里面的符号对于test
不是暴露。(有兴趣的同学可以打印一下TestExample
的导出符号表objdump --macho --exports-trie TestExample
)
这个时候我们就要用到链接器的参数-reexport_framework
-reexport_framework name[,suffix]
This is the same as the -framework name[,suffix] but also specifies that the all symbols
in that framework should be available to clients linking to the library being created.
This was previously done with a separate -sub_umbrella option.
我们在中间动态库
的插件TestExampleBuild.sh
里面添加这样一条指令(链接生成动态库的时候,不是编译的时候):
-Xlinker -reexport_framework -Xlinker SubTestExample \
这样,中间动态库
就会增加一条Load Command
: LC_REEXPORT_DYLIB
。
这样我们的可执行文件test
就可以通过读取LC_REEXPORT_DYLIB
找到后面的动态库。
使用的时候,在test
的build.sh
里面,test.m -> test.o
的时候,引入SubTestExample
的头文件:
-I./Frameworks/TestExample.framework/Frameworks/SubTestExample.framework/Headers \
这样test
就可以正常使用SubTestExample
里面的函数了。