补充
之前的文章iOS高级进阶系列之-库(上)静态库探索在后面讲到了Dead_Strip,也说道了-all_load、-ObjC以及-force_load,这篇文章让大家觉得-noall_load、-all_load、-ObjC以及-force_load可以控制Dead_Strip,其实不是这样的
- Dead_Strip在Xcode中是
Build Settings查找Dead
通过上面可以看出来这个
Dead_Strip和-noall_load、-all_load、-ObjC以及-force_load没有太大关系,-noall_load、-all_load、-ObjC以及-force_load只是在连接静态库的时候起作用,而Dead_Strip只是给开发者提供了一种优化代码的方式
代码验证
上图是test.m的代码,下图是文件,通过脚本将test.m编译成可执行文件。前面的文章介绍过:先将
test.m编译成静态库test.o文件,再将TestExample.m编译成静态库TestExample.o,最后通过将test.o连接TestExample.o生成可执行文件。我们直接执行脚本
当生成可执行文件后,查看macho信息
我们可以发现这个里面
没有任何关于TestExample的内容,下面我们对脚本进行改进
脚本
默认的为-noall_load,它不会将没有用过的代码放入到静态库中 改完脚本再次执行脚本发现多了很多东西,而且
未使用的TestExample也导入到项目中了 我们看下dead_strip的定义大意就是
删除没有被入口点或者导出符号使用过的代码。我们再改一下脚本运行下脚本,看下变化
这里看的是符号表,我们看下之前代码
我们发现
global_function是全局导出符号,但是未被使用,但是在导出的符号表中并没有global_function,这就是-dead_strip作用下面我们调用下global_function方法在执行脚本,读取符号表
发现
global_function在符号表中
总结Dead_Strip
- 1.
没有被入口点使用就会被干掉 - 2.
没有被导出符号使用的也会被干掉
-all_load
下面我们将TestExample代码放开,在执行脚本
再来看下符号表
发现都在符号表里
下面该下脚本和代码
问题:
此时符号表还是否有TestExample内容
发现还是
存在的,原因是OC代码是动态运行的代码,所以不敢干掉,权限不够
扩展
shell命令有个可以查看某个代码为什么存在的指令:-why_live
查看
global_function为什么会存在
解释为什么global_function符号存在,
它被从test.o来的,它被main函数使用,main函数又从test.o来
上面讲完后对Dead_Strip和-noall_load、-all_load、-ObjC以及-force_load没有太大关系有了更好的理解了吧
动态库
首先我们来把test.m和AFNetworking动态库进行连接
下图为test.m代码,我们引入了AFN,并初始化AFHTTPSessionManager
- 1.先将
test.m编译成目标文件
静态库中介绍过命令的用法,这里不再说明
- 2.连接AFN动态库
连接动态库和连接静态库使用的指令都是一样的
- 3.
运行test可执行文件
当我们运行,立即
发生了错误那究竟为什么会出错了?我们下面探究一下
动态库原理
准备之前的代码
- 1.为了节省时间,写一个脚本去编译可执行文件
红框的
-dynamiclib就是告诉编译器我下面要执行的是编译动态库
- 2.执行脚本
- 执行脚本报下面
错误 - 执行
chmod +x ./build.sh给权限再次运行脚本
- 执行脚本报下面
- 3.运行可执行文件
我们发现还是报和上面相同的错误,为什么呢,是不是脚本命令是不是有问题?
分析原理
静态库的文章已经说过静态库是.o的合集(文章直接演示将.o文件直接改成静态库可以运行),但是动态库是一个链接编译的最终产物。这也就意味着静态库可以通过链接变成动态库
静态库链接编译为动态库
- 1.修改脚本,将修改部分贴出来
- 2.跑脚本
报错了,就是在链接的过程中
找不到_TestExample导出符号
- 3.我们查找一下
动态库的符号表
发现
导出符号表是空的,什么都没有
- 4.为什么是空的呢?我们看下脚本
前面介绍
编译期在连接的时候默认执行的是-noall_load,因为动态库并没有使用到.a的内容,所以不加载。那么是不是这个原因造成的呢?我们改下脚本
- 5.
添加-all_load - 6.再次运行脚本
发现
符号表有内容了
- 7.再次跑可执行文件
还是熟悉的味道,是不是有点无语,问题究竟出在哪里呢?下面就来介绍动态库的另一个特性
总结
- 1.
静态库是.o文件的合集 - 2.
动态库是.o文件链接过后的产物 - 3.
静态库可以通过链接生成动态库 - 4.
动态库也就是最终产物,这也是动态库无法合并原因
动态库解析
下面我们连链接下动态库,我们下面来讲解LoginApp怎么去链接动态库SYCSSColor动态库
我们再看看SYCSSColor里面的内容
tbd格式
- 查看
xcconfig文件,具体内容查看iOS高级进阶系列之-项目开发基础(上)多环境配置,Mach-O与链接器
这个是链接路径
- 将
SYCSSColor.tbd文件直接拖到项目中,不在xcconfig中写编译链接,看看会发生什么
通过上面我们看到了,我们
引入了我SYCSSColor的头文件,使用了代码并且编译成功了 我们只是将SYCSSColor.tbd文件放入项目中,那么什么是tbd格式呢?
- 1.tbd全称是
text-based stub libraries,本质上就是一个YAML描述的文本文件。 - 2.他的作用是用于
记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息 - 3.用于
避免在真机开发过程中直接使用传统的dylib。 - 4.对于真机来说,由于
动态库都是在设备上,在Xcode上使用基于tbd格式的伪framework可以大大减少Xcode的大小。
分析tbd格式
我们查看下SYCSSColor.tbd的内容,
- 1.第9行
exports导出的意思 - 2.第11行
symbols是符号的意思,也就意味着11-14行都是导出符号 - 3.
objc-classes是objc类的集合通过上面我们可以知道我们用脚本去链接库的是用到-L,-l链接的是符号,也就是我们只需要知道符号所在的位置,不需要知道源码位置。上面编译通过了,下面就运行看看上面看到直接崩溃,为什么呢?这是因为
动态库是动态链接的,在运行的时候,由dyld动态加载动态库,在加载的时候,它回去找这个符号真实地址的时候,找不到了,而静态库在编译的时候就已将符号归放在一起,不会动态加载
tbd怎么生成
Xcode的Build Settings搜索text
原理:就是通过
拼上一些参数,来扫描Headers里面的头文件,然后把这个符号写到文件里去
查找Framework实际位置
- 1.下面我们使用脚本编译,先看framework文件中的脚本
- 2.运行脚本
- 3.我们看到
动态库是.dyld的形式,实际上是没有什么标识的,下面我们吧生成的删除,从新生成一份叫TestExample - 4.此时我们的Frameworks准备好了,下面就是
将test.m和我们的Frameworks进行链接 - 5.运行脚本
这就说明我们前面制作的
.framework是成功的
- 6.运行验证可执行文件
还是相同错误,为什么还会有这样的错误,这就要
从dyld加载动态库说起。如下图: - 1.这里的
Mach-o相当于我们的可执行文件Test - 2.Test的
Mach-o有个专门的LC_LOAD_DYLIB,它里面有我们需要用到的动态库路径` - 3.当它
找不到这个路径的时候就会报错下面我们看下路径到底是什么东西我们发现我们Test中一共使用了5个动态库,那么使用哪5个呢?
-A 5 意思就是找到动态库后
多现实5行我们看到我们使用的动态库中name字段就是该动态库路径,但是我们导入的动态库TestExample路径只有一个名字,别的什么都没有,这样肯定找不到TestExample。下面我们就来解决这个问题
动态库路径
动态库有一个专门的地方来保存自己的路径,也就是说动态库的路径是保存在自己的Mach-o中的,下面我们就来查看下这个路径
-A是向下展示,向上展示时-B
我们发现这个地方的路径就没有给对,我要给对正确的路径,外部有个命令来更改路径名称:install_name_tool
我们看到
install_name_tool给的解释就是改变动态库的install name
下面我们就使用一下,来改变路径
发现报错,这是因为我们并
没有告诉编译期往哪里加这个路径
发现此时的路径已经更改了,那我们再来执行一次build.sh脚本,重新生成test文件,在查看下引入的动态库
我们发现此时的路径已经改变了,那么此时我们再运行可执行文件
此时并没有报错,成功运行了起来。也说明我们此时将test.m成功的连接了TestExample动态库
优化
脚本优化
上面我们讲了是生成动态库后去更改路径的,那么可以在生成前进行更改呢?答案是当然可以,我们在脚本中更改,我们使用命令-install_name,先看下install_name解释
就是添加路径
【问题】
上面我们看到给到的是一个绝对路径,绝对路径有一个很大的问题,当我们做好SDK后给别人使用,那么这个路径就会改变,别人就需要更改路径,走着无法调用成功
路径优化
上面的问题怎么解决,这就需要双方约定好规则,什么规则,看下图:
A使用B,B提供一个
TestExample路径(Frameworks/TestExample.framework/TestExample),而A给B提供一个使用者路径(也就是test的路径)然后B给拼接起来,此时路径不久完整了嘛!
@rpath
这就牵扯到@rpath,@rpath:Runpath search Paths! dyld搜索路径。运行时@rpath指示dyld按顺序搜索路径列表,以找到动态库。@rpath保存一个或多个路径的变量!
也就是谁连接我谁给我提供@rpath,下面我们直接修改路径
此时我们看到路径已经修改完了
下面就是需要test给TestExample提供@rpath了,怎么提供,可执行文件test的mach-o中存一个@rpath,在连接的时候,将其提供给TestExample。
此时我们在test可执行文件中
搜索rpath,发现不存在,说明此时如果我们去链接TestExample是找不到位置的,此时需要我们手动添加
我们查找一下命令
上面是
替换rpath,下面的是添加rpath到指定的Mach-o
这时候就完成了,此时我们重新运行可执行文件
成功了
路径再优化
我们上面看到我们打印的path,依然是一个绝对路径,那么此时有么有什么办法,改为相对路径呢?这里系统给我们提供了两个方法:
- 1.
@executable_path:表示可执行程序所在的目录,解析为可执行文件的绝对路径。 - 2.
@loader_path:表示被加载的Mach-O所在的目录,每次加载时,都可能被设置为不同的路径,由上层指定。
@executable_path
也就是说如果用了@executable_path,不管在谁的电脑上它都指向执行程序所在的路径
下面我们来执行以下,因为我们已经设置过了rpath,所以需要替换,上面已经锁了替换方法,来进行操作
此时我们再看下test的rpath路径
已经将绝对路径修改成
@executable_path,运行一下test文件,验证一下
我们看到运行成功,说明我们的修改是正确的
@loader_path
我们看下这种情况,test中使用的framework中包含TestExample.framework,而TestExample.framework的framework中包含TestExampleLog.framework
下面我们编译一遍,从后往前编译
- 1.先编译
TestExampleLog.framework - 2.编译
TestExample.framework
我们发现链接的TestExampleLog.framework的name是不正确的,而且自身的name同样不正确
- 3.编译test
此时编译完成,但此时运行时报错的,因为
路径不对,下面我们来改一下 - 4.改动
TestExampleLog.framework中的脚本
加入name,地址使用rpath,再运行脚本
此时的路径就对了
- 5.改动
TestExample.framework的脚本运行脚本
- 6.改动test的脚本
test是
提供rpath路径的运行脚本
- 7.运行可执行文件
我们发现报错了,因为
找不到TestExampleLog的路径,也就是我们在TestExample.framework的脚本中配了自己的路径,但是并没有配TestExampleLog的路径
- 8.修改
TestExample.framework的脚本(截取部分)运行脚本
下面我们手动看一下
我们看到这个路径比较长,下面我们再来生成下test,如果上面写的都是对的,那就能运行起来
我们看到这样运行时运行起来了,所以说我们之前配置的路径是对的
但是有个问题,我们上面的路径太长了,下面就用到了@loader_path,上面讲了那么多,一方面为了给大家属性下脚本,第二是让大家更好的理解。
- 9.再次改脚本
在运行脚本,执行可执行文件
查看cocoaPod
为了加深@executable_path印象,我们可以看看cocoaPods的xcconfig文件
此时的
name是一个名称
当我们编译代码,看下可执行文件的包内容,我们发现动态库都会存放在framework文件中,也就是上面设置的文件里
总结
到此最难的链接动态库的一种方式已经说完了,链接动态库由于动态加载,所以一定要指定好路径,否咋就会报错。
- 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中设置我们上面说的命令
我们在搜下
rpath
我们上面的路径设置都可以在Xcode去处理
扩展
我们上面说的test是可以使用TestExample的方法,那么他test可以使用TestExampleLog的代码吗?
我们看下导出符号
我们看到这里面
没有TestExampleLog的导出符号,所以不能使用,那么怎么才能够使用呢?在之前在讲符号的时候,说过重新导出符号,下面说下命令:reexport
重新导出framework
重新导出TestExampleLog,运行脚本
多了一个
重新导出符号TestExampleLog,此时就可以使用TestExampleLog代码了
- 验证,我们将test.m代码做如下改动,若运行打印没有问题,那就说明我们之前的是对的
- 运行脚本,运行可执行文件
我们更改的代码以及打印
都能正常运行,验证了我们上面改的东西
写到最后
动态库写的内容比较多,写了一周多时间!写了将近6000字,写的比较细,都是把自己探索过程记录下来,希望大家能够按着文章去实操一遍,这部分也比较无聊,但是这是高阶必走的路,在过程中学会shell语言,会写脚本,这部分都是基础,后面会介绍项目高阶应用:静态库合并,动态库优化等内容。有什么疑问可以在下面留言,也希望大家多多交流,点赞!