- 因為這篇文章有些問題,所以建議看完之后再看下iOS 開發中的『庫』(二)這篇文章
看文章之前,你可以看下下面幾個問題,如果你都會了,或許可以不看。
- .framework 是什么?怎么制作?
- 談一談自己對動態庫和靜態庫的理解。
- 在項目中如何使用動態framework的 APP ?使用了動態framework 的 APP 能上架 Appstore 么?
- 可以通過 framework 的方式實現 app 的熱修復么?
我是前言
最近發現很多人分不清 『.framework && .a 』、『動態庫 && 靜態庫』、『.tbd && .dylib』這幾個東西。甚至, 還有人一直以誤為 framework 就是動態庫??!鑒于網上許多文章都表述的含糊不清,再加上很多文章都比較老了,所以今天寫點東西總結一下。
首先,看文章之前,你稍微了解這么幾個東西:編譯過程、內存分區。下面開始!
理論篇
動態庫 VS. 靜態庫
Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime
- 首先你得搞清楚,這兩個東西都是編譯好的二進制文件。就是用法不同而已。為什么要分為動態和靜態兩種庫呢?先看下圖:
-
我們可以很清楚的看到:
- 對于靜態庫而言,在編譯鏈接的時候,會將靜態庫的所有文件都添加到 目標 app 可執行文件中,并在程序運行之后,靜態庫與 app 可執行文件 一起被加載到同一塊代碼區中。
- app 可執行文件: 這個目標 app 可執行文件就是 ipa解壓縮后,再顯示的包內容里面與app同名的文件。
- 對于動態庫而言,在編譯鏈接的時候,只會將動態庫被引用的頭文件添加到目標** app 可執行文件,區別于靜態庫,動態庫** 是在程序運行的時候被添加另外一塊內存區域。
- 對于靜態庫而言,在編譯鏈接的時候,會將靜態庫的所有文件都添加到 目標 app 可執行文件中,并在程序運行之后,靜態庫與 app 可執行文件 一起被加載到同一塊代碼區中。
-
下面看下蘋果的官方文檔中有兩句對動態庫和靜態庫的解釋。
- A better approach is for an app to load code into its address space when it’s actually needed, either at launch time or at runtime. The type of library that provides this flexibility is called dynamic library.- **動態庫**:可以在 **運行 or 啟動** 的時候加載到內存中,加載到一塊**獨立的于 app ** 的內存地址中 - When an app is launched, the app’s code—which includes the code of the static libraries it was linked with—is loaded into the app’s address space.Applications with large executables suffer from slow launch times and large memory footprints - **靜態庫**:當程序在啟動的時候,會將 app 的代碼(包括靜態庫的代碼)一起在加載到 app 所處的內存地址上。相比于**靜態庫** 的方案,使用**動態庫**將花費更多的啟動時間和內存消耗。還會增加可執行文件的大小。
舉個??:假設 UIKit 編譯成靜態庫和動態庫的大小都看成 1M , 加載到內存中花銷 1s . 現在又 app1 和 app2 兩個 app。倘若使用靜態庫的方式,那么在 app1 啟動的時候, 需要花銷 2s 同時內存有 2M 分配給了 app1.同樣的道理 加上 app2 的啟動時間和內存消耗,采用靜態庫的方案,一共需要花銷 4s 啟動時間、4M 內存大小、4M 安裝包大小。那么換成動態庫的時候,對于啟動和 app1 可能花費一樣的時間,但是在啟動 app2 的時候 不用再加載 UIKit 動態庫 了。減少了 UIKit 的重復 使用問題,一共花銷 3s啟動時間、3M 內存大小、4M 安裝包大小。
而很多 app 都會使用很多相同的庫,如 UIKit 、 CFNetwork 等。所以,蘋果為了加快 app 啟動速度、減少內存花銷、減少安裝包體積大小,采用了大量 動態庫的形式 來優化系統。dyld 的共享緩存 :在 OS X 和 iOS 上的動態鏈接器使用了共享緩存,共享緩存存于 /var/db/dyld/。對于每一種架構,操作系統都有一個單獨的文件,文件中包含了絕大多數的動態庫,這些庫都已經鏈接為一個文件,并且已經處理好了它們之間的符號關系。當加載一個 Mach-O 文件 (一個可執行文件或者一個庫) 時,動態鏈接器首先會檢查 共享緩存 看看是否存在其中,如果存在,那么就直接從共享緩存中拿出來使用。每一個進程都把這個共享緩存映射到了自己的地址空間中。這個方法大大優化了 OS X 和 iOS 上程序的啟動時間。
兩者都是由*.o目標文件鏈接而成。都是二進制文件,閉源。
.framework VS .a
.a是一個純二進制文件,不能直接拿來使用,需要配合頭文件、資源文件一起使用。在 iOS 中是作為靜態庫的文件名后綴。
.framework中除了有二進制文件之外還有資源文件,可以拿來直接使用。
在不能開發動態庫的時候,其實 『.framework = .a + .h + bundle』。而當 Xcode 6 出來以后,我們可以開發動態庫后『.framework = 靜態庫/動態庫 + .h + bundle』
.tbd VS .dylib
對于靜態庫的后綴名是.a,那么動態庫的后綴名是什么呢?
可以從 libsqlite3.dylib 這里我們可以知道 .dylib 就是動態庫的文件的后綴名。
那么 .tbd 又是什么東西呢?其實,細心的朋友都早已發現了從 Xcode7 我們再導入系統提供的動態庫的時候,不再有.dylib了,取而代之的是.tbd。而 .tbd 其實是一個YAML本文文件,描述了需要鏈接的動態庫的信息。主要目的是為了減少app 的下載大小。具體細節可以看這里
小總結
- 首先,相比較與靜態庫和動態庫,動態庫在包體積、啟動時間還有內存占比上都是很有優勢的。
- 為了解決 .a 的文件不能直接用,還要配備 .h 和資源文件,蘋果推出了一個叫做 .framework 的東西,而且還支持動態庫。
Embedded VS. Linked
Embedded frameworks are placed within an app’s sandbox and are only available to that app. System frameworks are stored at the system-level and are available to all apps.
OK,前面說了那么多,那么如果我們自己開發了一個動態framework 怎么把它復制到 dyld 的共享緩存 里面呢?
一般來說,用正常的方式是不能滴,蘋果也不允許你這么做。(當然不排除一些搞逆向的大神通過一些 hack 手段達到目的)
那么,我們應該如何開發并使用我們自己開發的 動態framework 呢?
那就是 Embedded Binaries。
Embedded 的意思是嵌入,但是這個嵌入并不是嵌入 app 可執行文件,而是嵌入 app 的 bundle 文件。當一個 app 通過 Embedded 的方式嵌入一個 app 后,在打包之后解壓 ipa 可以在包內看到一個 framework 的文件夾,下面都是與這個應用相關的動態framework。在 Xcode 可以在這里設置,圖中紅色部分:
- 那么問題又來了,下面的 linded feameworks and libraries 又是什么呢?
- 首先在 linded feameworks and libraries 這個下面我們可以連接系統的動態庫、自己開發的靜態庫、自己開發的動態庫。對于這里的靜態庫而言,會在編譯鏈接階段連接到app可執行文件中,而對這里的動態庫而言,雖然不會鏈接到app可執行文件中,
但是會在啟動的時候就去加載這里設置的所有動態庫。(ps.理論上應該是這樣,但是在我實際測試中似乎加載不加載都和這個沒關系??赡芪业淖藙莶粚?。??) - 如果你不想在啟動的時候加載動態庫,可以在 linded feameworks and libraries 刪除,并使用dlopen加載動態庫。(dlopen 不是私有 api。)
- (void)dlopenLoad{
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.");
}
}
關于制作過程
關于如何制作,大家可以看下raywenderlich家的經典教程《How to Create a Framework for iOS 》,中文可以看這里《創建你自己的Framework》
-
閱讀完這篇教程,我補充幾點。
- 首先,framework 分為Thin and Fat Frameworks。Thin 的意思就是瘦,指的是單個架構。而 Fat 是胖,指的是多個架構。
- 要開發一個真機和模擬器都可以調試的 Frameworks 需要對Frameworks進行合并。合并命令是
lipo
lipo。 - 如果 app 要上架 appstore 在提交審核之前需要把 Frameworks 中模擬器的架構給去除掉。
- 個人理解,項目組件化或者做 SDK 的時候,最好以 framework 的形式來做。
實踐篇
framework 的方式實現 app 的熱修復
- 由于 Apple 不希望開發者繞過 App Store 來更新 app,因此只有對于不需要上架的應用,才能以 framework 的方式實現 app 的更新。
- 但是理論上只要保持簽名一致,在 dlopen 沒有被禁止的情況下應該是行的通的。(因為沒有去實踐,只能這樣 YY 了。)
- 但是不論是哪種方式都得保證 服務器上的 framework 與 app 的簽名要保持一致。
實現大致思路
- 下載新版的 framework
- 先到 document 下尋找 framework。然后根據條件加載 bundle or document 里的 framework。
NSString *fileName = @"remote";
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = nil;
if ([paths count] != 0) {
documentDirectory = [paths objectAtIndex:0];
}
NSFileManager *manager = [NSFileManager defaultManager];
NSString *bundlePath = [[NSBundle mainBundle]
pathForResource:fileName ofType:@"framework"];
BOOL loadDocument = YES;
// Check if new bundle exists
if (![manager fileExistsAtPath:bundlePath] && loadDocument) {
bundlePath = [documentDirectory stringByAppendingPathComponent:[fileName stringByAppendingString:@".framework"]];
}
- 再加載 framework
// Load bundle
NSError *error = nil;
NSBundle *frameworkBundle = [NSBundle bundleWithPath:bundlePath];
if (frameworkBundle && [frameworkBundle loadAndReturnError:&error]) {
NSLog(@"Load framework successfully");
}else {
NSLog(@"Failed to load framework with err: %@",error);
}
- 加載類并做事情
// Load class
Class PublicAPIClass = NSClassFromString(@"PublicAPI");
if (!PublicAPIClass) {
NSLog(@"Unable to load class");
}
NSObject *publicAPIObject = [PublicAPIClass new];
[publicAPIObject performSelector:@selector(mainViewController)];
番外篇
關于lipo
<a name="lipo"/>
$ lipo -info /Debug-iphoneos/Someframework.framwork/Someframework
# Architectures in the fat file: Someframework are: armv7 armv7s arm64
# 合并
$ lipo –create a.framework b.framework –output output.framework
#拆分
$ lipo –create a.framework -thin armv7 -output a-output-armv7.framework
<a name="build"/>
從源代碼到app
當我們點擊了 build 之后,做了什么事情呢?
- 預處理(Pre-process):把宏替換,刪除注釋,展開頭文件,產生 .i 文件。
- 編譯(Compliling):把之前的 .i 文件轉換成匯編語言,產生 .s文件。
- 匯編(Asembly):把匯編語言文件轉換為機器碼文件,產生 .o 文件。
- 鏈接(Link):對.o文件中的對于其他的庫的引用的地方進行引用,生成最后的可執行文件(同時也包括多個 .o 文件進行 link)。
ld && libtool
- ld :用于產生可執行文件。
- libtool:產生 lib 的工具。
Build phases && Build rules && Build settings
- Build phases: 主要是用來控制從源文件到可執行文件的整個過程的,所以應該說是面向源文件的,包括編譯哪些文件,以及在編譯過程中執行一些自定義的腳本什么的。
- Build rules: 主要是用來控制如何編譯某種類型的源文件的,假如說想對某種類型的原文件進行特定的編譯,那么就應該在這里進行編輯了。同時這里也會大量的運用一些 xcode 中的環境變量,完整的官方文檔在這里:Build Settings Reference
- Build settings:則是對編譯工作的細節進行設定,在這個窗口里可以看見大量的設置選項,從編譯到打包再到代碼簽名都有,這里要注意 settings 的 section 分類,同時一般通過右側的 inspector 就可以很好的理解選項的意義了。
談談 Mach-O
- 在制作 framework 的時候需要選擇這個 Mach-O Type.
- 為Mach Object文件格式的縮寫,它是一種用于可執行文件,目標代碼,動態庫,內核轉儲的文件格式。作為a.out格式的替代,Mach-O提供了更強的擴展性,并提升了符號表中信息的訪問速度。
參考資料
后記
- 水平有限,若有錯誤,希望多多指正![coderonevv@gmail.com]
@我就叫Sunny怎么了 提出的問題。
- 我已在iOS 開發中的『庫』(二) 中修改完畢。
更多
工作之余,寫了點筆記,如果需要可以在我的 GitHub 看。