10、iOS强化 --- 动态库

  • 什么事动态库?
    与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。

  • 动态库的格式有:.framework.dylib.tbd

  • 缺点:会导致一些性能损失。但是可以优化,比如延迟绑定 (Lazy Binding) 技术。

  • .framework.dylib 在之前的文章里都有介绍,这里就不多做赘述。那么什么事tbd格式呢?
    tbd:全称text-based stub libraries,本质上就是一个YAML描述的文本文件。
    它的作用是用于记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息等等。用于避免在真机开发过程中直接使用传统的dylib
    对于真机来说,由于动态库都是在设备上的,在Xcode上使用基于tbd格式的伪framework可以大大减少Xcode的大小。


接下来我们一起来探索一下动态库

动态库原理

首先看一下我们的测试环境:

image

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

执行完脚本是这个样子的:


image
  • 接下来我们运行一下test:
    Xnip2021-03-13_11-16-10.png

为什么会报这样一个错误呢?
这里我们就要弄明白动态库到底是一个什么东西:
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:

image

执行test
image

我们发现,test依然报错。

那么dyld: Library not loaded这个错误的是怎么产生的呢?
首先我们要明确一点,我们的动态库是通过dyld在运行时动态加载的。
那么我们在编译的时候只是告诉了test符号,但是在运行过程中,dyld动态加载动态库,此时去找符号的真实的地址,发现找不到。

动态库Framework

下面我们通过Framework来讲解一下,来解决一下上面的问题:

  • Framework本质上就是对静态库或者动态库的一层包装。
    首先我们创建如下的文件格式(这一点想必大家在静态库这一节里面已经非常熟悉了):
    image

    同样的我们使用脚本来编译和链接我们的代码:
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动态库

执行结果:

image

这样我们的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
image
  • 其实这个时候运行test还是会报和上面一样的错误。
    现在这个错误已经让人非常的头疼了,那为什么会产出这样一个错误呢?究竟我们要怎么做才能解决这个错误呢?

这就要从dyld加载动态库说起了,首先我们来看下面这张图:

image.png

  • 当我们的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 五行
image
  • 那么我们怎么去告诉可执行文件,动态库的路径呢?
    这里大家要明确一点,动态库的路径肯定是需要动态库自己去告诉可执行文件的。
    这就需要我们在生成动态库的时候,有一个专门的字段来保存动态库的路径。也就是说。
    我们查看一下这个Load Command(LC_ID_DYLIB):
    image

    此时这个路径是不对的。说明我们在生成动态库的时候,这个路径给错了。

下面我们就来修改一下动态库的路径。
先介绍一个搜索指令:

otool -l test | grep 'rpath' -A 5 -i
/// 这条指令是大小写敏感的,如果想要大小写不敏感,就在末尾加一个 "-i"
方法一:install_name_tool

通过 install_name_toolid指令,从外部修改LC_ID_DYLIB

image

接下来我们再来看一下test里面的LC_LOAD_DYLIB:
image

此时再运行test就不会报错了。
image

方法二:在生成动态库的时候,就将地址写进入

大家看到上面的方法是在生成动态库之后,才去修改动态库地址。
其实我们可以在生成的过程中,就去修改。
install_name是连接器(ld)的一个参数,我们来看一下:

image

install_name就是用来设置LC_ID_DYLIB的值的。

  • 这个时候我们来引入另一个知识点:@rpath
    上面我们在给LC_ID_DYLIB,设置值的时候,传入的是一个绝对路径,这就有一个不好的地方。那就是我们动态库不能在其他的地方使用了。
    ⅰ: @rpathRunpath search Pathsdyld搜索路径,运行时@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可以有多条,所以使用的时候需要注意。
    image

多个动态库嵌套

  • 多个动态库嵌套的原理,跟使用单个动态库一样。因为本身动态库就是编译连接的最终产物。比如中间动态库需要给下一级动态库设置rpath的时候,跟上面的build.sh一样。
    ⚠️ ⚠️⚠️ :注意,此时中间动态库给下一级动态库提供rpath的时候,使用的是@loader_path:
-Xlinker -rpath -Xlinker @loader_path/Frameworks \

同时,中间动态库处理引入自己的头文件之外,还要引入下一级动态库的头文件

-I./Headers \
-I./Frameworks/TestExampleLog.framework/Headers \

下面讲一下多个动态库的另一个问题:
比如:

image.png

动态库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找到后面的动态库。
使用的时候,在testbuild.sh里面,test.m -> test.o的时候,引入SubTestExample的头文件:

-I./Frameworks/TestExample.framework/Frameworks/SubTestExample.framework/Headers \

这样test就可以正常使用SubTestExample里面的函数了。

你可能感兴趣的:(10、iOS强化 --- 动态库)