iOS里的動態庫和靜態庫


介紹

  • 動態庫形式:.dylib和.framework

  • 靜態庫形式:.a和.framework

  • 動態庫和靜態庫的區別

靜態庫:鏈接時,靜態庫會被完整地復制到可執行文件中,被多次使用就有多份冗余拷貝(圖1所示)

系統動態庫:鏈接時不復制,程序運行時由系統動態加載到內存,供程序調用,系統只加載一次,多個程序共用,節省內存(圖2所示)

上圖中的綠框表示app的可執行文件。

動態庫的作用

應用插件化:

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

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

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

共享可執行文件:

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

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

Xcode6之后支持創建動態庫工程

Xcode6之后蘋果在iOS上開放了動態庫。

創建:File->New->Project

創建

我們上面說過Framework即可以是動態庫,也可以是靜態庫。那么我們上圖中默認創建的是動態庫,那么如何創建動態庫呢?比如我創建的framework叫testLib,然后在build setting中設置動態庫或靜態庫。如下圖,創建framework的時候默認是Dynamic Library,我們可以修改為Static Library

如果我們創建的framework是動態庫,那么我們直接在工程里使用的時候會報錯:Reason: Image Not Found。需要在工程的General里的Embedded Binaries添加這個動態庫才能使用。
因為我們創建的這個動態庫其實也不能給其他程序使用的,而你的App ExtensionAPP之間是需要使用這個動態庫的。這個動態庫可以App ExtensionAPP之間共用一份(App 和 Extension 的 Bundle 是共享的),因此蘋果又把這種 Framework 稱為 Embedded Framework,而我把這個動態庫稱為偽動態庫

具體創建靜態庫和Framework可以參考:Xcode7創建靜態庫和Framework

自己創建的動態庫

我們創建的動態庫和系統的動態庫有什么區別呢?我們創建的動態庫是在我們自己應用的.app目錄里面,只能自己的App ExtensionAPP使用。而系統的動態庫是在系統目錄里面,所有的程序都能使用。

可執行文件和自己創建的動態庫位置:

一般我們得到的iOS程序包是.ipa文件。其實就是一個壓縮包,解壓縮.ipa。解壓縮后里面會有一個payload文件夾,文件夾里有一個.app文件,右鍵顯示包內容,然后找到一個一般體積最大跟.app同名的文件,那個文件就是可執行文件。
而我們在模擬器上運行的時候用NSBundle *bundel = [[NSBundle mainBundle] bundlePath];就能得到.app的路徑。可執行文件就在.app里面。

而我們自己創建的動態庫就在.app目錄下的Framework文件夾里。

下圖就是測試工程DFCUserInterface.app的目錄

我這里用了一個測試工程,即有系統的動態庫(WebKit),又有自己的動態庫(DFCUserInterface),我們可以看一下可執行文件中對動態庫的鏈接地址。用MachOView查看可執行文件。其中@rpth這個路徑表示的位置可以查看Xcode 中的鏈接路徑問題,而現在表示的其實就是.app下的Framework文件夾。

下圖表示了靜態庫,自己創建的動態庫和系統動態庫:


簽名

系統在加載動態庫時,會檢查 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/youFramework.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。

我們在用cocoapodsuse_framework!的時候生成的動態庫也可以用codesign -dv /path/to/youFramework.framework查看到TeamIdentifier=not set。關于動態庫的簽名TeamIdentifier等之前沒接觸過,可以再去查看一下資料。

關于Framework

  • framework為什么既是靜態庫又是動態庫?

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

  • 什么是framework

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

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

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

創建靜態Framework

1.選擇Framework

創建

2.選擇為靜態庫


3.生成對應版本的靜態庫

靜態庫的版本(4種)

  • 真機-Debug版本
  • 真機-Release版本
  • 模擬器-Debug版本
  • 模擬器-Release版本

這里debug或release是否生成符號表,是否對代碼優化等可以在如何加快編譯速度查看。

我們選擇Release版本。編譯模擬器和真機的所有CPU架構。

然后選擇模擬器或者Generic iOS Device運行編譯就會生成對應版本的Framework了。


4.合成包含真機和模擬器的Framework

終端cd到Products,然后執行以下代碼,就會在Products目錄下生成新的包含兩種的執行文件,然后復制到任何一個testLib.framework里替換掉舊的testLib就可以了。

lipo -create Release-iphoneos/testLib.framework/testLib  Release-iphonesimulator/testLib.framework/testLib  -output testLib

或者在工程的Build Phases里添加以下腳本,真機和模擬器都Build一遍之后就會在工程目錄下生成Products文件夾,里面就是合并之后的Framework。

if [ "${ACTION}" = "build" ]
then
INSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}.framework

DEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework

SIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework


if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi

mkdir -p "${INSTALL_DIR}"

cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
#ditto "${DEVICE_DIR}/Headers" "${INSTALL_DIR}/Headers"

lipo -create "${DEVICE_DIR}/${PROJECT_NAME}" "${SIMULATOR_DIR}/${PROJECT_NAME}" -output "${INSTALL_DIR}/${PROJECT_NAME}"

#open "${DEVICE_DIR}"
#open "${SRCROOT}/Products"
fi

Framework目錄

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

  • info.plist
    主要就是這個Framework的一些配置信息。

  • Modules
    這個文件夾里有個module.modulemap文件,我們看到這里面有這樣一句umbrella header "testLib.h",umbrella有保護傘、庇護的意思。
    也就是說Headers中暴露的testLib.h文件被放在umbrella雨傘下保護起來了,所以我們需要將其他的所有需要暴露的.h文件放到testLib.h文件中保護起來,不然會出現警告。@import的時候也只能找到umbrella雨傘下保護起來的.h文件。

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

  • .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,比如我有一個pod叫做testLib,那么就會有一個叫testLibtarget,最后這個target生成的就是testLib.framework
那么如果這個pod有資源文件的話,就會有一個叫testLib-bundleNametarget,最后這個target生成的就是bundleName.bundle

上面創建靜態Framework例子里生成資源文件

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

靜態Framework里不需要加入資源文件

一般如果是靜態Framework的話,資源打包進Framework是讀取不了的。靜態Framework和.a文件都是編譯進可執行文件里面的。只有動態Framework能在.app的Framework文件夾下看到,并讀取.framework里的資源文件。

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

在framework或子工程中使用xib

問題

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

  • 如果使用framework的使用出現【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設置對應版本。

  • 如果創建的framework類中使用了.dylib或者.tbd,首先需要在實際項目中導入.dylib或者.tbd動態庫,然后需要設置【Allow Non-modular Includes ....】為YES,否則會報錯"Include of non-modular header inside framework module"。

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

NSBundle *bundle = [NSBundle bundleForClass:<#ClassFromFramework#>];
[UIImage imageWithContentsOfFile:[bundle pathForResource:@"imageName@2x"(@"bundleName.bundle/imageName@2x") ofType:@"png"]];

Swift 支持

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

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

CocoaPods 的做法

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

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

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

關于 use_frameworks!

在使用CocoaPods的時候在Podfile里加入use_frameworks! ,那么你在編譯的時候就會默認幫你生成動態庫,我們能看到每個源碼Pod都會在Pods工程下面生成一個對應的動態庫Framework的target,我們能在這個targetBuild 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里看到執行的腳本。

所以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
簽名

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容