SDK開發中我們可能希望使用已有的第三方開源庫,比如在發送請求的功能上我們更希望用AFNetworking而非直接使用NSURLSession,又如在實現socket連接時我們更希望用SocketRocket而非自己從零實現。但如果我們直接把AFNetworking的源文件拖到靜態庫SDK里,而宿主 ...
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部分樣例。
這個腳本可以覆蓋絕大部分情況,但由於OC屬性命名的特殊性,在拿到產出文件後最好人工核查category getter和setter這部分的正確性。