前面介紹過制作過程,這里不講如何制作動態庫、靜態庫。
靜態庫和動態庫都是以二進制提供代碼復用的代碼庫。
- 靜態庫常見的是 .a
- 動態庫(共享庫)常見的是 Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib/.tbd。
特別注意平時我們經常說的Framework(in Apple) 是Cocoa/Cocoa Touch程序中使用的一種資源打包方式,可以將代碼文件、頭文件、資源文件、說明文檔等集中在一起,方便開發者使用。也就是說我們的 Framework其實是資源打包的方式,和靜態庫動態庫的本質是沒有什么關系。
靜態庫和動態庫的區別
首先來看什么是庫,庫(Library)說白了就是一段編譯好的二進制代碼,加上頭文件就可以供別人使用。我們在和別人合作的時候,一種情況是某些代碼需要給別人使用,但是我們不希望別人看到源碼,就需要以庫的形式進行封裝,只暴露出頭文件。另外一種情況是,對于某些不會進行大的改動的代碼,我們想減少編譯的時間,就可以把它打包成庫,因為庫是已經編譯好的二進制了,編譯的時候只需要 Link 一下,不會浪費編譯時間。
- 靜態庫:鏈接時會被完整的復制到可執行文件中,所以如果兩個程序都用了某個靜態庫,那么每個二進制可執行文件里面其實都含有這份靜態庫的代碼。
- 動態庫: 鏈接時不復制,在程序啟動后用動態加載,然后再決議符號,所以理論上動態庫只用存在一份,好多個程序都可以動態鏈接到這個動態庫上面,達到了節省內存(不是磁盤是內存中只有一份動態庫),還有另外一個好處,由于動態庫并不綁定到可執行程序上,所以我們想升級這個動態庫就很容易,windows和linux上面一般插件和模塊機制都是這樣實現的。
動態庫和靜態庫都是由*.o
目標文件生成的。
對比一下靜態和動態庫的優缺點
庫類型 | 優點 | 缺點 |
---|---|---|
靜態庫 | 1. 目標程序沒有外部依賴,直接就可以運行。2. 效率教動態庫高。 |
1. 會使用目標程序的體積增大。 |
動態庫 | 1. 不需要拷貝到目標程序中,不會影響目標程序的體積。 2. 同一份庫可以被多個程序使用(因為這個原因,動態庫也被稱作共享庫)。 3. 編譯時才載入的特性,也可以讓我們隨時對庫進行替換,而不需要重新編譯代碼。實現動態更新 |
1. 動態載入會帶來一部分性能損失(可以忽略不計) 2. 動態庫也會使得程序依賴于外部環境。如果環境缺少動態庫或者庫的版本不正確,就會導致程序無法運行(Linux lib not found 錯誤)。 |
iOS的動態庫(被閹割的動態庫)
iOS平臺上規定不允許存在動態庫,并且所有的 IPA 都需要經過Apple的私鑰加密后才能用,基本你用了動態庫也會因為簽名不對無法加載,(越獄和非 APP store 除外)。于是就把開發者自己開發動態庫成為了天方夜譚。
iOS8之前因為 iOS 應用都是運行在沙盒當中,不同的程序之間不能共享代碼,并且iOS是單進程的,也就是某一時刻只有一個進程在運行,那么你寫個共享庫,給誰共享呢。同時動態下載代碼又是被蘋果明令禁止的,沒辦法發揮出動態庫的優勢,綜上所以上動態庫也就沒有存在的必要了。
但是后來iOS8之后,iOS有了App Extesion特性,而且Swift也誕生了。由于iOS主App需要和Extension共享代碼,Swift語言機制也需要動態庫,于是蘋果后來提出了Embedded Framework,這種動態庫允許APP和APP Extension共享代碼,但是這份動態庫的生命被限定在一個APP進程內。簡單點可以理解為被閹割的動態庫。
但是這種動態庫(Embedded Framework) 和系統的 UIKit.Framework 還是有很大區別,傳統的動態庫是給多個進程用的,而這里的動態庫(Embedded Framework)是給單個進程里面多個可執行文件用的。系統的 Framework 不需要拷貝到目標程序中,我們自己做出來的 動態庫(Embedded Framework) 哪怕是動態的,最后也還是要拷貝到 App 中(App 和 Extension 的 Bundle 是共享的)。所以蘋果沒有直接把這種Embedded Framework稱作動態庫而是叫Embedded Framework。
上面提到跟Swift也有原因,在Swift的項目中如果要在項目中使用外部的代碼,可選的方式只有兩種,一種是把代碼拷貝到工程中,另一種是用動態 Framework。使用靜態庫是不支持的。這個問題的根本原因主要是 Swift 的運行庫沒有被包含在 iOS 系統中,而是會打包進 App 中(這也是造成 Swift App 體積大的原因),靜態庫會導致最終的目標程序中包含重復的運行庫(這是蘋果自家的解釋)。原文如下:
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函數的可執行文件。
基礎知識
前面提到的靜態庫可以簡單理解為一堆目標文件(.o/.obj)的打包體(并非二進制文件),而動態庫可以簡單理解為 一個沒有main函數的可執行文件。
大學再講編譯原理的時候有兩個非常重要的過程,編譯和鏈接。編譯可以理解為將源代碼編譯為目標文件,鏈接可以理解為將各種目標文件上加一些第三方庫、并且和系統庫鏈接起來為可執行文件。因為某個目標文件的符號(可以理解為變化、函數)可能來至其他目標文件,鏈接最為主要的就是決議符號的地址。
編譯會生成目標文件,目標文件沒有經過鏈接的過程,某些符號還沒有調整過,Windows下的.obj文件,Linux下的.o文件,Unix的.out文件。
鏈接的過程可以簡單描述如下:
假如主程序main.c 使用了 fun.c 模塊的 foo函數,那么main.c在編譯的過程,對于調用foo函數的指令,對于指令的目標地址暫時擱置;待到鏈接的時候,由鏈接器來填寫foo函數的地址。
在決議符號的時候有如下規則:
- 若符號來自靜態庫(本質就是.o 的集合包)或 .o,將其納入鏈接產物,并確定符號地址。常見的符號沖突就出現在這一步。
- 若符號來自動態庫,打個標記,等啟動的時候再說---交給dyld去加載和鏈接符號。也就是把鏈接的過程推遲到運行時再進行,上面講到的靜態庫符號沖突就可以推遲到運行時在解決,而具體怎么解決由系統去決定。如果這兩個符號表示的意思是一樣(比如函數符號沖突但是函數的實現是一樣的)的就沒有問題。
如果要深入了解一下相關知識,建議看一下《程序員自我修養》這本書,我也只懂皮毛。
靜態庫和動態庫依賴關系
- 第一種靜態庫互相依賴,這種情況非常常見,制作靜態庫的時候只需要有被依賴的靜態庫頭文件在就能編譯出來。但是這就意味者你要收到告訴使用者你的依賴關系。
- 第二種動態庫依賴動態庫,兩個動態庫是相互隔離的具有隔離性。在制作的靜態庫的時候需要被依賴動態庫參與鏈接,最終具體的符號決議交給dyld來做。
- 第三種,靜態庫依賴動態庫,也很常見,靜態庫制作的時候也需要動態庫參與鏈接,但是符號的決議交給dyld來做。
- 第四種,動態庫依賴靜態庫,這種情況就有點特殊。首先我們設想動態庫編譯的時候需要靜態庫參與編譯,但是靜態庫交由dyld來做符號決議,這和我們前面說的就矛盾了啊。靜態庫本質是一堆.o 的打包體,首先并不是二進制可執行文件,再者你無法保證主程序把靜態庫參與鏈接共同生成二進制可執行文件。
對于第四種情況解決辦法如下:
目前的編譯器的解決辦法是,首先我無法保證主程序是否包含靜態庫,再者靜態庫也無法被dyld加載,那么我直接把你靜態庫的.o 偷過來,共同組成一個新的二進制。也被稱做吸附性。
如果有多個動態庫依賴這個靜態庫就會,每個動態庫為了保證自己的正確性會把靜態庫吸附進來。然后兩個庫包含了同樣的靜態庫,于是問題就出現了。
利用動態庫解決相關問題
有了上面的知識就可以解決一些平時遇到的疑難雜癥。
處理多個動態庫依賴一個靜態庫問題
通過前面我們知道可執文件(主程序或者動態庫)在構建的鏈接階段,遇到靜態庫,吸附進來;遇到動態庫,打標記,彼此保持獨立。
正因為動態庫是保持獨立的,那么我們可以自定義一個動態庫把依賴的靜態庫吸附進來。對外整體呈現的是動態庫特性。其他的組件依賴我們自定義的動態庫,由于隔離性的存在,不會出現問題。
這個思路在處理項目組件化的時候非常有用,尤其是在使用Swift的項目中。
利用動態庫處理靜態庫與靜態庫的符號沖突問題
需要知道,在打包IPA的時候,最終靜態庫會被連接到最終的那個可執行文件中。所以如果多個靜態庫擁有了相同的符號必定會產生符號沖突。
前面講過可以把動態庫看成一個獨立的沒有main函數入口的可執行文件,在iOS打包中直接copy到應用程序.app
目錄下的Frameworks目錄。既然是可執行文件那么內部編譯連接過程已經完成了,要處理的連接也只有在加載的時候由操作系統的dyld自動load + link。
所以最終系統在加載動態庫的時候和靜態庫的符號根本沒有絲毫關系,進而避免了鏈接時產生的符號沖突。
這一點在處理一些由于底層三方庫源碼不能手動修改(比如boringssl與openssl)的時候,非常有用。
動態庫的動態裝載
目前iOS中動態更新方案有如下幾種:
- HTML 5
- lua(wax)hotpatch
- react native
- framework
使用 framework 的方式來更新可以不依賴第三方庫,使用原生的 OC/Swift 來開發,體驗更好。由于 Apple 不希望開發者繞過 App Store 來更新 app,因此只有對于不需要上架的應用,才能以 framework 的方式實現 app 的更新。
使用framework實現動態更新常用用到的一些函數如下:
-
dlfcn.h中的的方法:用于處理動態庫的裝載、卸載。
- dlopen打開動態鏈接庫;
- dlerror返回錯誤;
- dlsym獲取函數名或者變量名;
- dlclose關閉動態庫;
-
Objective-C的方法: 用于動態庫中對象的具體使用。
- NSClassFromString根據名字返回類;
- NSSelectorFromString根據名字返回方法;
- performSelector執行方法;
注意:沒有在在Linked的設置里面設置的動態庫,通過dlopen的形式來打開。如果動態庫在Link Framwokrs and Libraries中設置了會在應用啟動的時候就會被加載。
在使用動態庫對象的時候必須使用NSClassFromString的方式,使用常見對象創建的方式是不可以的。在使用dlopen打開動態庫的時候注意在build settings里面設置對應的路徑,其中的@executable_path/表示可執行文件所在路徑,即沙盒中的.app目錄,注意不要漏掉最后的/。如果你將動態庫放到了沙盒中的其他目錄,只需要添加對應路徑的依賴就可以了。
實例代碼的代碼如下:
打開動態庫
- (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.");
}
}
使用動態庫中的內容
- (IBAction)onTriggerButtonAction:(id)sender
{
Class rootClass = NSClassFromString(@"Person");
if (rootClass) {
id object = [[rootClass alloc] init];
[(Person *)object run];
}
}