什么是可執(zhí)行文件?
要理解靜態(tài)庫我們就得清楚最終可執(zhí)行文件(.out)的生成過程了
當(dāng)我們寫的源代碼 hello.c 經(jīng)過上述4個(gè)步驟:預(yù)處理(Prepressing)旦签、編譯(Compilation)揩局、匯編(Assembly)和鏈接(Linking)后,就生成了我們的可執(zhí)行文件 a.out 了雕凹。
注意:可重定位目標(biāo)文件是(.o) 而 可執(zhí)行文件是 (.out)摧找,以下文章描述都遵循這種叫法无蜂。
當(dāng)我們明白到什么是可執(zhí)行文件后紊服,那么再來看看究竟什么是靜態(tài)庫叽躯?
靜態(tài)庫定義:
其實(shí)一個(gè)靜態(tài)庫可以簡單地看成一組目標(biāo)文件(.o)的集合柜候,即很多目標(biāo)文件經(jīng)過壓縮打包后形成的一個(gè)文件锦溪。
比如我們在Linux中最常用的C語言靜態(tài)庫libc位于 /usr/lib/libc.a累驮,它屬于 glibc 項(xiàng)目的一部分。而 glibc 本身是用C語言開發(fā)的煎饼,它由成百上千個(gè)C語言源代碼文件組成讹挎,也就是說,編譯完成以后有相同數(shù)量的目標(biāo)文件腺占,比如輸入輸出有 printf.o淤袜,scanf.o;文件操作有fread.o衰伯,fwrite.o;時(shí)間日期有date.o积蔚,time.o意鲸;內(nèi)存管理有malloc.o等。把這些零散的目標(biāo)文件直接提供給庫的使用者尽爆,很大程度上會(huì)造成文件傳輸怎顾、管理和組織方面的不便,于是通常人們使用 ar 壓縮程序?qū)⑦@些目標(biāo)文件壓縮到一起漱贱,并且對(duì)其進(jìn)行編號(hào)和索引槐雾,以便于查找和檢索,就形成了libc.a這個(gè)靜態(tài)庫文件幅狮。
例如當(dāng)我們使用如下代碼的時(shí)候
#include<stdio.h>
int main(int argc, const char* argv[] )
{
printf("Hello World");
}
我們先用編譯器和匯編器將上述代碼生成目標(biāo)文件 hello.o募强。
$gcc -c hello.c
示例代碼用到了 printf ()
這個(gè)iO函數(shù)株灸,而該函數(shù)的符號(hào)就位于 printf.o
中,所以我們就需要讓鏈接器鏈接 printf.o
擎值,當(dāng)然printf.o
也會(huì)依賴其他目標(biāo)文件慌烧,而 printf.o
等文件又在 靜態(tài)庫 libc.a 當(dāng)中,
我們就需要讓鏈接器 (ld) 靜態(tài)鏈接到靜態(tài)庫 libc.a 中去尋找示例代碼中需要用到的目標(biāo)文件鸠儿,并生成目標(biāo)文件 hello.out屹蚊。
$ ld -static -e main hello.o /usr/lib/libc.a -o hello.out
我們今天的主題主要在于鏈接與靜態(tài)庫這一步中
Xcode 中配置鏈接器(other linker flags)
other linker flags 是 xcode 這個(gè)集成開發(fā)環(huán)境所特有的,目的是讓連接器器 ld 除了默認(rèn)參數(shù)外再根添加額外參數(shù)進(jìn)行鏈接工作进每。
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的鏈接器并不會(huì)為每個(gè)方法建立符號(hào)表汹粤,而是為每個(gè)類建立鏈接符號(hào)。這樣的話靜態(tài)庫中定義了已存在的類的分類田晚,鏈接器就以為這個(gè)類存在了嘱兼,不會(huì)將分類和核心類代碼關(guān)聯(lián)(合并)起來,這樣在最后可執(zhí)行文件中肉瓦,就會(huì)找不到分類里所定義的方法遭京。
例如如下錯(cuò)誤:
就看 log 可以看出,是 NSString 的一個(gè)分類方法 designByOhterLinker
找不到實(shí)現(xiàn)了泞莉,而這個(gè)方法哪雕,確實(shí)是一個(gè)靜態(tài)庫里面的一個(gè)分類方法。
如何解決這個(gè)問題鲫趁?
三個(gè)Linker 參數(shù):
- -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.
加入這個(gè)參數(shù)后斯嚎,鏈接器會(huì)將靜態(tài)庫中的每個(gè)類和分類加載到最后的可執(zhí)行文件,當(dāng)然挨厚,這個(gè)參數(shù)會(huì)導(dǎo)致可執(zhí)行文件比較大堡僻,原因是加載了更多的額外對(duì)象的代碼到可執(zhí)行文件當(dāng)中去,但是這會(huì)解決 Objec-C 中靜態(tài)庫中已存在的類包含的分類問題疫剃。
上面說得很清楚钉疫,-ObjC 會(huì)解決靜態(tài)庫中已存在的類的分類問題,那么巢价,如果分類存在與靜態(tài)庫牲阁,但是類并不在靜態(tài)庫的這種情況,該怎么辦呢壤躲?
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:
該參數(shù)把所找到的目標(biāo)文件都加載到可執(zhí)行文件當(dāng)中去碉克,但是這就存在一個(gè)問題了凌唬,如果兩個(gè)靜態(tài)庫中,都使用了同一份可重定位的目標(biāo)文件(這是一個(gè)很常見的問題漏麦,例如大家的目標(biāo)文件都使用了用以名字 base64.o)就會(huì)發(fā)生 ld: duplicate symbol 符號(hào)沖突問題客税,所以不太建議使用况褪。
-force_load:
該參數(shù)的作用跟 -all_load 其實(shí)是一樣的,但是 -force_load 需要指定要進(jìn)行全部加載的庫文件的路徑霎挟,這樣的話窝剖,只要完全加載一個(gè)庫文件,不影響其余庫的可重定位目標(biāo)文件的按需加載酥夭。
但是也有一種最頭痛赐纱,就是當(dāng)兩個(gè)靜態(tài)庫中使用了相同的目標(biāo)文件
上圖的兩個(gè)上圖的兩個(gè) libMyOtherStaticLibrary.a 和 libMyStaticLibrary 中的 MyClass.o 類發(fā)生了沖突
那么,這個(gè)時(shí)候有兩種解決方法:
1熬北、利用 -force_load 讓鏈接器指定編譯把其中一個(gè)靜態(tài)庫的目標(biāo)文件疙描,不加載另一個(gè)靜態(tài)庫的重復(fù)目標(biāo)文件
具體做法:
但是這么做有一個(gè)弊端,如果這兩個(gè)靜態(tài)庫同時(shí)都使用到了分類(基本上都會(huì)使用吧)那么如果只讓編譯器加載其中一個(gè)靜態(tài)庫的目標(biāo)文件 (-force_load)讶隐,而不將另一個(gè)靜態(tài)庫中的分類合并加載到目標(biāo)文件的話起胰,也是會(huì)導(dǎo)致運(yùn)行的時(shí)候?qū)е律鲜龅谋罎栴}。但是如果 -foce_load 兩個(gè)靜態(tài)庫巫延,又會(huì)有符號(hào)沖突效五,那么,怎么辦呢炉峰?
2畏妖、簡單來說就是去除某個(gè)靜態(tài)庫中的重復(fù)目標(biāo)文件,然后再打包
具體做法:
1)通過使用壓縮工具命令 ar -t 去查看兩個(gè)靜態(tài)庫文件里的目標(biāo)文件那些存在沖突
如下:
很明顯就是 MyClass.o 這個(gè)目標(biāo)文件發(fā)生符號(hào)沖突了疼阔, 其實(shí)不這樣看也行戒劫,反正編譯的時(shí)候 Clang 編譯器就就會(huì)有符號(hào)沖突的報(bào)錯(cuò),上圖 Xcode 報(bào)錯(cuò)的那個(gè)圖就是很好的例子婆廊,可以看錯(cuò)那些目標(biāo)文件重復(fù)了迅细。
2)將其中一個(gè)靜態(tài)庫中的重復(fù)目標(biāo)文件去掉,然后再次打包成靜態(tài)庫使用
- 首先利用 lipo 命令將其中一個(gè)iOS靜態(tài)庫的文件解壓出來(因?yàn)閕OS的靜態(tài)庫文件是一個(gè)將不同 CPU 架構(gòu)靜態(tài)庫合并的一個(gè)打包文件)淘邻。
可以看出 libMyOtherStaticLibrary.a 中包含了 armv7 跟 arm64 兩種架構(gòu)的靜態(tài)庫文件
- 分別將兩種不同架構(gòu)的靜態(tài)庫文件提取出來
- 使用 ar 壓縮工具分別將這兩個(gè)不同架構(gòu)的靜態(tài)庫文件與另一個(gè)發(fā)生沖突的靜態(tài)庫文件中的目標(biāo)文件剔除出去茵典。
通過上面命令看出已成功將 MyClass.o 剔除出靜態(tài)庫
- 利用 lipo 將兩個(gè)不同架構(gòu)的靜態(tài)庫重新打包封裝成 iOS 的靜態(tài)庫文件
然后在 libMyOtherStaticLibraryOut.a 這個(gè)靜態(tài)庫重新放到工程當(dāng)中去替換原來的 libMyOtherStaticLibrary.a
Other linker flags 只需用 -ObjC 就可以了
編譯,Successful!
運(yùn)行宾舅,完美敬尺!
-dead_strip (2017.8.27 更新)
參數(shù)的作用在于解決我們上面可重定位目標(biāo)文件(.o)中類符號(hào)的沖突問題,如果發(fā)生了這種情況贴浙,使用該參數(shù)就是一個(gè)非常快捷的辦法了署恍,讓 Clang 編譯器幫助我們?nèi)コ貜?fù)符號(hào)的可重定位目標(biāo)文件問題崎溃。
但是使用這個(gè)參數(shù)卻有一個(gè)問題,就是如果我們使用了該參數(shù)盯质,就不能使用 -all_load 或 -force_load袁串,因?yàn)楦哦绻覀冎付俗尵幾g器幫我們決定哪些目標(biāo)文件該被鏈接,哪些不被鏈接(-dead_strip)囱修,那么我們就不能手動(dòng)的強(qiáng)制地讓所有目標(biāo)文件都進(jìn)行鏈接了(-all_load 或 -force_load)赎瑰。如果是這樣的話,我們又回到最初的問題了-ObjC 會(huì)解決靜態(tài)庫中已存在的類的分類問題破镰,那么餐曼,如果分類存在與靜態(tài)庫,但是類并不在靜態(tài)庫的這種情況鲜漩,該怎么辦呢源譬?
顯然,對(duì)于這種情況孕似,還是得使用上面的方法