什么是庫,使用庫有哪些好處?庫就是將代碼編譯成一個二進制文件,再加頭文件。常見的庫文件格式有.a .dylib .tbd .framework .xcframework。使用庫文件可以在不暴露源碼的情況供別人使用,開發(fā)中將一些不常修改的代碼打包成庫也可以減少編譯的時間。庫分為靜態(tài)庫和動態(tài)庫,在面試中經(jīng)常會問二者的區(qū)別,了解它們的本質(zhì)就需要親自探索一番,本文帶你一步步探索庫的本質(zhì)。有興趣的同學建議動手試驗一遍,然后再閱讀一遍,搞不明白你找我!!
靜態(tài)庫的本質(zhì)探索
靜態(tài)庫通常是以.a或.framework為后綴的庫文件,先準備一個macOS靜態(tài)庫.a文件,我就以AFNetworking為例,通過兩個命令file與ar來查看一下libAFNetworking.a,從終端打印描述可以看出它是一個文檔格式,里面是.o文件。

接下來模擬App鏈接靜態(tài)庫的過程,使用一個.o文件來鏈接libAFNetworking.a
- 創(chuàng)建一個test.m文件,然后在test.m文件中引用AFNetworking,test.m代碼:
#import <Foundation/Foundation.h>
#import <AFNetworking.h>
int main(){
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSLog(@"testApp----%@", manager);
return 0;
}
-
使用clang將test.m文件生成test.o文件,終端命令
clang -x objective-c -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I./AFNetworking -c test.m -o test.o- -isysroot是因為使用了Foundation庫,需要指定庫的位置,使用
find /Applications/Xcode.app/Contents -name SDKs可以快速進行定位到SDKs文件夾,進入文件夾再使用ls命令列出sdk名稱 - -I是指定AFNetworking頭文件位置,對應Xcode中的Header search path,在目標文件中有一個重定位符號表,它保存了當前文件里面使用的所有符號,在鏈接的時候鏈接器會根據(jù)重定位符號表再去重新定位查找具體的符號信息,因此在生成目標文件時我們只需要有一個頭文件,能夠生成重定位符號即可。
- 關(guān)于這些命令的介紹都可以通過
man clang來查看解釋,其他的就不再一一介紹
- -isysroot是因為使用了Foundation庫,需要指定庫的位置,使用
-
生成可執(zhí)行文件,終端命令
clang -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -L./AFNetworking -lAFNetworking test.o -o test- -L后面的是靜態(tài)庫的路徑,相當于Xcode配置項里面的Libarary Search Path,
- -l后面是靜態(tài)庫的名稱,這里有一個查詢規(guī)則是會按lib加上-l后面的組合在一起進行查找,因此libAFNetworking.a在這里只需要-lAFNetworking即可。
終端lldb運行測試,在終端輸入命令
lldb -file test或者先lldb后file test,進入lldb后使用r運行。可以看到在終端打印了我們test.m中輸出的語名,并且成功打印出了引用的AFNetworking庫中類創(chuàng)建的對象將test文件放到任意路徑嘗試,均可以運行打印出對象,說明最終鏈接會將所有的.o文件、靜態(tài)庫文件進行合并。
? Test lldb
(lldb) file test
Current executable set to '/Users/Peny/Desktop/Test/test' (x86_64).
(lldb) r
Process 78280 launched: '/Users/Peny/Desktop/Test/test' (x86_64)
2021-01-23 02:19:16.032613+0800 test[78280:5833698] testApp----<AFHTTPSessionManager: 0x10040f1c0, baseURL: (null), session: <__NSURLSessionLocal: 0x100410c70>, operationQueue: <NSOperationQueue: 0x10040fef0>{name = 'NSOperationQueue 0x10040fef0'}>
Process 78280 exited with status = 0 (0x00000000)
(lldb)
- 側(cè)面印證:將一個.o文件當成靜態(tài)庫來被鏈接,如果能成功說明.o等效于一個靜態(tài)庫,具體方式是創(chuàng)建一個TestB.h和TestB.m并定義一個OC方法打印日志,在test.m中調(diào)用oc方法。將TestB.m通過clang轉(zhuǎn)成.o然后使用ar命令將.o文件轉(zhuǎn)成.a靜態(tài)庫格式,完整命令
ar -rc libTestB.a TestB.o,用test.o來鏈接libTestB.a,最后通過lldb--> file test --> r,看是否能夠成功調(diào)用TestB中的方法(本人親測可以打印,注意要使用MacOS.sdk,不然無法在終端調(diào)試哦~)
通過以上測試可以總結(jié)出:
- 靜態(tài)庫的本質(zhì)就是.o文件的合集
- 要成功鏈接一個靜態(tài)庫有三要素:庫名稱、頭文件和庫文件路徑。
- 靜態(tài)庫鏈接之后所有的符號都將合并在一起,源文件就沒用了,好處就是能直接運行,但是也會使可執(zhí)行文件變大,占用__text section空間,蘋果對它目前限制500M
動態(tài)庫探索
常見的動態(tài)庫格式有.dylib、.tbd和.framework。我們采用與靜態(tài)庫相同的方式去鏈接一個AFNetworking的動態(tài)庫libAFNetworking.dylib探索嘗試,還是兩個命令,編寫腳本link_dylib.sh:
echo "編譯test.m生成test.o==="
clang -x objective-c -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I./AFNetworking -c test.m -o test.o
echo "鏈接libAFNetworking.dylib==="
clang -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -L./AFNetworking -lAFNetworking test.o -o test
echo "生成test可執(zhí)行文件"
將腳本放到與AFNetworking、test.m同級,添加可執(zhí)行權(quán)限chmod +x link_dylib.sh,執(zhí)行腳本./link_dylib.sh,報錯了???

為了排除動態(tài)庫的問題,我們再使用clang命令制作一個動態(tài)庫再進行嘗試,使用之前準備的TestB.h和TestB.m文件作成一個動態(tài)庫,將這兩個文件放到dylib文件夾中,在dylib同級目錄下編寫build_dylib.sh腳本
echo "將test.m編譯成test.o"
clang -x objective-c -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -I./dylib -c test.m -o test.o
pushd ./dylib
echo "將TestB.m編譯成TestB.o"
clang -x objective-c -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -c TestB.m -o TestB.o
echo "clang -dynamiclib編譯成動態(tài)庫"
clang -dynamiclib -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk TestB.o -o libTestB.dylib
popd
echo "鏈接libTestB.dylib生成EXEC"
clang -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -L./dylib -lTestB test.o -o test
執(zhí)行腳本可以看到test的exec文件已經(jīng)生成,再次執(zhí)行lldb -file test + r,還是報錯??!難道是動態(tài)庫生成的姿勢不對?嗯。。。再換一種方式,既然靜態(tài)庫它是一個.o文件的合集,那是不是也可以將靜態(tài)庫鏈接成為一個動態(tài)庫呢?繼續(xù)折騰~
- 使用clang命令將
TestB.m編譯成TestB.o文件 - 將
TestB.o打包成靜態(tài)庫,這次我們使用xcode官方自帶的命令libtool -static - 直接使用鏈接器
ld -dylib來鏈接 - 生成可執(zhí)行文件
步驟比較多,編寫腳本build_a_dylib.sh:
MACOS_SDK=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
echo "將test.m編譯成test.o"
clang -x objective-c -fobjc-arc -isysroot $MACOS_SDK -I./dylib -c test.m -o test.o
pushd ./dylib
echo "將TestB.m編譯成TestB.o"
clang -x objective-c -fobjc-arc -isysroot $MACOS_SDK -c TestB.m -o TestB.o
echo "libtool -static 將.o打包成靜態(tài)庫 libTestB.a"
libtool -static -arch_only x86_64 TestB.o -o libTestB.a
echo "ld -dylib 生成動態(tài)庫 libTestB.dylib"
ld -dylib -arch x86_64 \
-macosx_version_min 10.15 \
-syslibroot $MACOS_SDK \
-lsystem \
-framework Foundation \
libTestB.a -o libTestB.dylib
popd
echo "鏈接libTestB.dylib生成EXEC"
clang -fobjc-arc -isysroot $MACOS_SDK -L./dylib -lTestB test.o -o test
添加執(zhí)行權(quán)限chmod +x build_a_dylib.sh,運行,再次報錯?。。?br>

找不到符號
TestB,但是動態(tài)庫已經(jīng)生成了,說明動態(tài)庫dylib中沒有這個導出符號,可以通過查看導出符號的命令進行驗證objdump --macho --exports-trie ./dylib/libTestB.dylib,那么肯定是我們鏈接時出現(xiàn)了什么問題?使用man ld查看鏈接器,可以看到其中有幾個命令:
-all_load Loads all members of static archive libraries.
-ObjC Loads all members of static archive libraries that implement an Objective-C class or category.
-force_load path_to_archive
Loads all members of the specified static archive library. Note: -all_load forces all members of all archives to be loaded. This option allows you to target a specific archive.
-noall_load This is the default. This option is obsolete.
-noall_load為鏈接器的一個默認值,猜測當我們從靜態(tài)庫生成動態(tài)庫的時候,它的符號因為沒有被外部使用而被脫掉了才出現(xiàn)上面的錯誤信息,修改ld命令添加上-all_load或者-ObjC來再試一下,果然完美運行,再次使用objdump查看dylib的導出符號,赫然在列!再來lldb運行一下吧 。(用腳指頭想也知道還是會報錯。。。我就試一下~)

果不其然,依然是
Libarary not loaded and image not found,實在是煩?。?!不過也并非一無所獲,至少可以看出動態(tài)庫與靜態(tài)庫的區(qū)別,靜態(tài)庫只是一個.o文件的集合,而動態(tài)庫它比靜態(tài)庫多了一個鏈接的步驟,它是一個最終鏈接產(chǎn)物,也就是說動態(tài)庫它不能再進行合并。
探索到了這里,顯然還沒有結(jié)束,明明已經(jīng)編譯鏈接成功,為什么在運行時會出現(xiàn)這個錯誤?接下來我們借助一個工具MachOView來查看可執(zhí)行文件test,能報這個庫找不到說明Load Commands中肯定是有這個LC_LOAD_DYLIB,它是不是有什么問題呢?與我們使用cocoapods引入的動態(tài)庫對比一下看看


我好像發(fā)現(xiàn)了點什么。。。正常運行的項目里面引入的動態(tài)庫指向的路徑為
@rpath/AFNetworking.framework/AFNetworking,@rpath是什么暫且不管,后面的路徑看起來就是動態(tài)庫的所有路徑啊,第六感告訴我運行報錯很可能是因為libTestB.dylib的路徑有問題,查看我們的test文件目錄可以發(fā)現(xiàn)我們的test文件與libTestB.dylib并不在一個層級
大膽推測一下,如果將
libTestB.dylib文件與test文件放在同一層級是否就能成功運行,懷著激動的心情將libTestB.dylib文件放到同一目錄下,然后lldb -file test,在lldb下運行
天吶,運行成功了!??!我就說嘛,哈哈,我的第六感是很準的~,也就是說我們的動態(tài)庫在鏈接成功之后,其符號仍然保存在庫中,在運行時可執(zhí)行文件會按照LC_LOAD_DYLIB中的路徑去查找動態(tài)庫中的符號。所以在我們開發(fā)中如果再遇到某某動態(tài)庫Libarary not loaded時,十有八九就是路徑的問題了。
終于搞清楚了之后還要解決引用路徑的問題,這里不再碼字說明,僅給出研究方向,研究@rpath、@executable_path、@load_path這三者究竟是如何使用的,對于這三者理解之后可以幫助我們解決一些動態(tài)庫的鏈接依賴問題,這里給出一些簡單的介紹,有興趣的同學可以自行探索。
-
@rpath比較靈活,它是一個變量,一個占位,誰使用誰提供它的值,鏈接器可以通過install_name_tool的-add_rpath設置值,可以有多個值 -
@executable_path代表可以執(zhí)行程序所在的路徑。 -
@loader_path表示加載它的Mach-O所在的目錄,由它的上層來決定,用它可以解決動態(tài)庫依賴動態(tài)庫的復雜場景。
總結(jié)動態(tài)庫的特點:
- 動態(tài)庫與靜態(tài)庫相比多了一步鏈接,它是鏈接的最終產(chǎn)物,動態(tài)庫不能再次進行合并
- 動態(tài)庫在編譯鏈接之后并沒有將所有符號合并,在運行時會根據(jù)
LC_LOAD_DYLIB中的路徑去動態(tài)加載
再次建議:要想搞清楚原理一定要親自動手嘗試,不要輕信他人給出的結(jié)論,歡迎來懟!