什么是可執行文件?
要理解靜態庫我們就得清楚最終可執行文件(.out)的生成過程了
當我們寫的源代碼 hello.c 經過上述4個步驟:預處理(Prepressing)、編譯(Compilation)、匯編(Assembly)和鏈接(Linking)后,就生成了我們的可執行文件 a.out 了。
注意:可重定位目標文件是(.o) 而 可執行文件是 (.out),以下文章描述都遵循這種叫法。
當我們明白到什么是可執行文件后,那么再來看看究竟什么是靜態庫?
靜態庫定義:
其實一個靜態庫可以簡單地看成一組目標文件(.o)的集合,即很多目標文件經過壓縮打包后形成的一個文件。
比如我們在Linux中最常用的C語言靜態庫libc位于 /usr/lib/libc.a,它屬于 glibc 項目的一部分。而 glibc 本身是用C語言開發的,它由成百上千個C語言源代碼文件組成,也就是說,編譯完成以后有相同數量的目標文件,比如輸入輸出有 printf.o,scanf.o;文件操作有fread.o,fwrite.o;時間日期有date.o,time.o;內存管理有malloc.o等。把這些零散的目標文件直接提供給庫的使用者,很大程度上會造成文件傳輸、管理和組織方面的不便,于是通常人們使用 ar 壓縮程序將這些目標文件壓縮到一起,并且對其進行編號和索引,以便于查找和檢索,就形成了libc.a這個靜態庫文件。
例如當我們使用如下代碼的時候
#include<stdio.h>
int main(int argc, const char* argv[] )
{
printf("Hello World");
}
我們先用編譯器和匯編器將上述代碼生成目標文件 hello.o。
$gcc -c hello.c
示例代碼用到了 printf ()
這個iO函數,而該函數的符號就位于 printf.o
中,所以我們就需要讓鏈接器鏈接 printf.o
,當然printf.o
也會依賴其他目標文件,而 printf.o
等文件又在 靜態庫 libc.a 當中,
我們就需要讓鏈接器 (ld) 靜態鏈接到靜態庫 libc.a 中去尋找示例代碼中需要用到的目標文件,并生成目標文件 hello.out。
$ ld -static -e main hello.o /usr/lib/libc.a -o hello.out
我們今天的主題主要在于鏈接與靜態庫這一步中
Xcode 中配置鏈接器(other linker flags)
other linker flags 是 xcode 這個集成開發環境所特有的,目的是讓連接器器 ld 除了默認參數外再根添加額外參數進行鏈接工作。
Object-C 鏈接特性:
The "selector not recognized" runtime exception occurs due to an issue between the implementation of standard UNIX static libraries, the linker and the dynamic nature of Objective-C. Objective-C does not define linker symbols for each function (or method, in Objective-C) - instead, linker symbols are only generated for each class. If you extend a pre-existing class with categories, the linker does not know to associate the object code of the core class implementation and the category implementation. This prevents objects created in the resulting application from responding to a selector that is defined in the category.
Object-C的鏈接器并不會為每個方法建立符號表,而是為每個類建立鏈接符號。這樣的話靜態庫中定義了已存在的類的分類,鏈接器就以為這個類存在了,不會將分類和核心類代碼關聯(合并)起來,這樣在最后可執行文件中,就會找不到分類里所定義的方法。
例如如下錯誤:
就看 log 可以看出,是 NSString 的一個分類方法 designByOhterLinker
找不到實現了,而這個方法,確實是一個靜態庫里面的一個分類方法。
如何解決這個問題?
三個Linker 參數:
- -ObjC
- -all_load
- -force_load
- -dead_strip (8.27日更新)
-ObjC :
This flag causes the linker to load every object file in the library that defines an Objective-C class or category. While this option will typically result in a larger executable (due to additional object code loaded into the application), it will allow the successful creation of effective Objective-C static libraries that contain categories on existing classes.
加入這個參數后,鏈接器會將靜態庫中的每個類和分類加載到最后的可執行文件,當然,這個參數會導致可執行文件比較大,原因是加載了更多的額外對象的代碼到可執行文件當中去,但是這會解決 Objec-C 中靜態庫中已存在的類包含的分類問題。
上面說得很清楚,-ObjC 會解決靜態庫中已存在的類的分類問題,那么,如果分類存在與靜態庫,但是類并不在靜態庫的這種情況,該怎么辦呢?
Important: For 64-bit and iPhone OS applications, there is a linker bug that prevents -ObjC from loading objects files from static libraries that contain only categories and no classes. The workaround is to use the -allload or -forceload flags.
說得很清楚,使用-all_load 或 -force_load 就可以解決上述問題。
-all_load:
該參數把所找到的目標文件都加載到可執行文件當中去,但是這就存在一個問題了,如果兩個靜態庫中,都使用了同一份可重定位的目標文件(這是一個很常見的問題,例如大家的目標文件都使用了用以名字 base64.o)就會發生 ld: duplicate symbol 符號沖突問題,所以不太建議使用。
-force_load:
該參數的作用跟 -all_load 其實是一樣的,但是 -force_load 需要指定要進行全部加載的庫文件的路徑,這樣的話,只要完全加載一個庫文件,不影響其余庫的可重定位目標文件的按需加載。
但是也有一種最頭痛,就是當兩個靜態庫中使用了相同的目標文件
上圖的兩個上圖的兩個 libMyOtherStaticLibrary.a 和 libMyStaticLibrary 中的 MyClass.o 類發生了沖突
那么,這個時候有兩種解決方法:
1、利用 -force_load 讓鏈接器指定編譯把其中一個靜態庫的目標文件,不加載另一個靜態庫的重復目標文件
具體做法:
但是這么做有一個弊端,如果這兩個靜態庫同時都使用到了分類(基本上都會使用吧)那么如果只讓編譯器加載其中一個靜態庫的目標文件 (-force_load),而不將另一個靜態庫中的分類合并加載到目標文件的話,也是會導致運行的時候導致上述的崩潰問題。但是如果 -foce_load 兩個靜態庫,又會有符號沖突,那么,怎么辦呢?
2、簡單來說就是去除某個靜態庫中的重復目標文件,然后再打包
具體做法:
1)通過使用壓縮工具命令 ar -t 去查看兩個靜態庫文件里的目標文件那些存在沖突
如下:
很明顯就是 MyClass.o 這個目標文件發生符號沖突了, 其實不這樣看也行,反正編譯的時候 Clang 編譯器就就會有符號沖突的報錯,上圖 Xcode 報錯的那個圖就是很好的例子,可以看錯那些目標文件重復了。
2)將其中一個靜態庫中的重復目標文件去掉,然后再次打包成靜態庫使用
- 首先利用 lipo 命令將其中一個iOS靜態庫的文件解壓出來(因為iOS的靜態庫文件是一個將不同 CPU 架構靜態庫合并的一個打包文件)。
可以看出 libMyOtherStaticLibrary.a 中包含了 armv7 跟 arm64 兩種架構的靜態庫文件
- 分別將兩種不同架構的靜態庫文件提取出來
- 使用 ar 壓縮工具分別將這兩個不同架構的靜態庫文件與另一個發生沖突的靜態庫文件中的目標文件剔除出去。
通過上面命令看出已成功將 MyClass.o 剔除出靜態庫
- 利用 lipo 將兩個不同架構的靜態庫重新打包封裝成 iOS 的靜態庫文件
然后在 libMyOtherStaticLibraryOut.a 這個靜態庫重新放到工程當中去替換原來的 libMyOtherStaticLibrary.a
Other linker flags 只需用 -ObjC 就可以了
編譯,Successful!
運行,完美!
-dead_strip (2017.8.27 更新)
參數的作用在于解決我們上面可重定位目標文件(.o)中類符號的沖突問題,如果發生了這種情況,使用該參數就是一個非??旖莸霓k法了,讓 Clang 編譯器幫助我們去除重復符號的可重定位目標文件問題。
但是使用這個參數卻有一個問題,就是如果我們使用了該參數,就不能使用 -all_load 或 -force_load,因為,如果我們指定了讓編譯器幫我們決定哪些目標文件該被鏈接,哪些不被鏈接(-dead_strip),那么我們就不能手動的強制地讓所有目標文件都進行鏈接了(-all_load 或 -force_load)。如果是這樣的話,我們又回到最初的問題了-ObjC 會解決靜態庫中已存在的類的分類問題,那么,如果分類存在與靜態庫,但是類并不在靜態庫的這種情況,該怎么辦呢?
顯然,對于這種情況,還是得使用上面的方法