补充
之前的文章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语言
,会写脚本
,这部分都是基础
,后面会介绍项目高阶应用:静态库合并,动态库优化
等内容。有什么疑问可以在下面留言,也希望大家多多交流,点赞!