美文网首页iOS小知识点
10、iOS强化 --- 动态库

10、iOS强化 --- 动态库

作者: Jax_YD | 来源:发表于2021-03-13 19:11 被阅读0次
    • 什么事动态库?
      与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。

    • 动态库的格式有:.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
    • 那么我们怎么去告诉可执行文件,动态库的路径呢?
      这里大家要明确一点,动态库的路径肯定是需要动态库自己去告诉可执行文件的。
      这就需要我们在生成动态库的时候,有一个专门的字段来保存动态库的路径。也就是说\color{red}{动态库的路径,是保存在动态库自己的Mach-O中的}
      我们查看一下这个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是由可执行文件提供的,也就是说:\color{red}{谁链接 动态库,谁就给 动态库 提供 @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强化 --- 动态库

        本文链接:https://www.haomeiwen.com/subject/xtkzqltx.html