iOS 開發(fā)中的『庫』(一)

看文章之前,你可以看下下面幾個問題,如果你都會了,或許可以不看。

  • .framework 是什么?怎么制作?
  • 談一談自己對動態(tài)庫和靜態(tài)庫的理解。
  • 在項目中如何使用動態(tài)framework的 APP ?使用了動態(tài)framework 的 APP 能上架 Appstore 么?
  • 可以通過 framework 的方式實現(xiàn) app 的熱修復(fù)么?

我是前言

  • 最近發(fā)現(xiàn)很多人分不清 『.framework && .a 』、『動態(tài)庫 && 靜態(tài)庫』、『.tbd && .dylib』這幾個東西。甚至, 還有人一直以誤為 framework 就是動態(tài)庫??!鑒于網(wǎng)上許多文章都表述的含糊不清,再加上很多文章都比較老了,所以今天寫點東西總結(jié)一下。

  • 首先,看文章之前,你稍微了解這么幾個東西:編譯過程、內(nèi)存分區(qū)。下面開始!


理論篇

動態(tài)庫 VS. 靜態(tài)庫

Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime

  • 首先你得搞清楚,這兩個東西都是編譯好的二進(jìn)制文件。就是用法不同而已。為什么要分為動態(tài)和靜態(tài)兩種庫呢?先看下圖:
靜態(tài)庫
動態(tài)庫
  • 我們可以很清楚的看到:

    • 對于靜態(tài)庫而言,在編譯鏈接的時候,會將靜態(tài)庫所有文件都添加到 目標(biāo) app 可執(zhí)行文件中,并在程序運行之后,靜態(tài)庫app 可執(zhí)行文件 一起被加載到同一塊代碼區(qū)中。
      • app 可執(zhí)行文件: 這個目標(biāo) app 可執(zhí)行文件就是 ipa解壓縮后,再顯示的包內(nèi)容里面與app同名的文件。
    • 對于動態(tài)庫而言,在編譯鏈接的時候,只會將動態(tài)庫被引用的頭文件添加到目標(biāo)** app 可執(zhí)行文件,區(qū)別于靜態(tài)庫動態(tài)庫** 是在程序運行的時候被添加另外一塊內(nèi)存區(qū)域。
  • 下面看下蘋果的官方文檔中有兩句對動態(tài)庫靜態(tài)庫的解釋。
    - 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.

     - **動態(tài)庫**:可以在 **運行 or 啟動** 的時候加載到內(nèi)存中,加載到一塊**獨立的于 app ** 的內(nèi)存地址中
    
     - 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
    
     - **靜態(tài)庫**:當(dāng)程序在啟動的時候,會將 app 的代碼(包括靜態(tài)庫的代碼)一起在加載到 app 所處的內(nèi)存地址上。相比于**靜態(tài)庫** 的方案,使用**動態(tài)庫**將花費更多的啟動時間和內(nèi)存消耗。還會增加可執(zhí)行文件的大小。
    
  • 舉個??:假設(shè) UIKit 編譯成靜態(tài)庫和動態(tài)庫的大小都看成 1M , 加載到內(nèi)存中花銷 1s . 現(xiàn)在又 app1 和 app2 兩個 app。倘若使用靜態(tài)庫的方式,那么在 app1 啟動的時候, 需要花銷 2s 同時內(nèi)存有 2M 分配給了 app1.同樣的道理 加上 app2 的啟動時間和內(nèi)存消耗,采用靜態(tài)庫的方案,一共需要花銷 4s 啟動時間4M 內(nèi)存大小、4M 安裝包大小。那么換成動態(tài)庫的時候,對于啟動和 app1 可能花費一樣的時間,但是在啟動 app2 的時候 不用再加載 UIKit 動態(tài)庫 了。減少了 UIKit 的重復(fù) 使用問題,一共花銷 3s啟動時間3M 內(nèi)存大小、4M 安裝包大小。

  • 而很多 app 都會使用很多相同的庫,如 UIKit 、 CFNetwork 等。所以,蘋果為了加快 app 啟動速度、減少內(nèi)存花銷、減少安裝包體積大小,采用了大量 動態(tài)庫的形式 來優(yōu)化系統(tǒng)。dyld 的共享緩存 :在 OS X 和 iOS 上的動態(tài)鏈接器使用了共享緩存,共享緩存存于 /var/db/dyld/。對于每一種架構(gòu),操作系統(tǒng)都有一個單獨的文件,文件中包含了絕大多數(shù)的動態(tài)庫,這些庫都已經(jīng)鏈接為一個文件,并且已經(jīng)處理好了它們之間的符號關(guān)系。當(dāng)加載一個 Mach-O 文件 (一個可執(zhí)行文件或者一個庫) 時,動態(tài)鏈接器首先會檢查 共享緩存 看看是否存在其中,如果存在,那么就直接從共享緩存中拿出來使用。每一個進(jìn)程都把這個共享緩存映射到了自己的地址空間中。這個方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動時間。

  • 兩者都是由*.o目標(biāo)文件鏈接而成。都是二進(jìn)制文件,閉源。

.framework VS .a

  • .a是一個純二進(jìn)制文件,不能直接拿來使用,需要配合頭文件、資源文件一起使用。在 iOS 中是作為靜態(tài)庫的文件名后綴。

  • .framework中除了有二進(jìn)制文件之外還有資源文件,可以拿來直接使用。

  • 在不能開發(fā)動態(tài)庫的時候,其實 『.framework = .a + .h + bundle』。而當(dāng) Xcode 6 出來以后,我們可以開發(fā)動態(tài)庫后『.framework = 靜態(tài)庫/動態(tài)庫 + .h + bundle』

.tbd VS .dylib

  • 對于靜態(tài)庫的后綴名是.a,那么動態(tài)庫的后綴名是什么呢?

  • 可以從 libsqlite3.dylib 這里我們可以知道 .dylib 就是動態(tài)庫的文件的后綴名。

  • 那么 .tbd 又是什么東西呢?其實,細(xì)心的朋友都早已發(fā)現(xiàn)了從 Xcode7 我們再導(dǎo)入系統(tǒng)提供的動態(tài)庫的時候,不再有.dylib了,取而代之的是.tbd。而 .tbd 其實是一個YAML本文文件,描述了需要鏈接的動態(tài)庫的信息。主要目的是為了減少app 的下載大小。具體細(xì)節(jié)可以看這里

小總結(jié)

  • 首先,相比較與靜態(tài)庫和動態(tài)庫,動態(tài)庫在包體積、啟動時間還有內(nèi)存占比上都是很有優(yōu)勢的。
  • 為了解決 .a 的文件不能直接用,還要配備 .h 和資源文件,蘋果推出了一個叫做 .framework 的東西,而且還支持動態(tài)庫。

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,前面說了那么多,那么如果我們自己開發(fā)了一個動態(tài)framework 怎么把它復(fù)制到 dyld 的共享緩存 里面呢?

  • 一般來說,用正常的方式是不能滴,蘋果也不允許你這么做。(當(dāng)然不排除一些搞逆向的大神通過一些 hack 手段達(dá)到目的)

  • 那么,我們應(yīng)該如何開發(fā)并使用我們自己開發(fā)的 動態(tài)framework 呢?

  • 那就是 Embedded Binaries。

  • Embedded 的意思是嵌入,但是這個嵌入并不是嵌入 app 可執(zhí)行文件,而是嵌入 app 的 bundle 文件。當(dāng)一個 app 通過 Embedded 的方式嵌入一個 app 后,在打包之后解壓 ipa 可以在包內(nèi)看到一個 framework 的文件夾,下面都是與這個應(yīng)用相關(guān)的動態(tài)framework。在 Xcode 可以在這里設(shè)置,圖中紅色部分:

Embedded && Link
  • 那么問題又來了,下面的 linded feameworks and libraries 又是什么呢?
  • 首先在 linded feameworks and libraries 這個下面我們可以連接系統(tǒng)的動態(tài)庫、自己開發(fā)的靜態(tài)庫、自己開發(fā)的動態(tài)庫。對于這里的靜態(tài)庫而言,會在編譯鏈接階段連接到app可執(zhí)行文件中,而對這里的動態(tài)庫而言,雖然不會鏈接到app可執(zhí)行文件中,但是會在啟動的時候就去加載這里設(shè)置的所有動態(tài)庫。(ps.理論上應(yīng)該是這樣,但是在我實際測試中似乎加載不加載都和這個沒關(guān)系。可能我的姿勢不對。??)
  • 如果你不想在啟動的時候加載動態(tài)庫,可以在 linded feameworks and libraries 刪除,并使用dlopen加載動態(tài)庫。(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.");
    }
}

關(guān)于制作過程

  • 關(guān)于如何制作,大家可以看下raywenderlich家的經(jīng)典教程《How to Create a Framework for iOS 》,中文可以看這里《創(chuàng)建你自己的Framework》

  • 閱讀完這篇教程,我補充幾點。

    • 首先,framework 分為Thin and Fat Frameworks。Thin 的意思就是瘦,指的是單個架構(gòu)。而 Fat 是胖,指的是多個架構(gòu)。
    • 要開發(fā)一個真機和模擬器都可以調(diào)試的 Frameworks 需要對Frameworks進(jìn)行合并。合并命令是 lipolipo。
    • 如果 app 要上架 appstore 在提交審核之前需要把 Frameworks 中模擬器的架構(gòu)給去除掉。
    • 個人理解,項目組件化或者做 SDK 的時候,最好以 framework 的形式來做。

實踐篇

framework 的方式實現(xiàn) app 的熱修復(fù)

  • 由于 Apple 不希望開發(fā)者繞過 App Store 來更新 app,因此只有對于不需要上架的應(yīng)用,才能以 framework 的方式實現(xiàn) app 的更新。
  • 但是理論上只要保持簽名一致,在 dlopen 沒有被禁止的情況下應(yīng)該是行的通的。(因為沒有去實踐,只能這樣 YY 了。)
  • 但是不論是哪種方式都得保證 服務(wù)器上的 framework 與 app 的簽名要保持一致。

實現(xiàn)大致思路

  • 下載新版的 framework
  • 先到 document 下尋找 framework。然后根據(jù)條件加載 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)];

番外篇

關(guān)于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

當(dāng)我們點擊了 build 之后,做了什么事情呢?

  • 預(yù)處理(Pre-process):把宏替換,刪除注釋,展開頭文件,產(chǎn)生 .i 文件。
  • 編譯(Compliling):把之前的 .i 文件轉(zhuǎn)換成匯編語言,產(chǎn)生 .s文件。
  • 匯編(Asembly):把匯編語言文件轉(zhuǎn)換為機器碼文件,產(chǎn)生 .o 文件。
  • 鏈接(Link):對.o文件中的對于其他的庫的引用的地方進(jìn)行引用,生成最后的可執(zhí)行文件(同時也包括多個 .o 文件進(jìn)行 link)。

ld && libtool

  • ld :用于產(chǎn)生可執(zhí)行文件。
  • libtool:產(chǎn)生 lib 的工具。

Build phases && Build rules && Build settings

  • Build phases: 主要是用來控制從源文件到可執(zhí)行文件的整個過程的,所以應(yīng)該說是面向源文件的,包括編譯哪些文件,以及在編譯過程中執(zhí)行一些自定義的腳本什么的。
  • Build rules: 主要是用來控制如何編譯某種類型的源文件的,假如說想對某種類型的原文件進(jìn)行特定的編譯,那么就應(yīng)該在這里進(jìn)行編輯了。同時這里也會大量的運用一些 xcode 中的環(huán)境變量,完整的官方文檔在這里:Build Settings Reference
  • Build settings:則是對編譯工作的細(xì)節(jié)進(jìn)行設(shè)定,在這個窗口里可以看見大量的設(shè)置選項,從編譯到打包再到代碼簽名都有,這里要注意 settings 的 section 分類,同時一般通過右側(cè)的 inspector 就可以很好的理解選項的意義了。

談?wù)?Mach-O

Mach-O
  • 在制作 framework 的時候需要選擇這個 Mach-O Type.
  • 為Mach Object文件格式的縮寫,它是一種用于可執(zhí)行文件,目標(biāo)代碼,動態(tài)庫,內(nèi)核轉(zhuǎn)儲的文件格式。作為a.out格式的替代,Mach-O提供了更強的擴展性,并提升了符號表中信息的訪問速度。

參考資料


后記

@我就叫Sunny怎么了 提出的問題。

問題

更多

工作之余,寫了點筆記,如果需要可以在我的 GitHub 看。

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

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

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