前面介紹過制作過程掠械,這里不講如何制作動態(tài)庫由缆、靜態(tài)庫注祖。
靜態(tài)庫和動態(tài)庫都是以二進制提供代碼復(fù)用的代碼庫。
- 靜態(tài)庫常見的是 .a
- 動態(tài)庫(共享庫)常見的是 Windows 下的 .dll均唉,Linux 下的 .so是晨,Mac 下的 .dylib/.tbd。
特別注意平時我們經(jīng)常說的Framework(in Apple) 是Cocoa/Cocoa Touch程序中使用的一種資源打包方式舔箭,可以將代碼文件罩缴、頭文件、資源文件层扶、說明文檔等集中在一起箫章,方便開發(fā)者使用。也就是說我們的 Framework其實是資源打包的方式镜会,和靜態(tài)庫動態(tài)庫的本質(zhì)是沒有什么關(guān)系炉抒。
靜態(tài)庫和動態(tài)庫的區(qū)別
首先來看什么是庫,庫(Library)說白了就是一段編譯好的二進制代碼稚叹,加上頭文件就可以供別人使用。我們在和別人合作的時候拿诸,一種情況是某些代碼需要給別人使用扒袖,但是我們不希望別人看到源碼,就需要以庫的形式進行封裝亩码,只暴露出頭文件季率。另外一種情況是,對于某些不會進行大的改動的代碼描沟,我們想減少編譯的時間飒泻,就可以把它打包成庫,因為庫是已經(jīng)編譯好的二進制了吏廉,編譯的時候只需要 Link 一下泞遗,不會浪費編譯時間。
- 靜態(tài)庫:鏈接時會被完整的復(fù)制到可執(zhí)行文件中席覆,所以如果兩個程序都用了某個靜態(tài)庫史辙,那么每個二進制可執(zhí)行文件里面其實都含有這份靜態(tài)庫的代碼。
- 動態(tài)庫: 鏈接時不復(fù)制佩伤,在程序啟動后用動態(tài)加載聊倔,然后再決議符號,所以理論上動態(tài)庫只用存在一份生巡,好多個程序都可以動態(tài)鏈接到這個動態(tài)庫上面耙蔑,達(dá)到了節(jié)省內(nèi)存(不是磁盤是內(nèi)存中只有一份動態(tài)庫),還有另外一個好處孤荣,由于動態(tài)庫并不綁定到可執(zhí)行程序上甸陌,所以我們想升級這個動態(tài)庫就很容易须揣,windows和linux上面一般插件和模塊機制都是這樣實現(xiàn)的。
動態(tài)庫和靜態(tài)庫都是由*.o
目標(biāo)文件生成的邀层。
對比一下靜態(tài)和動態(tài)庫的優(yōu)缺點
庫類型 | 優(yōu)點 | 缺點 |
---|---|---|
靜態(tài)庫 | 1. 目標(biāo)程序沒有外部依賴返敬,直接就可以運行。2. 效率教動態(tài)庫高寥院。 |
1. 會使用目標(biāo)程序的體積增大劲赠。 |
動態(tài)庫 | 1. 不需要拷貝到目標(biāo)程序中,不會影響目標(biāo)程序的體積秸谢。 2. 同一份庫可以被多個程序使用(因為這個原因凛澎,動態(tài)庫也被稱作共享庫)。 3. 編譯時才載入的特性估蹄,也可以讓我們隨時對庫進行替換塑煎,而不需要重新編譯代碼。實現(xiàn)動態(tài)更新 |
1. 動態(tài)載入會帶來一部分性能損失(可以忽略不計) 2. 動態(tài)庫也會使得程序依賴于外部環(huán)境臭蚁。如果環(huán)境缺少動態(tài)庫或者庫的版本不正確最铁,就會導(dǎo)致程序無法運行(Linux lib not found 錯誤)。 |
iOS的動態(tài)庫(被閹割的動態(tài)庫)
iOS平臺上規(guī)定不允許存在動態(tài)庫垮兑,并且所有的 IPA 都需要經(jīng)過Apple的私鑰加密后才能用冷尉,基本你用了動態(tài)庫也會因為簽名不對無法加載,(越獄和非 APP store 除外)系枪。于是就把開發(fā)者自己開發(fā)動態(tài)庫成為了天方夜譚雀哨。
iOS8之前因為 iOS 應(yīng)用都是運行在沙盒當(dāng)中,不同的程序之間不能共享代碼私爷,并且iOS是單進程的雾棺,也就是某一時刻只有一個進程在運行,那么你寫個共享庫衬浑,給誰共享呢捌浩。同時動態(tài)下載代碼又是被蘋果明令禁止的,沒辦法發(fā)揮出動態(tài)庫的優(yōu)勢工秩,綜上所以上動態(tài)庫也就沒有存在的必要了嘉栓。
但是后來iOS8之后,iOS有了App Extesion特性拓诸,而且Swift也誕生了侵佃。由于iOS主App需要和Extension共享代碼,Swift語言機制也需要動態(tài)庫奠支,于是蘋果后來提出了Embedded Framework馋辈,這種動態(tài)庫允許APP和APP Extension共享代碼,但是這份動態(tài)庫的生命被限定在一個APP進程內(nèi)倍谜。簡單點可以理解為被閹割的動態(tài)庫迈螟。
但是這種動態(tài)庫(Embedded Framework) 和系統(tǒng)的 UIKit.Framework 還是有很大區(qū)別叉抡,傳統(tǒng)的動態(tài)庫是給多個進程用的,而這里的動態(tài)庫(Embedded Framework)是給單個進程里面多個可執(zhí)行文件用的答毫。系統(tǒng)的 Framework 不需要拷貝到目標(biāo)程序中褥民,我們自己做出來的 動態(tài)庫(Embedded Framework) 哪怕是動態(tài)的,最后也還是要拷貝到 App 中(App 和 Extension 的 Bundle 是共享的)洗搂。所以蘋果沒有直接把這種Embedded Framework稱作動態(tài)庫而是叫Embedded Framework消返。
上面提到跟Swift也有原因,在Swift的項目中如果要在項目中使用外部的代碼耘拇,可選的方式只有兩種撵颊,一種是把代碼拷貝到工程中,另一種是用動態(tài) Framework惫叛。使用靜態(tài)庫是不支持的倡勇。這個問題的根本原因主要是 Swift 的運行庫沒有被包含在 iOS 系統(tǒng)中,而是會打包進 App 中(這也是造成 Swift App 體積大的原因)嘉涌,靜態(tài)庫會導(dǎo)致最終的目標(biāo)程序中包含重復(fù)的運行庫(這是蘋果自家的解釋)妻熊。原文如下:
The current runtime doesn't ship with the OS, so static libs would lead to multiple runtimes in the final executable. A statically linked runtime would be much more difficult to patch for compatibility with newer OS or Swift.
iOS中的Embedded Framework可以理解為獨立的沒有main函數(shù)的可執(zhí)行文件。
基礎(chǔ)知識
前面提到的靜態(tài)庫可以簡單理解為一堆目標(biāo)文件(.o/.obj)的打包體(并非二進制文件)仑最,而動態(tài)庫可以簡單理解為 一個沒有main函數(shù)的可執(zhí)行文件固耘。
大學(xué)再講編譯原理的時候有兩個非常重要的過程,編譯和鏈接词身。編譯可以理解為將源代碼編譯為目標(biāo)文件,鏈接可以理解為將各種目標(biāo)文件上加一些第三方庫番枚、并且和系統(tǒng)庫鏈接起來為可執(zhí)行文件法严。因為某個目標(biāo)文件的符號(可以理解為變化、函數(shù))可能來至其他目標(biāo)文件葫笼,鏈接最為主要的就是決議符號的地址深啤。
編譯會生成目標(biāo)文件,目標(biāo)文件沒有經(jīng)過鏈接的過程路星,某些符號還沒有調(diào)整過溯街,Windows下的.obj文件,Linux下的.o文件洋丐,Unix的.out文件呈昔。
鏈接的過程可以簡單描述如下:
假如主程序main.c 使用了 fun.c 模塊的 foo函數(shù),那么main.c在編譯的過程友绝,對于調(diào)用foo函數(shù)的指令堤尾,對于指令的目標(biāo)地址暫時擱置;待到鏈接的時候迁客,由鏈接器來填寫foo函數(shù)的地址郭宝。
在決議符號的時候有如下規(guī)則:
- 若符號來自靜態(tài)庫(本質(zhì)就是.o 的集合包)或 .o辞槐,將其納入鏈接產(chǎn)物,并確定符號地址粘室。常見的符號沖突就出現(xiàn)在這一步榄檬。
- 若符號來自動態(tài)庫,打個標(biāo)記衔统,等啟動的時候再說---交給dyld去加載和鏈接符號鹿榜。也就是把鏈接的過程推遲到運行時再進行,上面講到的靜態(tài)庫符號沖突就可以推遲到運行時在解決缰冤,而具體怎么解決由系統(tǒng)去決定犬缨。如果這兩個符號表示的意思是一樣(比如函數(shù)符號沖突但是函數(shù)的實現(xiàn)是一樣的)的就沒有問題。
如果要深入了解一下相關(guān)知識棉浸,建議看一下《程序員自我修養(yǎng)》這本書怀薛,我也只懂皮毛。
靜態(tài)庫和動態(tài)庫依賴關(guān)系
- 第一種靜態(tài)庫互相依賴迷郑,這種情況非常常見枝恋,制作靜態(tài)庫的時候只需要有被依賴的靜態(tài)庫頭文件在就能編譯出來。但是這就意味者你要收到告訴使用者你的依賴關(guān)系嗡害。
- 第二種動態(tài)庫依賴動態(tài)庫焚碌,兩個動態(tài)庫是相互隔離的具有隔離性。在制作的靜態(tài)庫的時候需要被依賴動態(tài)庫參與鏈接霸妹,最終具體的符號決議交給dyld來做十电。
- 第三種,靜態(tài)庫依賴動態(tài)庫叹螟,也很常見鹃骂,靜態(tài)庫制作的時候也需要動態(tài)庫參與鏈接,但是符號的決議交給dyld來做罢绽。
- 第四種畏线,動態(tài)庫依賴靜態(tài)庫,這種情況就有點特殊良价。首先我們設(shè)想動態(tài)庫編譯的時候需要靜態(tài)庫參與編譯寝殴,但是靜態(tài)庫交由dyld來做符號決議,這和我們前面說的就矛盾了啊明垢。靜態(tài)庫本質(zhì)是一堆.o 的打包體蚣常,首先并不是二進制可執(zhí)行文件,再者你無法保證主程序把靜態(tài)庫參與鏈接共同生成二進制可執(zhí)行文件痊银。
對于第四種情況解決辦法如下:
目前的編譯器的解決辦法是史隆,首先我無法保證主程序是否包含靜態(tài)庫,再者靜態(tài)庫也無法被dyld加載曼验,那么我直接把你靜態(tài)庫的.o 偷過來泌射,共同組成一個新的二進制粘姜。也被稱做吸附性。
如果有多個動態(tài)庫依賴這個靜態(tài)庫就會熔酷,每個動態(tài)庫為了保證自己的正確性會把靜態(tài)庫吸附進來孤紧。然后兩個庫包含了同樣的靜態(tài)庫,于是問題就出現(xiàn)了拒秘。
利用動態(tài)庫解決相關(guān)問題
有了上面的知識就可以解決一些平時遇到的疑難雜癥号显。
處理多個動態(tài)庫依賴一個靜態(tài)庫問題
通過前面我們知道可執(zhí)文件(主程序或者動態(tài)庫)在構(gòu)建的鏈接階段,遇到靜態(tài)庫躺酒,吸附進來押蚤;遇到動態(tài)庫,打標(biāo)記羹应,彼此保持獨立揽碘。
正因為動態(tài)庫是保持獨立的,那么我們可以自定義一個動態(tài)庫把依賴的靜態(tài)庫吸附進來园匹。對外整體呈現(xiàn)的是動態(tài)庫特性雳刺。其他的組件依賴我們自定義的動態(tài)庫,由于隔離性的存在裸违,不會出現(xiàn)問題掖桦。
這個思路在處理項目組件化的時候非常有用,尤其是在使用Swift的項目中供汛。
利用動態(tài)庫處理靜態(tài)庫與靜態(tài)庫的符號沖突問題
需要知道枪汪,在打包IPA的時候,最終靜態(tài)庫會被連接到最終的那個可執(zhí)行文件中怔昨。所以如果多個靜態(tài)庫擁有了相同的符號必定會產(chǎn)生符號沖突雀久。
前面講過可以把動態(tài)庫看成一個獨立的沒有main函數(shù)入口的可執(zhí)行文件,在iOS打包中直接copy到應(yīng)用程序.app
目錄下的Frameworks目錄朱监。既然是可執(zhí)行文件那么內(nèi)部編譯連接過程已經(jīng)完成了,要處理的連接也只有在加載的時候由操作系統(tǒng)的dyld自動load + link原叮。
所以最終系統(tǒng)在加載動態(tài)庫的時候和靜態(tài)庫的符號根本沒有絲毫關(guān)系赫编,進而避免了鏈接時產(chǎn)生的符號沖突。
這一點在處理一些由于底層三方庫源碼不能手動修改(比如boringssl與openssl)的時候奋隶,非常有用擂送。
動態(tài)庫的動態(tài)裝載
目前iOS中動態(tài)更新方案有如下幾種:
- HTML 5
- lua(wax)hotpatch
- react native
- framework
使用 framework 的方式來更新可以不依賴第三方庫,使用原生的 OC/Swift 來開發(fā)唯欣,體驗更好嘹吨。由于 Apple 不希望開發(fā)者繞過 App Store 來更新 app,因此只有對于不需要上架的應(yīng)用境氢,才能以 framework 的方式實現(xiàn) app 的更新蟀拷。
使用framework實現(xiàn)動態(tài)更新常用用到的一些函數(shù)如下:
-
dlfcn.h中的的方法:用于處理動態(tài)庫的裝載碰纬、卸載。
- dlopen打開動態(tài)鏈接庫问芬;
- dlerror返回錯誤悦析;
- dlsym獲取函數(shù)名或者變量名;
- dlclose關(guān)閉動態(tài)庫此衅;
-
Objective-C的方法: 用于動態(tài)庫中對象的具體使用强戴。
- NSClassFromString根據(jù)名字返回類;
- NSSelectorFromString根據(jù)名字返回方法挡鞍;
- performSelector執(zhí)行方法骑歹;
注意:沒有在在Linked的設(shè)置里面設(shè)置的動態(tài)庫,通過dlopen的形式來打開墨微。如果動態(tài)庫在Link Framwokrs and Libraries中設(shè)置了會在應(yīng)用啟動的時候就會被加載道媚。
在使用動態(tài)庫對象的時候必須使用NSClassFromString的方式,使用常見對象創(chuàng)建的方式是不可以的欢嘿。在使用dlopen打開動態(tài)庫的時候注意在build settings里面設(shè)置對應(yīng)的路徑衰琐,其中的@executable_path/表示可執(zhí)行文件所在路徑,即沙盒中的.app目錄炼蹦,注意不要漏掉最后的/羡宙。如果你將動態(tài)庫放到了沙盒中的其他目錄,只需要添加對應(yīng)路徑的依賴就可以了掐隐。
實例代碼的代碼如下:
打開動態(tài)庫
- (IBAction)onDlopenLoadAtPathAction1:(id)sender
{
NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/Dylib.framework/Dylib",NSHomeDirectory()];
[self dlopenLoadDylibWithPath:documentsPath];
}
- (void)dlopenLoadDylibWithPath:(NSString *)path
{
libHandle = NULL;
libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
if (libHandle == NULL) {
char *error = dlerror();
NSLog(@"dlopen error: %s", error);
} else {
NSLog(@"dlopen load framework success.");
}
}
使用動態(tài)庫中的內(nèi)容
- (IBAction)onTriggerButtonAction:(id)sender
{
Class rootClass = NSClassFromString(@"Person");
if (rootClass) {
id object = [[rootClass alloc] init];
[(Person *)object run];
}
}
擴展閱讀
編譯器的工作過程
編譯器
Mach-O 可執(zhí)行文件
Linux下的靜態(tài)庫狗热、動態(tài)庫和動態(tài)加載庫