在實(shí)際項(xiàng)目過程中,我們經(jīng)常會(huì)碰到引入三方的靜態(tài)庫后出現(xiàn)符號(hào)沖突的現(xiàn)象,也就是出現(xiàn) duplicate symbols 錯(cuò)誤,那么如何解決這類沖突呢?
這里我們區(qū)分幾種不同的沖突情況
最復(fù)雜的情況: 項(xiàng)目中使用的 libSDKA.a 和 libSDKB.a中有符號(hào)沖突,這里假定兩者都包含了很多同名的代碼等
這里,兩者重復(fù)的符號(hào),并一定是在同一個(gè)文件中,或者即使在同一個(gè).o中,但是,鬼知道它們有沒有對(duì)這些重復(fù)符號(hào)的類啥的方法添加了另外的內(nèi)容,也就是說即使它們兩包含了同一份代碼,也可能是并不兼容的兩個(gè)版本.
這里的解決辦法
最先能想到的肯定是讓這兩個(gè)靜態(tài)庫的開發(fā)者中的任意一個(gè)修改下他們的實(shí)現(xiàn),重新打個(gè)包過來,簡單,且解決的徹底!
不過,現(xiàn)實(shí)中往往只能呵呵,這兩個(gè)庫的開發(fā)者,不一定配合.
那么這個(gè)時(shí)候怎么解決?
剩下的唯一辦法,也就是 二進(jìn)制的符號(hào)重命名了!!
目前,對(duì)于二進(jìn)制的符號(hào)重命名,并沒有什么特別好的辦法,本來,感覺這個(gè)并不難,按說,應(yīng)該有一些工具可以用來重命名二進(jìn)制的符號(hào),很可惜,找了一圈,沒有這樣的工具,如果有,煩請(qǐng)各位大神也告知我一聲,謝謝
于是乎,在受到歐陽大哥的靜態(tài)攔截iOS對(duì)象方法調(diào)用的簡易實(shí)現(xiàn)的啟發(fā),外加最近剛好重新閱讀了pod package的實(shí)現(xiàn)mangle的源碼,于是乎想
我們?nèi)绻全@取了所有需要重命名的符號(hào)
是不是可以直接進(jìn)行二進(jìn)制的字符串替換來實(shí)現(xiàn)符號(hào)重命名呢?
當(dāng)然了,因?yàn)椴]有二進(jìn)制符號(hào)的直接修改工具,所以這里的符號(hào)重命名后的名字的長度一定要同原來的名字是同樣的長度!!!! 否則就破壞了mach-o格式了,也就沒法被識(shí)別和加載了,切記!
理論還是要實(shí)踐來檢驗(yàn),于是
我們用一個(gè)例子來檢驗(yàn),為了更加結(jié)合實(shí)踐,這里,我選擇一個(gè)稍微復(fù)雜點(diǎn)的庫來測(cè)試 FLEX
我們生成個(gè)FLEX的靜態(tài)庫 libFLEX.a
創(chuàng)建一個(gè)demo工程,拖入兩個(gè) libFLEX.a(當(dāng)然,一個(gè)重新命名為libRenameLib.a )
編譯運(yùn)行
不出意外,報(bào)錯(cuò)了,提示有1028個(gè)dumlicate symbols
我們首先是要提取libFLEX.a中的所有的需要重命名的符號(hào),這個(gè)腳本,我們可以直接提取pod package源碼中的提取部分來修改
這里我附上處理的代碼 getAllname.rb
#!/usr/bin/ruby
# 提取某個(gè)庫的滿足條件的符號(hào)
def symbols_from_library(library)
if !library
puts "文件不存在!"
return
end
# --defined-only :Display only defined symbols
# -g, --extern-only Display only external symbols
syms = `nm -defined-only -extern-only #{library}`.split("\n") # 獲取一個(gè)滿足符號(hào)的數(shù)組
result = classes_from_symbols(syms)
result += constants_from_symbols(syms)
result.select do |e|
case e
when 'llvm.cmdline', 'llvm.embedded.module', '__clang_at_available_requires_core_foundation_framework'
false
else
true
end
end
end
#獲取所有的class符號(hào)
def classes_from_symbols(syms)
classes = syms.select { |klass| klass[/OBJC_CLASS_\$_/] } #字符串的正則查找,滿足前綴是OBJC_CLASS_$_
classes = classes.uniq
classes.map! { |klass| klass.gsub(/^.*\$_/, '') }
end
#獲取所有的常量,全局變量符號(hào)
def constants_from_symbols(syms)
consts = syms.select { |const| const[/ S /] }
consts = consts.select { |const| const !~ /OBJC|\.eh/ }
consts = consts.uniq
consts = consts.map! { |const| const.gsub(/^.* _/, '') }
other_consts = syms.select { |const| const[/ T /] }
other_consts = other_consts.uniq
other_consts = other_consts.map! { |const| const.gsub(/^.* _/, '') }
consts + other_consts
end
# 輸出所有的符號(hào)
def create_symbols_file(library)
if !library
puts "文件不存在!"
return
end
syms = symbols_from_library(library)
syms = syms.uniq #去重
#puts "syms = #{syms}"
create_output_symbols_file(syms)
end
def create_output_symbols_file(syms)
all_new_str = ""
syms = syms.sort_by {|x| x.length} #按長度來排序
syms.each do |sym|
symDest = sym.clone;
symDest[symDest.length - 1] = '2' #這個(gè)規(guī)則可以自己定,這里我是把符號(hào)的最后一位換為2
all_new_str += "#{sym}==#{symDest}\n"
end
aFile = File.new("./input.txt", "w+")
if aFile
aFile.syswrite(all_new_str)
aFile.rewind
else
puts "Unable to open file!"
end
end
lib = ARGV[0]
#puts("ARGV= #{ARGV},lib = #{lib}")
create_symbols_file(lib)
使用的時(shí)候 傳入需要獲取修改的符號(hào)的文件名
ruby ./getAllname.rb ../libFLEX.a
在當(dāng)前目錄會(huì)生成一個(gè)input.txt,所有需要重命名的符號(hào)都在這個(gè)文件中.
接下來,使用如下的腳本 rename.rb 來對(duì)二進(jìn)制的符號(hào)進(jìn)行重命名
#!/usr/bin/ruby
require 'fileutils'
require 'pathname'
$symbol_file = ''
def disposeBin(oringin,dest)
binnaryFile = $symbol_file
command = "LC_CTYPE=C sed -i '' 's/#{oringin}/#{dest}/g' #{binnaryFile}"
puts("command = #{command}")
result = `LC_CTYPE=C sed -i '' 's/#{oringin}/#{dest}/g' #{binnaryFile}`
output = result
#puts("output = #{output}")
return output
end
def disposeLine(line)
arr = line.split("==")
origin = arr[0].chomp
dest = arr[1].chomp
puts ("origin = #{origin},dest = #{dest}")
return disposeBin(origin,dest)
end
def main
$symbol_file = ARGV[0]
fileName = ARGV[1]
if !$symbol_file || $symbol_file == ''
puts "文件不存在!"
return
end
if !fileName
puts "符號(hào)文件不存在!"
return
end
#獲得當(dāng)前執(zhí)行文件的完整路徑
path =File.dirname(__FILE__)
#path = Pathname.new(__FILE__).realpath
puts(path)
puts("cp -f #{$symbol_file} #{path}/libDispose.a")
`cp -f #{$symbol_file} #{path}/libDispose.a`
$symbol_file = "#{path}/libDispose.a"
File.open(fileName, "r") do |file|
file.each_line do |line|
res = disposeLine(line)
if !res
return
end
end
end
end
main
使用的時(shí)候
ruby ./rename.rb ../libFLEX.a ./input.txt
傳入的是 二進(jìn)制文件名和上面生成的要重名的符號(hào)文件
將生成的新的libDispose.a拷貝到demo工程中,可以看到,可以編譯運(yùn)行了,并且兩個(gè)都可以使用!!
當(dāng)然,其實(shí)還有一種方式:靜態(tài)庫動(dòng)態(tài)化
將某個(gè)靜態(tài)庫用動(dòng)態(tài)庫來包含,也就是靜態(tài)庫動(dòng)態(tài)化,這樣也可以解決問題, 我們繼續(xù)可以嘗試下:
建立一個(gè)Dynamic framework的工程(記得添加工程的 other link flags中的-ObjC,不然靜態(tài)庫中的很多內(nèi)容不會(huì)添加到動(dòng)態(tài)庫中)
將 libFLEX.a 添加進(jìn)去,生成動(dòng)態(tài)庫
將這個(gè)生成的DymamicContainer.framework添加到測(cè)試工程中,運(yùn)行,也是可以的,只是會(huì)出現(xiàn)運(yùn)行時(shí)警告
雖然編譯是通過了,不過這種方式帶來的問題,還是比較麻煩的
如果兩個(gè)重復(fù)的符號(hào)的實(shí)現(xiàn),完全一致,那非常好,皆大歡喜
如果兩個(gè)重復(fù)的符號(hào)的實(shí)現(xiàn)是不一致的,那由于不確定加載的會(huì)是哪個(gè),導(dǎo)致最后的行為是不可預(yù)知的
這種實(shí)現(xiàn),其實(shí)并不好,通常只是在兩個(gè)庫的重復(fù)符號(hào)的實(shí)現(xiàn)完全一致的情況下才比較好
相對(duì)來說,第一種辦法,是一種非常好的方式:
并不需要兩個(gè)靜態(tài)庫中的重復(fù)符號(hào)的實(shí)現(xiàn)是一致的!!
三方庫包含了某個(gè)開源代碼
例如,某靜態(tài)庫libSDK.a中融入了開源代碼AFnetworking,對(duì)于這種比較簡單的情況來說,我們有兩種處理方式
第一種處理方式:核心就是將.a分離成.o文件,然后把重復(fù)的.o文件去掉再重新打包成.o文件.
查看libSDK.a的架構(gòu)信息
lipo -info libSDK.a 或者 file libSDK.a
file libWeChatSDK.a
libWeChatSDK.a: Mach-O universal binary with 4 architectures: [i386:current ar archive] [arm_v7] [x86_64] [arm64]
libWeChatSDK.a (for architecture i386): current ar archive
libWeChatSDK.a (for architecture armv7): current ar archive
libWeChatSDK.a (for architecture x86_64): current ar archive
libWeChatSDK.a (for architecture arm64): current ar archive
或者
lipo -info libWeChatSDK.a
Architectures in the fat file: libWeChatSDK.a are: i386 armv7 x86_64 arm64
一般的靜態(tài)庫都會(huì)包含真機(jī)arm64,armv7和模擬器x86_64三種架構(gòu).
對(duì)于每種架構(gòu)要分別處理,然后再合并(為什么要分離? 因?yàn)槊糠N架構(gòu)里面都有同樣的.o文件啊,如果你不分離,不是亂套了....)
對(duì)于每種架構(gòu),例如arm64:
- 創(chuàng)建一個(gè)臨時(shí)文件夾 mkdir arm64,分離出arm64架構(gòu)
lipo -thin arm64 libWeChatSDK.a -output arm64/libWeChatSDK_arm64.a
2)解壓出object file(即.o后綴文件)
cd arm64 && ar -xv libWeChatSDK_arm64.a
輸出的結(jié)果是
.
├── .DS_Store
├── AppCommunicateData.o
├── WXApi+ExtraUrl.o
├── WXApi+HandleOpenUrl.o
├── WXApi.o
├── WXApiObject.o
├── WXLogUtil.o
├── WapAuthHandler.o
├── WeChatApiUtil.o
├── WeChatIdentityHandler.o
├── WechatAuthSDK.o
├── .SYMDEF
└── base64.o
這里的.SYMDEF 文件是符號(hào)定義,其內(nèi)容是要被要被加載的符號(hào).
- 找到?jīng)_突的.o,例如AppCommunicate.o,刪除它
rm AppCommunicate.o
其實(shí)如果我們知道要移除的是哪個(gè).o,那么可以直接使用命令
ar -d libWeChatSDK_arm64.a AppCommunicate.o
從.a中直接刪除.o,省卻了先分離,刪除,再合并!
- 重新打包object file
cd .. && ar rcs libWeChatSDK_arm64 arm64/*.o
-s表示無論ar 命令是否修改了庫內(nèi)容都強(qiáng)制重新生成庫符號(hào)表
當(dāng)然,還可以用
libtool -static -o ../libWeChatSDK_arm64.a *.o
這兩個(gè)生成的.a效果是一樣的
在當(dāng)前目錄生成了去除了AppCommunicate.o后的libWeChatSDK_arm64
這個(gè)時(shí)候,可以確認(rèn)下這個(gè)新的libWeChatSDK_arm64還有沒有那個(gè)AppCommunicate.o了,用命令
ar -t libWeChatSDK_arm64
可以看到,列表里已經(jīng)沒有AppCommunicate.o了.說明ok了
將其他幾個(gè)架構(gòu)(armv7s, x86_64,i386)等重復(fù)上面的1)-4)步驟
最后將去掉AppCommunicate.o的各種架構(gòu),合并成新的.a文件
lipo -create libWeChatSDK_arm64 libWeChatSDK_armv7.a -output libWeChatSDK-new.a
覆蓋掉項(xiàng)目中原來的文件,即可!!
當(dāng)然,這種方式也是有缺點(diǎn)的 ,你不知道這個(gè)靜態(tài)庫中重復(fù)的.o文件是不是同你項(xiàng)目中的版本兼容,如果不兼容呢,這樣去掉了,會(huì)導(dǎo)致行為的錯(cuò)誤.或者說,假如這個(gè)靜態(tài)庫中包含的重復(fù)符號(hào)所在的文件進(jìn)行了他們自定義的修改呢?這樣去掉,也是有問題的.
第二種辦法:
由于,libSDK.a中包含了AFnetworking的符號(hào),那簡單啊,直接把項(xiàng)目中用到了AFnetworking整個(gè)的重命名,也就是重命名包括類名左医、分類名宇挫、全局常量名袱蚓、協(xié)議名等會(huì)導(dǎo)致沖突的符號(hào).這也是目前很多項(xiàng)目中的做法,這種做法,修改的地方太多了,且會(huì)造成另外一種問題,就是每次如果要升級(jí)AFnetworking,那么要整個(gè)的再重新修改一次,太麻煩了,屬于典型的吃力不討好!
我們不去除libSDK.a中用到的AFnetworking,我們可以建立個(gè)預(yù)編譯的頭文件,將AFnetworking的所有符號(hào)都重新宏定義成另外的名字,這個(gè)在# iOS靜態(tài)庫開發(fā)中引入的第三方庫可能與宿主APP中沖突的解決方案
這個(gè)文章中作者有提供一個(gè)簡單腳本來生成.
當(dāng)然,除了 符號(hào)重定義+pch
還可以使用 GCC_PREPROCESSOR_DEFINITIONS 的形式
xcodebuild GCC_PREPROCESSOR_DEFINITIONS='$(inherited) PodsDummy_FLEX=PodFLEX_PodsDummy_FLEX FHSRangeSlider=PodFLEX_FHSRangeSlider FHSSnapshotNodes=PodFLEX_FHSSnapshotNodes' CONFIGURATION_BUILD_DIR=build clean build -configuration Debug -sdk iphonesimulator -arch x86_64 -target RenameLib -project RenameLib.xcodeproj
兩種方式添加都可以