iOS静态库开发中引入的第三方库可能与宿主APP中冲突的解决方案
SDK开发中我们可能希望使用已有的第三方开源库,比如在发送请求的功能上我们更希望用AFNetworking而非直接使用NSURLSession,又如在实现socket连接时我们更希望用SocketRocket而非自己从零实现。但如果我们直接把AFNetworking的源文件拖到静态库SDK里,而宿主APP也引入了AFNetworking,这时运行代码就会报符号冲突(duplicate symbols)的错误。
符号冲突报错
这时大部分人的解决方案都是手动修改引入到SDK里的开源库代码,包括类名、分类名、全局常量名、协议名等会导致冲突的符号。其实对于像AFNetworking(v3.2.1)这种源码量较少的第三方库来说,需要修改的地方都要多达47个,可想而知这是一项多么低效和易错的解决方案,而且如果下次需要升级SDK中的该第三方库,你需要再重新手动改一遍……下边我们来一步步深入解决这件麻烦事。
首先我们考虑下怎样避免每次都要修改第三方库源码,如果有一个单独的文件来存原符号和重命名符号的对应关系就好了,我们自然而然地会想到用宏定义。创建一个头文件,比如叫XNGNamespace.h
// XNGNamespace.h
#define AFURLSessionManager XNGURLSessionManager
#define AFNetworkingReachabilityDidChangeNotification XNGNetworkingReachabilityDidChangeNotification
#define AFImageResponseSerializer XNGImageResponseSerializer
...
然后在你的SDK工程中,如果你已经有一个预编译头文件(一般为xxx.pch),在最上一行引入XNGNamespace.h,否则在Build Settings -> Prefix Header配置该文件的路径(即把这个文件作为预编译头文件)。这时你可以在Xcode中看到原本的比如AFURLSessionManager
类名颜色变成和宏一样的颜色,准确地说,这个类现在其实叫XNGURLSessionManager
了。
现在有了这个文件后,即使要升级SDK中的第三方库,我们也只需要在这个文件里做少量增删了。
但到目前为止最麻烦的这部分事还没解决,毕竟现在还是要靠肉眼找出那些符号,手动编写宏定义。有没有什么命令或脚本帮我们分析出这些符号呢,这正好可以借助nm命令了。nm是Linux下用于查看指定文件(对象文件、可执行文件或对象文件库)中符号列表的命令,所以为了用这个命令,我们需要先做点准备工作。
一、准备工作
如上所述,我们需要得到一个可供nm命令分析的文件。新建一个库工程,Framework类型或Static Library类型都可以,将第三方库的源码拖入其中,运行产出静态库文件。因为后边分析也是直接跑在MacOS上,所以这里直接产出当前架构的debug模式库即可。如果是Static Library类型,我们需要的直接就是.a文件,如果是Framework类型,我们需要的是.framework中的那个同名文件。这两种文件分析起来无差异,下文统一用.a的情况来说明。
二、分析
不了解nm命令的同学可以先看下这个Tutorial,也建议看下完整的man page。下面以分析AFNetworking库为例,假如我们的库名叫libMyAFNetworking,cd到所在目录执行nm libMyAFNetworking
,即可得到每个.o文件中的符号。下图截取了AFURLRequestSerialization.o中的部分符号。
通过nm命令的文档,我们了解到.o文件中频繁出现的几种符号是如下定义:
对于每一个符号来说,其类型如果是小写的,则表明该符号是local的;大写则表明该符号是global(external)的。
- B 该符号的值出现在非初始化数据段(bss)中。例如,在一个文件中定义全局static int test。则该符号test的类型为b,位于bss section中。其值表示该符号在bss段中的偏移。一般而言,bss段分配于RAM中。
- D 该符号位于初始化数据段中。一般来说,分配到data section中。
例如:定义全局int baud_table[5] = {9600, 19200, 38400, 57600, 115200},会分配到初始化数据段中。- S 符号位于非初始化数据区,用于small object。
- T 该符号位于代码区text section。
- U 该符号在当前文件中是未定义的,即该符号的定义在别的文件中。
例如,当前文件调用另一个文件中定义的函数,在这个被调用的函数在当前就是未定义的;但是在定义它的文件中类型是T。但是对于全局变量来说,在定义它的文件中,其符号类型为C,在使用它的文件中,其类型为U。
一般OC文件
现在我们先不考虑category属性的getter和setter这种私有方法(下文会单独说明),所以只关注类型是大写字母的符号。我们可以很容易的归纳出
- 类型是S,且以
_OBJC_CLASS_$_
开头的是类名,以__OBJC_LABEL_PROTOCOL_$_
开头的是协议名,只以下划线_
开头的是全局常量名 - 类型是T,且只以下划线
_
开头的是全局函数名 - 类型是D,且以
__OBJC_PROTOCOL_$_
开头的是协议名,不过我们直接用S的规则就可以了。D类型其实也存在以_OBJC_CLASS_$_
开头的类名和以下划线_
开头的全局常量名,上边样例文件中未给出。
有了目标后,我们就可以对于每行符号,用正则[0-9a-f]{16} [STD] (_OBJC_CLASS_\$|__OBJC_LABEL_PROTOCOL_\$)?_([_A-Za-z][^_]\w+)\n
来匹配得到目标符号了。但这里还有个比较坑的问题,对于D类型的符号,可以看到苹果官方SDK中的协议名也会被列出来,考虑到知名第三方库一般不会和苹果官方前缀相同,所以我会过滤掉以官方前缀(如NS、UI、WK等等)开头的协议名。
C++文件
有些第三方库包含C++代码,由于编译器的name mangling机制,直接用nm命令只能看到更改后的函数名。我们可以用Linux的另一个命令c++filt
显示原本的函数名。
// 同样是PLCrashAsyncDwarfEncoding.o
// nm libCrashReporter-iOS.a
T __ZN7plcrash3PL_5async18dwarf_frame_reader4initEP21plcrash_async_mobjectPK23plcrash_async_byteorderbb
// nm libCrashReporter-iOS.a | c++filt
T plcrash::PL_::async::dwarf_frame_reader::init(plcrash_async_mobject*, plcrash_async_byteorder const*, bool, bool)
不过要注意下,如果加了c++filt的pipe,得到的符号列表中,t类型会变为"unsigned short",下边我们分析category属性时要注意这点。
OC category文件
我们先考虑下对于category,哪些符号会冲突。首先分类名肯定是要改的,但是只保证分类名不同就万事大吉了吗?不同分类中的方法和属性都是往主类的方法列表和属性列表中插入的,如果我们SDK中使用的第三方库版本和宿主APP使用的版本不一致,就可能存在分类方法名相同但方法实现不同,分类属性名相同但关联对象存取策略不同,导致代码逻辑错误。所以除了分类名,我们还有必要修改分类的属性名和方法名。
下面的例子是SDWebImage的UIImage+ForceDecode.o文件中的符号
这样我们可以用另一个正则
[0-9a-f]{16} unsigned short [+-]\[\w+\((\w+)\) ([\w:]+)\]\n
匹配分类中所需要的符号了。这里要注意我们只根据属性的getter方法得到属性名,而不要把setter方法也加入到需要修改的符号行列。
三、产出宏定义
我们已经通过第二步的分析获取到了所有需要重定义的符号,除category属性外,遍历一遍,将加了前缀的符号宏定义为原始符号。对于category属性则需要点额外操作,可以想象下属性名为foo
,如果要加前缀XN
,那么它的getter方法是直接加前缀为-XNfoo
,但setter方法不是直接加前缀变为-XNsetFoo:
,而应该是-setXNfoo:
了。
完整流程
分析和产出的过程我已经写了个Python脚本来做,代码放在这里https://github.com/xuning0/RedefineSymbols
。用法的话,比如你要分析的是libMySDWebImage.a,要加的命名空间前缀是ABC,那么执行
python3 redefine_symbols.py --ns ABC libMySDWebImage.a
,即可在当前目录产出ABCNamespace.h文件。如上文所说将其拖入你的SDK工程,设置为预编译头文件或在已存在的预编译头文件第一行import。以下截图就是针对SDWebImage产出的ABCNamespace.h部分样例。针对SDWebImage产出的ABCNamespace.h部分样例
这个脚本可以覆盖绝大部分情况,但由于OC属性命名的特殊性,在拿到产出文件后最好人工核查category getter和setter这部分的正确性。