1. 程式人生 > >iOS裡的動態庫和靜態庫

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:@"[email protected]"(@"bundleName.bundle/[email protected]") 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
簽名



作者:齊滇大聖
連結:https://www.jianshu.com/p/42891fb90304
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。