SDK開發(fā)中我們可能希望使用已有的第三方開源庫酸纲,比如在發(fā)送請求的功能上我們更希望用AFNetworking而非直接使用NSURLSession却桶,又如在實(shí)現(xiàn)socket連接時(shí)我們更希望用SocketRocket而非自己從零實(shí)現(xiàn)万栅。但如果我們直接把AFNetworking的源文件拖到靜態(tài)庫SDK里随闺,而宿主APP也引入了AFNetworking龟梦,這時(shí)運(yùn)行代碼就會報(bào)符號沖突(duplicate symbols)的錯(cuò)誤驾荣。
這時(shí)大部分人的解決方案都是手動修改引入到SDK里的開源庫代碼夺欲,包括類名谓罗、分類名媒至、全局常量名顶别、協(xié)議名等會導(dǎo)致沖突的符號。其實(shí)對于像AFNetworking(v3.2.1)這種源碼量較少的第三方庫來說拒啰,需要修改的地方都要多達(dá)47個(gè)驯绎,可想而知這是一項(xiàng)多么低效和易錯(cuò)的解決方案,而且如果下次需要升級SDK中的該第三方庫谋旦,你需要再重新手動改一遍……下邊我們來一步步深入解決這件麻煩事剩失。
首先我們考慮下怎樣避免每次都要修改第三方庫源碼,如果有一個(gè)單獨(dú)的文件來存原符號和重命名符號的對應(yīng)關(guān)系就好了蛤织,我們自然而然地會想到用宏定義赴叹。創(chuàng)建一個(gè)頭文件,比如叫XNGNamespace.h
// XNGNamespace.h
#define AFURLSessionManager XNGURLSessionManager
#define AFNetworkingReachabilityDidChangeNotification XNGNetworkingReachabilityDidChangeNotification
#define AFImageResponseSerializer XNGImageResponseSerializer
...
然后在你的SDK工程中指蚜,如果你已經(jīng)有一個(gè)預(yù)編譯頭文件(一般為xxx.pch)乞巧,在最上一行引入XNGNamespace.h,否則在Build Settings -> Prefix Header配置該文件的路徑(即把這個(gè)文件作為預(yù)編譯頭文件)摊鸡。這時(shí)你可以在Xcode中看到原本的比如AFURLSessionManager
類名顏色變成和宏一樣的顏色绽媒,準(zhǔn)確地說,這個(gè)類現(xiàn)在其實(shí)叫XNGURLSessionManager
了免猾。
現(xiàn)在有了這個(gè)文件后是辕,即使要升級SDK中的第三方庫,我們也只需要在這個(gè)文件里做少量增刪了猎提。
但到目前為止最麻煩的這部分事還沒解決获三,畢竟現(xiàn)在還是要靠肉眼找出那些符號,手動編寫宏定義锨苏。有沒有什么命令或腳本幫我們分析出這些符號呢疙教,這正好可以借助nm命令了。nm是Linux下用于查看指定文件(對象文件伞租、可執(zhí)行文件或?qū)ο笪募欤┲蟹柫斜淼拿钫晡剑詾榱擞眠@個(gè)命令,我們需要先做點(diǎn)準(zhǔn)備工作葵诈。
一裸弦、準(zhǔn)備工作
如上所述祟同,我們需要得到一個(gè)可供nm命令分析的文件。新建一個(gè)庫工程理疙,F(xiàn)ramework類型或Static Library類型都可以晕城,將第三方庫的源碼拖入其中,運(yùn)行產(chǎn)出靜態(tài)庫文件沪斟。因?yàn)楹筮叿治鲆彩侵苯优茉贛acOS上广辰,所以這里直接產(chǎn)出當(dāng)前架構(gòu)的debug模式庫即可。如果是Static Library類型主之,我們需要的直接就是.a文件,如果是Framework類型李根,我們需要的是.framework中的那個(gè)同名文件槽奕。這兩種文件分析起來無差異,下文統(tǒng)一用.a的情況來說明房轿。
二粤攒、分析
不了解nm命令的同學(xué)可以先看下這個(gè)Tutorial,也建議看下完整的man page囱持。下面以分析AFNetworking庫為例夯接,假如我們的庫名叫libMyAFNetworking,cd到所在目錄執(zhí)行nm libMyAFNetworking
纷妆,即可得到每個(gè).o文件中的符號盔几。下圖截取了AFURLRequestSerialization.o中的部分符號。
通過nm命令的文檔掩幢,我們了解到.o文件中頻繁出現(xiàn)的幾種符號是如下定義:
對于每一個(gè)符號來說逊拍,其類型如果是小寫的,則表明該符號是local的际邻;大寫則表明該符號是global(external)的芯丧。
- B 該符號的值出現(xiàn)在非初始化數(shù)據(jù)段(bss)中。例如世曾,在一個(gè)文件中定義全局static int test缨恒。則該符號test的類型為b,位于bss section中轮听。其值表示該符號在bss段中的偏移骗露。一般而言,bss段分配于RAM中蕊程。
- D 該符號位于初始化數(shù)據(jù)段中椒袍。一般來說,分配到data section中藻茂。
例如:定義全局int baud_table[5] = {9600, 19200, 38400, 57600, 115200}驹暑,會分配到初始化數(shù)據(jù)段中玫恳。- S 符號位于非初始化數(shù)據(jù)區(qū),用于small object优俘。
- T 該符號位于代碼區(qū)text section京办。
- U 該符號在當(dāng)前文件中是未定義的,即該符號的定義在別的文件中帆焕。
例如惭婿,當(dāng)前文件調(diào)用另一個(gè)文件中定義的函數(shù),在這個(gè)被調(diào)用的函數(shù)在當(dāng)前就是未定義的叶雹;但是在定義它的文件中類型是T财饥。但是對于全局變量來說,在定義它的文件中折晦,其符號類型為C钥星,在使用它的文件中,其類型為U满着。
一般OC文件
現(xiàn)在我們先不考慮category屬性的getter和setter這種私有方法(下文會單獨(dú)說明)谦炒,所以只關(guān)注類型是大寫字母的符號。我們可以很容易的歸納出
- 類型是S风喇,且以
_OBJC_CLASS_$_
開頭的是類名宁改,以__OBJC_LABEL_PROTOCOL_$_
開頭的是協(xié)議名,只以下劃線_
開頭的是全局常量名 - 類型是T魂莫,且只以下劃線
_
開頭的是全局函數(shù)名 - 類型是D还蹲,且以
__OBJC_PROTOCOL_$_
開頭的是協(xié)議名,不過我們直接用S的規(guī)則就可以了豁鲤。D類型其實(shí)也存在以_OBJC_CLASS_$_
開頭的類名和以下劃線_
開頭的全局常量名秽誊,上邊樣例文件中未給出。
有了目標(biāo)后琳骡,我們就可以對于每行符號锅论,用正則[0-9a-f]{16} [STD] (_OBJC_CLASS_\$|__OBJC_LABEL_PROTOCOL_\$)?_([_A-Za-z][^_]\w+)\n
來匹配得到目標(biāo)符號了。但這里還有個(gè)比較坑的問題楣号,對于D類型的符號最易,可以看到蘋果官方SDK中的協(xié)議名也會被列出來,考慮到知名第三方庫一般不會和蘋果官方前綴相同炫狱,所以我會過濾掉以官方前綴(如NS藻懒、UI、WK等等)開頭的協(xié)議名视译。
C++文件
有些第三方庫包含C++代碼嬉荆,由于編譯器的name mangling機(jī)制,直接用nm命令只能看到更改后的函數(shù)名酷含。我們可以用Linux的另一個(gè)命令c++filt
顯示原本的函數(shù)名鄙早。
// 同樣是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類型會變?yōu)?unsigned short"舱污,下邊我們分析category屬性時(shí)要注意這點(diǎn)。
OC category文件
我們先考慮下對于category弥虐,哪些符號會沖突扩灯。首先分類名肯定是要改的,但是只保證分類名不同就萬事大吉了嗎霜瘪?不同分類中的方法和屬性都是往主類的方法列表和屬性列表中插入的珠插,如果我們SDK中使用的第三方庫版本和宿主APP使用的版本不一致,就可能存在分類方法名相同但方法實(shí)現(xiàn)不同颖对,分類屬性名相同但關(guān)聯(lián)對象存取策略不同丧失,導(dǎo)致代碼邏輯錯(cuò)誤。所以除了分類名惜互,我們還有必要修改分類的屬性名和方法名。
下面的例子是SDWebImage的UIImage+ForceDecode.o文件中的符號
這樣我們可以用另一個(gè)正則
[0-9a-f]{16} unsigned short [+-]\[\w+\((\w+)\) ([\w:]+)\]\n
匹配分類中所需要的符號了琳拭。這里要注意我們只根據(jù)屬性的getter方法得到屬性名训堆,而不要把setter方法也加入到需要修改的符號行列。
三白嘁、產(chǎn)出宏定義
我們已經(jīng)通過第二步的分析獲取到了所有需要重定義的符號坑鱼,除category屬性外,遍歷一遍絮缅,將加了前綴的符號宏定義為原始符號鲁沥。對于category屬性則需要點(diǎn)額外操作,可以想象下屬性名為foo
耕魄,如果要加前綴XN
画恰,那么它的getter方法是直接加前綴為-XNfoo
,但setter方法不是直接加前綴變?yōu)?code>-XNsetFoo:吸奴,而應(yīng)該是-setXNfoo:
了允扇。
分析和產(chǎn)出的過程我已經(jīng)寫了個(gè)Python腳本來做,代碼放在這里https://github.com/xuning0/RedefineSymbols
则奥。用法的話考润,比如你要分析的是libMySDWebImage.a,要加的命名空間前綴是ABC读处,那么執(zhí)行
python3 redefine_symbols.py --ns ABC libMySDWebImage.a
糊治,即可在當(dāng)前目錄產(chǎn)出ABCNamespace.h文件。如上文所說將其拖入你的SDK工程罚舱,設(shè)置為預(yù)編譯頭文件或在已存在的預(yù)編譯頭文件第一行import井辜。以下截圖就是針對SDWebImage產(chǎn)出的ABCNamespace.h部分樣例绎谦。這個(gè)腳本可以覆蓋絕大部分情況,但由于OC屬性命名的特殊性抑胎,在拿到產(chǎn)出文件后最好人工核查category getter和setter這部分的正確性燥滑。