duplicate symbol /undefind symbol出現的原因
前言:
作為一個iOS開發,相信大家都會遇到類似於 “duplicate symbol” 的程式報錯。 對於很多新手來說,可能會有點手足無措,因為這種型別的報錯一般並非是程式碼的邏輯錯誤,大部分情況下是在編譯過程出錯導致的,因此相對來說排查不易。在前幾天,我在引用了兩個SDK的過程中出現了這個問題,運用不同的手段最終解決了這個問題。今天本文就這個錯誤進行一個詳細的分析以及如何處理做一個探討,如果有錯誤的地方還請指出。
一、duplicate symbol /undefind symbol出現的原因
duplicate symbol錯誤原因
這種錯誤的原因多種多樣,解決方式因此也並不固定,以下幾種情況都會導致高錯誤的出現。
- 誤將.m檔案引入為標頭檔案。
- 同一個工程中含有同名的檔案。(一般在多人開發過程容易出現)
- 引入的第三方框架包涵了與本地同名的檔案
- 第三方框架的.o檔案同名
- 引用了不同庫檔案中含有相同的方法名
undefind symbol錯誤原因
-
- 相關的.a檔案沒有加入。
- 相關的.a檔案中缺少對應的方法。
原因分析:
雖然這麼多種類的錯誤原因看似複雜,實際上他們的根源是一樣的。 前面說了這是在編譯的階段的錯誤,我們就從編譯器開始分析。
Xcode3所使用的編譯器是gcc編譯器,而在Xcode4之後和版本中,Xcode所使用的編譯器已經替換成為新的LLVM編譯器。LLVM編譯器的前端是clang,當然就算是現在,Xcode還是支援GCC的,不過可能需要手動下載。編譯器的流程大概是下面的圖的樣子,不過原理大致一樣,主要是有於理解,在網上看到的講解(
預處理: 預處理相當於根據預處理命令組裝成新的C程式,不過常以i為副檔名。
編譯: 將得到的i檔案翻譯成彙編程式碼。s檔案。
彙編: 將彙編檔案翻譯成機器指令,並打包成可重定位目標程式的O檔案。該檔案是二進位制檔案,位元組編碼是機器指令。
連結: 將引用的其他O檔案併入到我們程式所在的o檔案中,處理得到最終的可執行檔案。
雖然這是針對C程式的講解,但是作為C大家族的OC也是類似。.s檔案是彙編檔案,貌似Xcode的clang是沒有這一步的,而是直接到了.o檔案。*.o檔案原來就是我們說的目標檔案,是二進位制的,在很多第三方庫中,我們一般會看到.a或者 .framework 或者直接XXSDK, 其實這些檔案中包涵的都是.o檔案。 不信你可以隨便找一個.a檔案,然後控制檯對其進行 ar -x *.a 指令, 會解壓出很多.o檔案, 這些都是製作者寫的.m檔案經過編譯得到的,經過打包成了.a檔案。 .o檔案最終會被用來連結,才可以變成一個可執行的檔案,比如iPA檔案就是啦。這裡要好好弄清楚連結過程了。
“
連結器 (linker) 將一個個的目標檔案 ( 或許還會有若干程式庫 ) 連結在一起生成一個完整的可執行檔案。
在符號解析 (symbol resolution) 階段,連結器按照所有目標檔案和庫檔案出現在命令列中的順序從左至右依次掃描它們,在此期間它要維護若干個集合 :
(1) 集合 E 是將被合併到一起組成可執行檔案的所有目標檔案集合;
(2) 集合 U 是未解析符號 (unresolved symbols ,比如已經被引用但是還未被定義的符號 ) 的集合;
(3) 集合 D 是所有之前已被加入到 E 的目標檔案定義的符號集合。一開始, E 、 U 、 D 都是空的。
連結器的工作過程:
(1): 對命令列中的每一個輸入檔案 f ,連結器確定它是目標檔案還是庫檔案,如果它是目標檔案,就把 f 加入到 E ,並把 f 中未解析的符號和已定義的符號分別加入到 U 、 D 集合中,然後處理下一個輸入檔案。
(2): 如果 f 是一個庫檔案,連結器會嘗試把 U 中的所有未解析符號與 f 中各目標模組定義的符號進行匹配。如果某個目標模組 m 定義了一個 U 中的未解析符號,那麼就把 m 加入到E 中,並把 m 中未解析的符號和已定義的符號分別加入到 U 、 D 集合中。不斷地對 f 中的所有目標模組重複這個過程直至到達一個不動點 (fixed point) ,此時 U 和 D 不再變化。而那些未加入到 E 中的 f 裡的目標模組就被簡單地丟棄,連結器繼續處理下一輸入檔案。
(3): 如果處理過程中往 D 加入一個已存在的符號 ,或者當掃描完所有輸入檔案時 U 非空,連結器報錯並停止動作。否則,它把 E 中的所有目標檔案合併在一起生成可執行檔案。
”
文中看到 “符號”這個字眼,是不是很熟悉(看本文標題),報錯指的就是symbol這個東西,它實際上將我們在程式中的全域性變數名
、函式名
或類名
,通過OC的訊息傳送方式,我們大概也聯想到了,OC中通過函式名來識別函式進行相應的處理。上文中我門看到有三個集合,D集合就是合法的目標程式的符號集合,也即是我門所說的.o中的全域性變數名
、函式名
或類名
都在裡面了。我門自己也可以檢視這個表格的,方法是在控制檯
對.o檔案進行
1 |
nm
*.o >> symbols.txt
|
比如我這裡獲取到了cJSON中的符號列表是這樣的
最終會生成*.o檔案中所有的函式名儲存在symbols.txt中。 在編譯器連結的過程中,每當獲取到一個符號名的時候,都會將制放入表格,如果有同名的則會報錯。
現在知道為什麼程式報錯了嗎?
分析結果:
-
- 誤將.m檔案引入為標頭檔案。 : 當引入了.m檔案,說明同時也引入了.m檔案中的.h檔案,編譯器不管那麼多,所有的函式名照單全收,於是重名了,報錯!
- 同一個工程中含有同名的檔案。(一般在多人開發過程容易出現)
- 引入的第三方框架包涵了與本地同名的檔案: 框架中的同名檔案中,一般都是開源的,裡面含有相同的符號名可以理解了
- 第三方框架的.o檔案同名 同上
- 引用了不同庫檔案中含有相同的方法名 同上
其實這個錯誤說白了就是定義相同的函式名。
-
- 相關的.a檔案沒有加入。 連結列表中沒有的符號名 被使用 報錯
- 相關的.a檔案中缺少對應的方法。 連結列表中沒有的函式名 被使用 報錯
二、duplicate symbol /undefind symbol錯誤的解決辦法
現在我門知道原因了,那就開始著手解決了。
duplicate symbol解決辦法
1.前面三個原因屬於專案中的因素,就相對簡單了,如果是.m 檔案引用錯誤,改為h檔案即可。工程中有同名檔案,要不刪除,要不改名字。有相通的函式名,要不刪除,要不修改。搞定!
2.後兩個原因是引用的庫檔案問題,解決稍微麻煩。
如果是因為第三方框架的引入導致duplicate symbol 錯誤,那就好好看看提示,一般都會有提示告知,在某個地方含有相同的函式名(下圖這樣的)。
這裡要分下情況,如果是使用了相同的庫檔案,那直接解開對應的SDK,將相同的半部分刪除即可。操作流程如下:
一般的sdk都會介入不同的架構用於不同的平臺上的編譯,比如如果想在模擬器上編譯就需要相容 i386架構, 真機需要相容armv7架構等。我門先要將不同的架構的檔案分離出來。 上控制檯console 指令 檢視庫檔案支援的架構:
$ lipo -info FunSDK
打印出:
1 |
/Users/JDiOS/Desktop/嘉德/jadeApp2/jadeApp2/FunSDK.framework/Versions/A/FunSDK
are: armv7 armv7s i386 x86_64 arm64
|
說明該靜態庫支援的是 armv7 armv7s i386 x86_64 arm64 5個不同的架構。
如果我們要修改的話,需要全部剝離,下面只挑選 armv7架構進行演示:
將armv7解壓出來lipo FunSDK -thin armv7 -output Funrmv7.a
成功之後 可以看一下檔案大小的對比,大概是沒有分離之前的1/5。 說明靜態庫.a檔案其實只是一個壓縮包,將多個架構的檔案壓縮在一起了。
之前說了.a檔案其實是很多.o檔案的集合,我門要刪除某個.o檔案還要再進行分解,這裡要提醒一下,先建立好一個資料夾,將獲得的armv7架構庫檔案放進資料夾,在資料夾中處理,不然 .o檔案太多,不好管理,丟失了就不好,因為處理完我門還要打包回去。指令:
1 |
ar
-x Funrmv7.a
|
可以看到很多 .o檔案 。
這時候找到找到要刪除的.o 將之刪除即可。 也可以指令
1 |
rm
cJSON.o //刪除的是
cJSON.o 檔案
|
刪除完畢之後,就該合併回去了。一步一步來,想將所有得.o檔案打包回去
1 |
libtool
- static -o
../ new -armv7.a
*.o //將所有的o檔案打包成new-armv7.a
|
剛才之將armv7一個架構的檔案處理完,要完全做完,其它的架構也要處理。 最終 獲得所有的架構的檔案合併成靜態庫 指令
lipo -create -output FunSDKnew armv76.a i386.a arm64.a armv7s.a x86_64.a
最後將FunSDKnew改名 FunSDK(你的專案中的原來的SDK的名字) 覆蓋原來專案中的SDK即可。
注意點:
如果是名字相同的當然可以這麼處理, 可是,我門已經分析過,.o檔案衝突的直接原因不是檔案本身,而是檔案中包含的符號。如果SDK對cJSON檔案的內容進了更改,那麼剛才的方法雖然可以避免符號衝突,但是,整個檔案被刪除,修改的內容也被刪了,最終可能導致SDK部分功能沒了, 這當然不是我們想看到的。實際上,我遇上的就是這個問題, 最後編譯過程中,因為直接刪除了FunSDK中的檔案,導致符號缺失 ,像這樣:
是不是看到什麼了,沒錯,也是本文說的一個點 undefind symbol 錯誤。
原來雖然cJSON是開源的,但是FunSDK 在開源的基礎上做了修改,直接刪除cJSON等於把FunSDK新增的setStringValue的函式給刪掉了,預編譯器又報錯! 怎麼辦?
請看:
undefind symbol 解決辦法
兩個辦法:
- 找SDK供應方 要.o檔案的原始碼,修改之後打包回去 (呵呵,原始碼有點難啊)。 最好還是跟供應商協商修改。
- 一個取巧的辦法 (這是我無意中發現的) 一般SDK有衝突的檔案大部分原因是因為打包相同的開原始檔,去網上都可down的到,下載過來,將裡面的缺失的方法(符號)新增,放到專案中,這樣可以覆蓋庫中的方法。 但是有一定的風險,運氣好可能可以避免,因為你的專案中不一定會用到SDK中所有的方法,恰巧你庫中缺失的方法就是不需要用到的,那麼只需要寫入個方法,不需要實現什麼,編譯過程可以通過。 解決!
- 缺失庫檔案嘛,看看是不是target -> bulid Phases 中加入對應的.a 檔案(主要還是這個原因吧)
三、其它的方法 解決duplicate symbol問題
是不是每次出現這種問題都需要進行上上述的方案來解決呢? 不一定。
我門已經知道這是在編譯連結的時候出現的錯誤,Xcode在連結的時候,提供了一個可選的連結方式。
專案中: target -> bulid setting -> link -> oteher link flags
這裡可以使用的庫連結方式有: -all_load -Objc -force_load -dead_strip 等等。
為了解決duplicate symbol的問題,以下連結方式可以使用:
- 設定-dead_strip之後將會是專案忽略掉重複的符號, 最終編譯會通過的,但是如果就像之前所說的,忽略的東西如果被修改過,有部分可能導致SDK的部分API不能使用。
- -force_load也可以通過編譯,這種方式是強制連結靜態庫。 需要指定連結的庫檔案
在大部分情況下,使用-dead_strip 和 -force_load連結方式是可行的,畢竟比上面的方法要輕鬆無數倍不是嗎?如果沒什麼特別的要求,建議使用這個方式。
附一張連結方式的含義。