iOS 靜態庫和動態庫


一、庫

1.1 什么是庫?

庫就是程序代碼的集合,將 N 個文件組織起來,是共享程序代碼的一種方式。從本質上來說是一種可執行代碼的二進制格式,可以被載入內存中執行。

1.2 庫的分類

根據程序代碼的開源情況,庫可以分為兩類

  • 開源庫

    源代碼是公開的,你可以看到具體實現。比如知名的第三方框架:AFNetworking、SDWebImage。

  • 閉源庫

    不公開源代碼,只公開調用的接口,看不到具體的實現,是一個編譯后的二進制文件。這種常見於一些公司的 SDK 包,比如高德地圖 SDK、環信即時通訊 SDK 等。而閉源庫又分為兩類:靜態庫和動態庫。

1.3 從源代碼到 app

當我們點擊了 build 之后,做了什么事情呢?

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

1.4 iOS 設備的 CPU 架構

模擬器:

4s-5: i386
5s-iPhone X(包括 iPhone SE): x86_64

真機(iOS設備):

armv6:iPhone、iPhone 2、iPhone 3G、iPod Touch(第一代)、iPod Touch(第二代)
armv7:iPhone 3Gs、iPhone 4、iPhone 4s、iPad、iPad 2
armv7s:iPhone 5、iPhone 5c(靜態庫只要支持了 armv7,就可以在 armv7s 的架構上運行,向下兼容)
arm64:iPhone 5s、iPhone 6、iPhone 6 Plus、iPhone 6s、iPhone 6s Plus、iPad Air、iPad Air2、iPad mini2、iPad mini3、iPhone 7、iPhone 7 Plus、iPhone 8、iPhone 8 Plus、iPhone X

二、靜態庫和動態庫

靜態和動態是相對編譯期和運行期而言的:

靜態庫在程序編譯時會被鏈接到目標代碼中,程序運行時將不再需要該靜態庫;動態庫在程序編譯時並不會被鏈接到目標代碼中,只是在程序運行時才被載入。

存在形式:

  • 靜態庫

    以 “.a” 或者 “.framework” 為文件后綴名。

    .a 是一個純二進制文件,.framework 中除了有二進制文件之外還有資源文件。.a 要有 .h 文件以及資源文件配合,.framework 文件可以直接使用。總的來說,.a + .h + sourceFile = .framework。所以創建靜態庫最好還是用 .framework 的形式。

  • 動態庫

    以 “.dylib” 或者 “.framework” 為文件后綴名(Xcode7 之后 .tbd 代替了 .dylib)

使用區別:

  1. 靜態庫鏈接時會被完整的復制到可執行文件中,被多次使用就有多份拷貝。

    image.png

    利用靜態函數庫編譯成的文件比較大,因為整個函數庫的所有數據都會被整合進目標代碼中。

    它的優點就顯而易見了,即編譯后的執行程序不需要外部的函數庫支持,因為所有使用的函數都已經被編譯進去了。當然這也會成為他的缺點,因為如果靜態函數庫改變了,那么你的程序必須重新編譯。

  2. 動態庫鏈接時不復制,程序運行時由系統動態加載到內存,供程序調用。而且系統只加載一次,多個程序共用,節省內存。

    image.png

    相對於靜態函數庫,動態函數庫在編譯的時候 並沒有被編譯進目標代碼中,你的程序執行到相關函數時才調用該函數庫里的相應函數,因此動態函數庫所產生的可執行文件比較小。由於函數庫沒有被整合進你的程序,而是程序運行時動態的申請並調用,所以程序的運行環境中必須提供相應的庫。動態函數庫的改變並不影響你的程序,所以動態函數庫的升級比較方便。

各自優點:

  • 靜態庫:

    ①、模塊化,分工合作,提高了代碼的復用及核心技術的保密程度
    ②、避免少量改動經常導致大量的重復編譯連接
    ③、也可以重用,注意不是共享使用

  • 動態庫:

    ①、可以將最終可執行文件體積縮小,將整個應用程序分模塊,團隊合作,進行分工,影響比較小
    ②、多個應用程序共享內存中得同一份庫文件,節省資源
    ③、可以不重新編譯連接可執行程序的前提下,更新動態庫文件達到更新應用程序的目的
    ④、應用插件化
    ⑤、軟件版本實時模塊升級
    ⑥、在其它大部分平台上,動態庫都可以用於不同應用間共享, 共享可執行文件,這就大大節省了內存。

iOS8 之前,蘋果不允許第三方框架使用動態方式加載,從 iOS8 開始允許開發者有條件地創建和使用動態框架,這種框架叫做 Cocoa Touch Framework。雖然同樣是動態框架,但是和系統 framework 不同,蘋果系統專屬的 framework 是共享的(如 UIKit),使用 Cocoa Touch Framework 制作的動態庫在打包和提交 app 時會被放到 app main bundle 的根目錄中,運行在沙盒里,而不是系統中。也就是說,不同的 app 就算使用了同樣的 framework,但還是會有多份的框架被分別簽名、打包和加載。不過 iOS8 上開放了 App Extension 功能,可以為一個應用創建插件,這樣主 app 和插件之間共享動態庫還是可行的。

三、靜態庫的處理方式

  • 對於一個靜態庫而言,其實已經是編譯好的了(類似一個 .o 的集合),這里並沒有連接。在 build 的過程中只會參與鏈接的過程,而這個鏈接的過程簡單的講就是合並,並且鏈接器只會將靜態庫中被使用的部分合並到可執行文件中去。相比較於動態庫,靜態庫的處理起來要簡單的多,具體如下圖:

    23

  • 鏈接器會將所有 .o 用到的 global symbol 和 unresolved symbol 放入一個臨時表,而且是 global symbol 是不能重復的。

  • 對於靜態庫的 .o,鏈接器會將沒有任何 symbol 在 unresolved symbol table 的給忽略。

  • unresolved symbol 類似 extern int test(); ---  .h 的聲明?

  • global symbol 類似 void test() { print("test") } -- .m 的實現?

  • 最后,鏈接器會用函數的實際地址來代替函數引用。

四、動態庫的處理方式

  • 首先,對於動態庫而言其實分動態鏈接庫和動態加載庫兩種的,這兩個最本質的區別還是加載時間

    動態鏈接庫:在沒有被加載到內存的前提下,當可執行文件被加載,動態庫也隨着被加載到內存中。在 Linked Framework and Libraries 設置的一些 share libraries。【隨着程序啟動而啟動】

    動態加載庫:當需要的時候再使用 dlopen 等通過代碼或者命令的方式來加載。【在程序啟動之后】

  • 但是不論是哪種動態庫,相比較與靜態庫,動態庫處理起來要棘手的多。由於動態庫是動態的,所以你事先不知道某個函數的具體地址,因此動態鏈接器在鏈接函數的時候需要做大量的工作。

因為動態庫在鏈接函數需要做大量的工作,而靜態庫已經實現處理好了。所以單純的在所有都沒有加載的情況下,靜態庫的加載速度會更快一點。而在 iOS 開發中的『庫』(一) 提到的有所不妥,正確應該是,雖然動態庫更加耗時,但是對於加載過的 share libraries 不需要再加載的這個前提下,使用動態庫可以節省一些啟動時間。

  • 而實現這個動態鏈接是使用了 Procedure Linkage Table (PLT)。首先這個 PLT 列出了程序中每一個函數的調用,當程序開始運行,如果動態庫被加載到內存中,PLT 會去尋找動態的地址並記錄下來,如果每個函數都被調用過的話,下一次調用就可以通過 PLT 直接跳轉了,但是和靜態庫還是有點區別的是,每一個函數的調用還是需要通過一張 PLT。這也正是 sunny 所說的所有靜態鏈接做的事情都搬到運行時來做了,會導致更慢的原因。

五、動態庫的作用

  1. 應用插件化

    每一個功能點都是一個動態庫,在用戶想使用某個功能的時候讓其從網絡下載,然后手動加載動態庫,實現功能的的插件化。

    雖然技術上來說這種動態更新是可行的,但是對於 AppStore 上上架的 app 是不可以的。iOS8 之后雖然可以上傳含有動態庫的 app,但是蘋果不僅需要你動態庫和 app 的簽名一致,而且蘋果會在你上架的時候再經過一次 AppStore 的簽名。所以你想在線更新動態庫,首先你得有蘋果 AppStore 私鑰,而這個基本不可能。

    除非你的應用不需要通過 AppStore 上架,比如企業內部的應用,通過企業證書發布,那么就可以實現應用插件化在線更新動態庫了。

  2. 共享可執行文件

    在其它大部分平台上,動態庫都可以用於不同應用間共享,這就大大節省了內存。從目前來看,iOS 仍然不允許進程間共享動態庫,即 iOS 上的動態庫只能是私有的,因為我們仍然不能將動態庫文件放置在除了自身沙盒以外的其它任何地方。

    iOS8 上開放了 App Extension 功能,可以為一個應用創建插件,這樣主 app 和插件之間共享動態庫還是可行的。

六、簽名

系統在加載動態庫時,會檢查 framework 的簽名,簽名中必須包含 TeamIdentifier,並且 framework 和 host app 的 TeamIdentifier 必須一致。

我們在 Debug 測試的時候是不會報錯的,在打包時如果有動態庫,那么就會檢查 TeamIdentifier。

如果不一致,否則會報下面的錯誤:

Error loading /path/to/framework: dlopen(/path/to/framework, 265): no suitable image found. Did find:/path/to/framework: mmap() error 1

如果用來打包的證書是 iOS 8 發布之前生成的,則打出的包驗證的時候會沒有 TeamIdentifier 這一項。這時在加載 framework 的時候會報下面的錯誤:

[deny-mmap] mapped file has no team identifier and is not a platform binary:/private/var/mobile/Containers/Bundle/Application/5D8FB2F7-1083-4564-94B2-0CB7DC75C9D1/YourAppNameHere.app/Frameworks/YourFramework.framework/YourFramework

可以通過 codesign 命令來驗證。

codesign -dv /path/to/YourApp.app
或
codesign -dv /path/to/YourFramework.framework

如果證書太舊,輸出的結果如下:

Executable=/path/to/YourApp.app/YourApp
Identifier=com.company.yourapp
Format=bundle with Mach-O thin (armv7)
CodeDirectory v=20100 size=221748 flags=0x0(none) hashes=11079+5 location=embedded
Signature size=4321
Signed Time=2015年10月21日 上午10:18:37
Info.plist entries=42
TeamIdentifier=not set
Sealed Resources version=2 rules=12 files=2451
Internal requirements count=1 size=188

注意其中的 TeamIdentifier=not set

我們在用 cocoapods 的 use_framework! 的時候生成的動態庫也可以用 codesign -dv /path/to/youFramework.framework 查看到 TeamIdentifier=not set。

七、Framework

7.1 什么是 Framework

Framework 是 Cocoa/Cocoa Touch 程序中使用的一種資源打包方式,可以將代碼文件、頭文件、資源文件、說明文檔等集中在一起,方便開發者使用。一般如果是靜態 Framework 的話,資源打包進 Framework 是讀取不了的。靜態 Framework 和 .a 文件都是編譯進可執行文件里面的。只有動態 Framework 能在 .app 下面的 Framework 文件夾下看到,並讀取 .framework 里的資源文件。

13

Cocoa/Cocoa Touch 開發框架本身提供了大量的 Framework,比如 Foundation.framework/UIKit.framework 等。需要注意的是,這些 Framework 無一例外都是動態庫

平時用的第三方 SDK 的 Framework 都是靜態庫,真正的動態庫是上不了 AppStore(iOS8 之后能上 AppStore,因為 App Extension,需要動態庫支持)。

Framework 為什么既是靜態庫又是動態庫?

系統的 .framework 是動態庫,我們自己建立的 .framework 一般都是靜態庫。但是現在用 xcode 創建Framework 的時候默認是動態庫,一般打包成 SDK 給別人用的話都使用的是靜態庫,可以修改 Build Settings的 Mach-O Type 為 Static Library。

八、Framework 目錄

14

  • Headers

    表示暴露的頭文件,一般都會有一個和 Framework 同名的 .h 文件,在創建 Framework 的時候文件夾里也會默認生成這樣一個文件。有這個和 Framework 同名的 .h 文件 @import 導入庫的時候編譯器才能找到這個庫(@import 導入頭文件可參考 iOS里的導入頭文件)。

  • info.plist

    主要就是這個 Framework 的一些配置信息。

  • Modules

    這個文件夾里有個 module.modulemap 文件

    framework module DynamicFramework {
      umbrella header "DynamicFramework.h"
    export *
    module * { export * }
    }

    這里面有這樣一句 umbrella header "DynamicFramework.h",umbrella 有保護傘、庇護的意思。

    也就是說 Headers 中暴露的 DynamicFramework.h 文件被放在 umbrella 雨傘下保護起來了,所以我們需要將其他的所有需要暴露的 .h 文件放到 DynamicFramework.h 文件中保護起來,不然會出現警告。@import 的時候也只能找到 umbrella 雨傘下保護起來的 .h 文件

  • 二進制文件(Unix 可執行文件)

    這個就是你源碼編譯而成的二進制文件,主要的執行代碼就在這個里面。

  • .bundle 文件

    如果我們在 Build Phases -> Copy Bundle Resources 里加入 .bundle 文件,那么創建出來的 .Framework 里就會有這個 .bundle 的資源文件夾。

九、Framework 的資源文件

CocoaPods 如何生成 Framework 的資源文件?

我們能看到用 cocoapods 創建 Framework 的時候,Framework 里面有一個 .bundle 文件,跟 Framework 同級目錄里也有一個 .bundle文件。這兩個文件其實是一樣的。

那這兩個 .bundle 是怎么來的呢?我們能看到用 use_frameworks! 生成的 pod 里面,pods 這個 PROJECT 下面會為每一個 pod 生成一個 target。

15

那么如果這個 pod 有資源文件的話,就會有一個叫 xxx-bundleName 的 target,最后這個 target 生成的就是 bundleName.bundle。

16

在 xxx 的 target 的 Build Phases -> Copy Bundle Resources 里加入這個 .bundle,在 Framework 里面就會生成這樣一個 bundle。在 xxx 的 target 的 Build Phases -> Target Dependencies 里加入這個 target:xxx-bundleName,就會在 Framework 的同級目錄里生成這樣一個 bundle。

靜態 Framework 里不需要加入資源文件。一般資源打包進靜態 Framework 是讀取不了的。

靜態 Framework 和 .a 文件都是編譯進可執行文件里面的。只有動態 Framework 能在 .app 的 Framework 文件夾下看到,並讀取 .framework 里的資源文件。

你可以用 NSBundle * bundle = [[NSBundle mainBundle] bundlePath]; 得到 .app 目錄,如果是動態庫你能在 Framework 目錄下看到這個動態庫以及動態庫里面資源文件。然后你只要用 NSBundle * bundle = [NSBundle bundleForClass:<#ClassFromFramework#>]; 得到這個動態庫的路徑就能讀取到里面的資源了。但是如果是靜態庫的話,因為編譯進了可執行文件里面,你也就沒辦法讀到這個靜態庫了,你能看到 .app 下的 Framework 目錄為空。

在 Framework 或子工程中使用 xib

十、問題

  • 如果靜態庫中有 category 類,則在使用靜態庫的項目配置中【Other Linker Flags】需要添加參數[-ObjC] 或者 [-all_load]。

  • 出現 Umbrella header for module 'XXXX' does not include header 'XXXXX.h'

    因為把 xxxxx.h 錯誤的拖到了 public 中。

  • 出現 dyld: Library not loaded:XXXXXX

    是因為打包的 Framework 版本太高。比如打包 Framework 時,選擇的是 iOS 9.0,而實際的工程環境是 iOS 8 開始的。需要到 iOS Deployment Target 設置對應版本。

  • 報錯 "Include of non-modular header inside framework module"

    如果創建的 Framework 類中使用了 .dylib 或者 .tbd,首先需要在實際項目中導入 .dylib 或者 .tbd 動態庫,然后需要設置 Allow Non-modular Includes In Framework Modules = YES

  • 有時候我們會發現在使用的時候加載不了動態 Framework 里的資源文件,其實是加載方式不對,比如用 pod 的時候使用的是 use_frameworks!,那么資源是在 Framework 里面的,需要使用以下代碼加載(具體可參考給pod添加資源文件):

    NSBundle * bundle = [NSBundle bundleForClass:<#ClassFromFramework#>];
    NSString * path = [bundle pathForResource:@"imageName@2x"(@"bundleName.bundle/imageName@2x") ofType:@"png"];
    UIImage * image = [UIImage imageWithContentsOfFile:path];
  • 報錯 Reason: image not found

    如果直接在工程里使用創建的動態庫時候會出現此錯誤,需要在工程的 General 里的 Embedded Binaries 添加這個動態庫才能使用。

    因為創建的這個動態庫其實也不能給其他程序使用的,而你的 App Extension 和 APP 之間是需要使用這個動態庫的。這個動態庫可以 App Extension 和 APP 之間共用一份(App 和 Extension 的 Bundle 是共享的),因此蘋果又把這種 Framework 稱為 Embedded Framework。

十一、Swift 支持

跟着 iOS8/Xcode6 同時發布的還有 Swift。如果要在項目中使用外部的代碼,可選的方式只有兩種:1、把代碼拷貝到工程中;2、用動態 Framework。使用靜態庫是不支持的。

造成這個問題的原因主要是 Swift 的運行庫沒有被包含在 iOS 系統中,而是會打包進 App 中(這也是造成 Swift App 體積大的原因),靜態庫會導致最終的目標程序中包含重復的運行庫(這是蘋果自家的解釋)。同時拷貝 Runtime 這種做法也會導致在純 ObjC 的項目中使用 Swift 庫出現問題。蘋果聲稱等到 Swift 的 Runtime 穩定之后會被加入到系統當中,到時候這個限制就會被去除了(參考這個問題的問題描述,也是來自蘋果自家文檔)。

十二、CocoaPods 的做法

在純 ObjC 的項目中,CocoaPods 使用編譯靜態庫 .a 方法將代碼集成到項目中。在 Pods 項目中的每個 target 都對應着一個 Pod 的靜態庫。

18

當不想發布代碼的時候,也可以使用 Framework 發布 Pod,CocoaPods 提供了 vendored_framework 選項來使用第三方 Framework。

對於 Swift 項目,CocoaPods 提供了動態 Framework 的支持。通過 use_frameworks! 選項控制。對於 Swift 寫的庫來說,想通過 CocoaPods 引入工程,必須加入 use_frameworks! 選項

17

十三、關於 use_frameworks!

在使用 CocoaPods 的時候在 Podfile 里加入 use_frameworks!,那么你在編譯的時候就會默認幫你生成動態庫,我們能看到每個源碼 Pod 都會在 Pods 工程下面生成一個對應的動態庫 Framework 的 target,我們能在這個 target 的 Build Settings -> Mach-O Type 看到默認設置是 Dynamic Library,也就是會生成一個動態 Framework,我們能在 Products 下面看到每一個 Pod 對應生成的動態庫。

這些生成的動態庫將鏈接到主項目給主工程使用,但是我們上面說過動態庫需要在主工程 target 的 General -> Embedded Binaries 中添加才能使用,而我們並沒有在 Embedded Binaries 中看到這些動態庫。那這是怎么回事呢,其實是 cocoapods 已經執行了腳本把這些動態庫嵌入到了 .app 的 Framework 目錄下,相當於在 Embedded Binaries 加入了這些動態庫。我們能在主工程 target 的 Build Phase -> Embed Pods Frameworks 里看到執行的腳本。

19

所以 Pod 默認是生成動態庫,然后嵌入到 .app 下面的 Framework 文件夾里。我們去 Pods 工程的 target 里把 Build Settings -> Mach-O Type 設置為 Static Library。那么生成的就是靜態庫,但是 cocoapods 也會把它嵌入到 .app 的Framework目錄下,而因為它是靜態庫,所以會報錯:unrecognized selector sent to instanceunrecognized selector sent to instance。

十四、參考

創建一個 iOS Framework 項目
Xcode7創建靜態庫和Framework
iOS 靜態庫開發
靜態庫與動態庫的使用
iOS 靜態庫,動態庫與 Framework
簽名
iOS打包靜態庫(完整篇)
iOS armv7、armv7s、 arm64
iOS 創建 .a 和 .framework 靜態庫,以及 Bundle 資源文件的使用
iOS 靜態庫和動態庫(庫詳解)
齊滇大聖 & iOS里的動態庫和靜態庫


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM