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中的部分符号。

AFURLRequestSerialization中的符号

通过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文件中的符号

UIImage+ForceDecode中的符号

这样我们可以用另一个正则[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这部分的正确性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容