iOS靜態(tài)庫和動態(tài)庫

什么是庫,使用庫有哪些好處?庫就是將代碼編譯成一個二進制文件,再加頭文件。常見的庫文件格式有.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為例,通過兩個命令filear來查看一下libAFNetworking.a,從終端打印描述可以看出它是一個文檔格式,里面是.o文件。

image.png

接下來模擬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來查看解釋,其他的就不再一一介紹
  • 生成可執(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 或者先lldbfile 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í)行文件"

將腳本放到與AFNetworkingtest.m同級,添加可執(zhí)行權(quán)限chmod +x link_dylib.sh,執(zhí)行腳本./link_dylib.sh,報錯了???

image.png

為了排除動態(tài)庫的問題,我們再使用clang命令制作一個動態(tài)庫再進行嘗試,使用之前準備的TestB.hTestB.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>

image.png

找不到符號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運行一下吧 。(用腳指頭想也知道還是會報錯。。。我就試一下~)

image.png

果不其然,依然是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)庫對比一下看看

image.png

image.png

我好像發(fā)現(xiàn)了點什么。。。正常運行的項目里面引入的動態(tài)庫指向的路徑為@rpath/AFNetworking.framework/AFNetworking,@rpath是什么暫且不管,后面的路徑看起來就是動態(tài)庫的所有路徑啊,第六感告訴我運行報錯很可能是因為libTestB.dylib的路徑有問題,查看我們的test文件目錄可以發(fā)現(xiàn)我們的test文件與libTestB.dylib并不在一個層級
image.png

大膽推測一下,如果將libTestB.dylib文件與test文件放在同一層級是否就能成功運行,懷著激動的心情將libTestB.dylib文件放到同一目錄下,然后lldb -file test,在lldb下運行
image.png

天吶,運行成功了!??!我就說嘛,哈哈,我的第六感是很準的~,也就是說我們的動態(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é)論,歡迎來懟!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容