iOS高级进阶系列之-库(中)动态库探索

2,726 阅读14分钟

系列文章:OC底层原理系列OC基础知识系列Swift底层探索系列iOS高级进阶系列

补充

之前的文章iOS高级进阶系列之-库(上)静态库探索在后面讲到了Dead_Strip,也说道了-all_load-ObjC以及-force_load,这篇文章让大家觉得-noall_load-all_load-ObjC以及-force_load可以控制Dead_Strip,其实不是这样的

  • Dead_Strip在Xcode中是Build Settings 查找Dead image.png

通过上面可以看出来这个Dead_Strip-noall_load-all_load-ObjC以及-force_load没有太大关系-noall_load、-all_load、-ObjC以及-force_load只是在连接静态库的时候起作用,而Dead_Strip只是给开发者提供了一种优化代码的方式

代码验证

image.png image.png

上图是test.m的代码,下图是文件,通过脚本将test.m编译成可执行文件。前面的文章介绍过:先将test.m编译成静态库test.o文件,再将TestExample.m编译成静态库TestExample.o,最后通过将test.o连接TestExample.o生成可执行文件。我们直接执行脚本

当生成可执行文件后,查看macho信息 image.png 我们可以发现这个里面没有任何关于TestExample内容,下面我们对脚本进行改进 image.png

脚本默认的为-noall_load,它不会将没有用过的代码放入到静态库中 改完脚本再次执行脚本 image.png 发现多了很多东西,而且未使用的TestExample也导入到项目中了 我们看下dead_strip的定义 image.png 大意就是删除没有被入口点或者导出符号使用过的代码。我们再改一下脚本 image.png 运行下脚本,看下变化 image.png 这里看的是符号表,我们看下之前代码 image.png 我们发现global_function全局导出符号,但是未被使用,但是在导出的符号表中并没有global_function,这就是-dead_strip作用 下面我们调用下global_function方法 image.png 在执行脚本,读取符号表 image.png 发现global_function在符号表

总结Dead_Strip

  • 1.没有被入口点使用就会被干掉
  • 2.没有被导出符号使用的也会被干掉

-all_load

下面我们将TestExample代码放开,在执行脚本 image.png 再来看下符号表 image.png

发现都在符号表里

下面该下脚本和代码 image.png image.png 问题:此时符号表还是否有TestExample内容 image.png

发现还是存在的,原因是OC代码动态运行的代码,所以不敢干掉权限不够

扩展

shell命令有个可以查看某个代码为什么存在的指令:-why_live image.png

查看global_function为什么会存在

image.png

解释为什么global_function符号存在,它被从test.o来的,它被main函数使用,main函数又从test.o来

上面讲完后对Dead_Strip和-noall_load、-all_load、-ObjC以及-force_load没有太大关系有了更好的理解了吧

动态库

首先我们来把test.m和AFNetworking动态库进行连接 image.png

下图为test.m代码,我们引入了AFN,并初始化AFHTTPSessionManager image.png

  • 1.先将test.m编译成目标文件 image.png

静态库中介绍过命令的用法,这里不再说明 image.png

  • 2.连接AFN动态库 image.png image.png

连接动态库连接静态库使用的指令都是一样

  • 3.运行test可执行文件 image.png

当我们运行,立即发生了错误 那究竟为什么会出错了?我们下面探究一下

动态库原理

准备之前的代码 image.png image.png

  • 1.为了节省时间,写一个脚本去编译可执行文件 image.png

红框的-dynamiclib就是告诉编译器我下面要执行的是编译动态库

  • 2.执行脚本
    • 执行脚本报下面错误 image.png
    • 执行chmod +x ./build.sh权限 再次运行脚本 image.png image.png
  • 3.运行可执行文件 image.png 我们发现还是报和上面相同的错误,为什么呢,是不是脚本命令是不是有问题?

分析原理

静态库的文章已经说过静态库.o的合集(文章直接演示将.o文件直接改成静态库可以运行),但是动态库是一个链接编译最终产物。这也就意味着静态库可以通过链接变成动态库

静态库链接编译为动态库

  • 1.修改脚本,将修改部分贴出来 image.png
  • 2.跑脚本 image.png

报错了,就是在链接的过程中找不到_TestExample导出符号

  • 3.我们查找一下动态库的符号表 image.png

发现导出符号表空的,什么都没有

  • 4.为什么是空的呢?我们看下脚本 image.png

前面介绍编译期连接的时候默认执行的是-noall_load,因为动态库没有使用.a内容,所以不加载。那么是不是这个原因造成的呢?我们改下脚本

  • 5.添加-all_load image.png
  • 6.再次运行脚本 image.png

发现符号表有内容

  • 7.再次跑可执行文件 image.png

还是熟悉的味道,是不是有点无语,问题究竟出在哪里呢?下面就来介绍动态库的另一个特性

总结

  • 1.静态库是.o文件的合集
  • 2.动态库是.o文件链接过后的产物
  • 3.静态库可以通过链接生成动态库
  • 4.动态库也就是最终产物,这也是动态库无法合并原因

动态库解析

下面我们连链接下动态库,我们下面来讲解LoginApp怎么去链接动态库SYCSSColor动态库 image.png 我们再看看SYCSSColor里面的内容 image.png

tbd格式

这个是链接路径

  • SYCSSColor.tbd文件直接拖到项目中,不在xcconfig中写编译链接,看看会发生什么
    image.png image.png image.png

通过上面我们看到了,我们引入了我SYCSSColor头文件,使用了代码并且编译成功了 我们只是将SYCSSColor.tbd文件放入项目中,那么什么是tbd格式呢?

  • 1.tbd全称是text-based stub libraries,本质上就是一个YAML描述的文本文件
  • 2.他的作用是用于记录动态库的一些信息,包括导出的符号动态库的架构信息动态库的依赖信息
  • 3.用于避免真机开发过程中直接使用传统的dylib
  • 4.对于真机来说,由于动态库都是在设备上,在Xcode上使用基于tbd格式伪framework可以大大减少Xcode的大小

分析tbd格式

我们查看下SYCSSColor.tbd的内容, image.png

  • 1.第9行exports导出的意思
  • 2.第11行symbols是符号的意思,也就意味着11-14行都是导出符号
  • 3.objc-classes是objc类的集合 通过上面我们可以知道我们用脚本去链接库的是用到-L,-l链接是符号,也就是我们只需要知道符号所在的位置不需要知道源码位置。上面编译通过了,下面就运行看看 image.png 上面看到直接崩溃,为什么呢?这是因为动态库是动态链接的,在运行的时候,由dyld动态加载动态库,在加载的时候,它回去找这个符号真实地址的时候,找不到了,而静态库编译的时候就已将符号归放在一起,不会动态加载

tbd怎么生成

Xcode的Build Settings搜索text image.png

原理:就是通过拼上一些参数,来扫描Headers里面的头文件,然后把这个符号写到文件里去

查找Framework实际位置

image.png image.png

  • 1.下面我们使用脚本编译,先看framework文件中的脚本 image.png
  • 2.运行脚本 image.png
  • 3.我们看到动态库是.dyld的形式,实际上是没有什么标识的,下面我们吧生成的删除,从新生成一份叫TestExample image.png image.png
  • 4.此时我们的Frameworks准备好了,下面就是将test.m和我们的Frameworks进行链接 image.png
  • 5.运行脚本 image.png

这就说明我们前面制作的.framework成功

  • 6.运行验证可执行文件 image.png 还是相同错误,为什么还会有这样的错误,这就要从dyld加载动态库说起。如下图: image.png
  • 1.这里的Mach-o相当于我们的可执行文件Test
  • 2.Test的Mach-o有个专门的LC_LOAD_DYLIB,它里面有我们需要用到的动态库路径`
  • 3.当它找不到这个路径的时候就会报错 下面我们看下路径到底是什么东西 image.png 我们发现我们Test中一共使用了5个动态库,那么使用哪5个呢? image.png

-A 5 意思就是找到动态库后多现实5行 我们看到我们使用的动态库中name字段就是该动态库路径,但是我们导入的动态库TestExample路径只有一个名字,别的什么都没有,这样肯定找不到TestExample。 image.png 下面我们就来解决这个问题

动态库路径

动态库有一个专门的地方保存自己的路径,也就是说动态库的路径是保存在自己的Mach-o中的,下面我们就来查看下这个路径 image.png

-A是向下展示向上展示时-B

我们发现这个地方的路径就没有给对,我要给对正确的路径,外部有个命令来更改路径名称:install_name_tool image.png image.png

我们看到install_name_tool给的解释就是改变动态库install name

下面我们就使用一下,来改变路径 image.png

发现报错,这是因为我们并没有告诉编译期往哪里加这个路径 image.png

发现此时的路径已经更改了,那我们再来执行一次build.sh脚本,重新生成test文件,在查看下引入的动态库 image.png

我们发现此时的路径已经改变了,那么此时我们再运行可执行文件

image.png

此时并没有报错,成功运行了起来。也说明我们此时将test.m成功的连接了TestExample动态库

优化

脚本优化

上面我们讲了是生成动态库后去更改路径的,那么可以在生成前进行更改呢?答案是当然可以,我们在脚本中更改,我们使用命令-install_name,先看下install_name解释 image.png

就是添加路径 image.png 【问题】上面我们看到给到的是一个绝对路径,绝对路径有一个很大的问题,当我们做好SDK后给别人使用,那么这个路径就会改变,别人就需要更改路径,走着无法调用成功

路径优化

上面的问题怎么解决,这就需要双方约定好规则,什么规则,看下图: image.png

A使用B,B提供一个TestExample路径(Frameworks/TestExample.framework/TestExample),而A给B提供一个使用者路径(也就是test的路径)然后B给拼接起来,此时路径不久完整了嘛!

@rpath

这就牵扯到@rpath@rpath:Runpath search Paths! dyld搜索路径。运行时@rpath指示dyld按顺序搜索路径列表,以找到动态库@rpath保存一个或多个路径的变量! 也就是谁连接我谁给我提供@rpath,下面我们直接修改路径 image.png

此时我们看到路径已经修改完了

下面就是需要test给TestExample提供@rpath了,怎么提供,可执行文件test的mach-o中存一个@rpath,在连接的时候,将其提供给TestExample。 image.png

此时我们在test可执行文件中搜索rpath,发现不存在,说明此时如果我们去链接TestExample是找不到位置的,此时需要我们手动添加

我们查找一下命令 image.png

上面是替换rpath,下面的是添加rpath到指定的Mach-o

image.png

这时候就完成了,此时我们重新运行可执行文件

image.png

成功了

路径再优化

我们上面看到我们打印的path,依然是一个绝对路径,那么此时有么有什么办法,改为相对路径呢?这里系统给我们提供了两个方法:

  • 1.@executable_path:表示可执行程序所在的目录,解析为可执行文件的绝对路径。
  • 2.@loader_path:表示被加载的Mach-O所在的目录,每次加载时,都可能被设置为不同的路径,由上层指定。

@executable_path

也就是说如果用了@executable_path,不管在谁的电脑上它都指向执行程序所在的路径 下面我们来执行以下,因为我们已经设置过了rpath,所以需要替换,上面已经锁了替换方法,来进行操作 image.png 此时我们再看下test的rpath路径 image.png 已经将绝对路径修改成@executable_path,运行一下test文件,验证一下 image.png 我们看到运行成功,说明我们的修改是正确的

@loader_path

我们看下这种情况,test中使用的framework中包含TestExample.framework,而TestExample.framework的framework中包含TestExampleLog.framework image.png 下面我们编译一遍,从后往前编译

  • 1.先编译TestExampleLog.framework image.png
  • 2.编译TestExample.framework image.png image.png

我们发现链接的TestExampleLog.framework的name是不正确的,而且自身的name同样不正确

  • 3.编译test image.png 此时编译完成,但此时运行时报错的,因为路径不对,下面我们来改一下
  • 4.改动TestExampleLog.framework中的脚本 image.png

加入name,地址使用rpath,再运行脚本

image.png 此时的路径就对了

  • 5.改动TestExample.framework的脚本 image.png 运行脚本 image.png
  • 6.改动test的脚本 test是提供rpath路径image.png 运行脚本 image.png
  • 7.运行可执行文件 image.png

我们发现报错了,因为找不到TestExampleLog的路径,也就是我们在TestExample.framework的脚本中配了自己的路径,但是并没有配TestExampleLog的路径

  • 8.修改TestExample.framework的脚本(截取部分) image.png 运行脚本 image.png 下面我们手动看一下 image.png

我们看到这个路径比较长,下面我们再来生成下test,如果上面写的都是对的,那就能运行起来 image.png 我们看到这样运行时运行起来了,所以说我们之前配置的路径是对的

但是有个问题,我们上面的路径太长了,下面就用到了@loader_path,上面讲了那么多,一方面为了给大家属性下脚本,第二是让大家更好的理解。

  • 9.再次改脚本 image.png 在运行脚本,执行可执行文件 image.png

查看cocoaPod

为了加深@executable_path印象,我们可以看看cocoaPods的xcconfig文件 image.png

此时的name是一个名称

当我们编译代码,看下可执行文件的包内容,我们发现动态库都会存放在framework文件中,也就是上面设置的文件里 image.png

总结

到此最难的链接动态库的一种方式已经说完了,链接动态库由于动态加载,所以一定要指定好路径,否咋就会报错。

  • 1.我们介绍了通过install_name_tool更改路径
  • 2.为了优化路径,我们引入@rpath
  • 3.-rpath lod new 是替换路径-add_rpath是将路径添加到指定位置
  • 4.为了再优化,我们又引入了@executable_path@loader_path 上面讲了可以替换路径,那么就想到了破解,因为我们可以改变路径,将我们自己写的动态库作为目标软件的依赖达到破解的目的。我们这么做事改变了Mach-o,之前讲过Mach-o只有签名后才会被苹果系统承认,所以再破解软件是都会调用一句命令:code sign --force --deep --sign - 这句命令也就是强制打一个签名

实际应用

当我们做动态库时,可以在Xcode中设置我们上面说的命令 image.png 我们在搜下rpath image.png 我们上面的路径设置都可以在Xcode去处理

扩展

我们上面说的test是可以使用TestExample的方法,那么他test可以使用TestExampleLog的代码吗? 我们看下导出符号 image.png 我们看到这里面没有TestExampleLog的导出符号,所以不能使用,那么怎么才能够使用呢?在之前在讲符号的时候,说过重新导出符号,下面说下命令:reexport image.png

重新导出framework

image.png

重新导出TestExampleLog,运行脚本

image.png

多了一个重新导出符号TestExampleLog,此时就可以使用TestExampleLog代码

  • 验证,我们将test.m代码做如下改动,若运行打印没有问题,那就说明我们之前的是对的 image.png
  • 运行脚本,运行可执行文件 image.png 我们更改的代码以及打印都能正常运行,验证了我们上面改的东西

写到最后

动态库写的内容比较多,写了一周多时间!写了将近6000字,写的比较细,都是把自己探索过程记录下来,希望大家能够按着文章去实操一遍,这部分也比较无聊,但是这是高阶必走的路,在过程中学会shell语言,会写脚本,这部分都是基础,后面会介绍项目高阶应用:静态库合并,动态库优化等内容。有什么疑问可以在下面留言,也希望大家多多交流,点赞!