(iOS)年輕人,聽說你想使用Framework - 基礎觀念

jamesdouble發表於2018-12-12

Frameworks

本文為譯文,並已取得作者Nick Teissler同意。 原文連結:原文

Framework這小子問了幾個很好的問題

本文章適合初學Framework的讀者。

前言

Apple 已經將 iOS, macOS 的程式碼分成 Modules, libraries, frameworks。

Frameworks 的設計不單單只是為了封裝資源跟模組化程式碼,更不只是為了減少程式碼的重編譯時間而已。

要想減輕程式碼量、加速Debug、增加程式碼複用性,就不能只知道Framework是一個可以拖來拖去的工具箱,必須更近一步的瞭解這些:

  • 靜態庫 - Static Libraries
  • 動態庫 - Dynamic Libraries
  • Framework的結構
  • Linking 連結 與 Embed 嵌入的不同
  • Q & A

以下內容適用 macOS, tvOS, iOS. 可能會隨時間有改動。

靜態庫 Static Libraries

在靜態庫之前,我們要先從Object File說起,以基礎的角度來說,object files 就是個有結構的位元塊。這些位元塊包含著一些程式程式碼、有些包含著準備給Linker跟Loader使用的資料結構。

可以試著在終端輸入以下指令,瞄一眼objectfile的樣子

objdump -macho -section-headers /bin/ls
複製程式碼

Objectfiles通常以四種形式登場

  • Relocatable: 包含可以在編譯時可被其他Relocatable Link的程式碼和資料,以共同製作成Executable。
  • Executable:準備好載入記憶體的可執行檔。
  • Shared: 一種特殊的Relocatable file,概念類似 動態庫。
  • Bundle: 我們不特別描述Bundle, 在macOS上通常當作外掛使用。

譯文外補充: Relocatable:包含著能與其他relocatable在編譯時進行Linked的程式碼(執行程式碼)以及資料(變數全域性資料)。 Shared: 編譯時只依Header作api上的確認,不包進任一Executable,共享於Executable之間。

若你嘗試編譯一段沒有 main函式的 C 程式碼,會得到一個 relocatable object file

fancy.c

void fancySwap(int *xp, int *yp) {
    if (xp == yp) return; // try it!
    *xp = *xp ^ *yp;  
    *yp = *xp ^ *yp;  
    *xp = *xp ^ *yp;  
}
複製程式碼

在Terminal輸入

cc -c fancy.c
複製程式碼

產出的 fancy.o 是一種 objectFile ,包含著 fancySwap 符號以及對應的實現。為了更方便地處理 Relocatable object files,多個object files能被封裝成 .a (a 代表著 archive) .a 檔也被稱作 靜態庫(static library) 或 靜態歸檔(static archive)。

當executable中某function 或 資料來自於 .a,linker是能夠智慧地只連結其對應的符號,但是,linker還是會將所有的這些.a裡的程式碼給一個固定的load地址幷包含(copies and relocates)進executable中,造成肥大的executable,且增長讀進memory的時間。

當你有10個應用使用此靜態庫,就需要個別有一份copy,也就是十個複本!而且若是靜態庫的開發者更新了程式碼,則所有的應用都必須重新編譯一遍。而動態庫就是為了解決這一步便利性而誕生的。

動態庫 Dynamic Libraries

動態庫 (也稱作Shared Library, Shared object, 動態連結庫), 跟靜態庫一樣是多個object files封裝起來的,不一樣的是動態庫只有在程式的載入時間(load time)或執行時(Run time)會被載入,並且在memory隨機的配發一段地址。

以上行為是由動態連結器(Dynamic linker, macOS稱dyld)來完成,動態庫在:

  • macOS上 以 .dylib 存在
  • windows上 以 .DLL 存在
  • linux上 以 .so 存在

然而在執行時進行才做連結其實是一個笨重的負擔,應合理安排哪些庫需要Load以及時機。

在 Treminal輸入以下指令檢視更多關於動態載入的內容

man dlopen
複製程式碼

Shared object 解決了前面static object files的問題

  • 現在多個executable只動態連結到 動態庫的『唯一』拷貝。
  • 更新庫並不影響executable
  • 庫的依賴會被自動載入且連結(依賴庫必須在search path裡)

macOS 大規模的使用shared libraries,可以前往路徑 /usr/lib資料夾檢視系統的動態庫。

如果你想要使用自己的動態庫,必須確保它被 embedded 進你的App裡。

看完上面這段,你可能會想 “太好了, 我不需要重編譯了,但我最好為我庫的使用者提供相容性”。

是的沒錯,你的確需要考慮每當你update動態庫時,API的相容性,或許這也是你正在看這篇文章的原因.. 學習一種具有地址相容性的bundling格式 - Framework

Framework

一個Framework其實就是一個有著特定結構的資料夾裝著各種共享的資源。

這些資源通常是 圖片、Xibs、動態庫、靜態庫、文件...等,他們被封裝成bundle儲存卻又不像其他的bundles( 像是 .app),Framework毫不掩飾的表明它純粹就是一個資料夾。

這裡指的Bundle並不是上面所提及的Bundle Object

Framework 以及其組成的子資料夾

Headers

這資料夾包含了Framework對外公開的C & Obj-C headers,Swift 並不會用到這些Headers,如果你的framework是用Swift寫的,Xcode會自動幫你建立這個資料夾以提供互用性。

若你有時不清楚在Swift程式碼里加 @objc, @objcMembers 的影響,可以檢視 Build Settings 裡“SWIFT_OBJC_INTERFACE_HEADER_NAME”所指的檔案,並試著改動@objc @objcMembers的宣告,看看這些改動對這檔案的影響。

Modules

這資料夾包含了LLVM, Swift 的 Module資訊。

.modulemap檔案是給Clang使用的。

關於Clang 可看譯者的另一篇文 LLVM前端Clang

.swiftmodule 資料夾下的檔案類似headers,但是不像是 headers, 這些檔案是二進位制的且“無格式也有可能會改變”,在你Cmd-click 一個Swift函式時Xcode就是利用這些檔案去定位其所屬的module。

儘管這些都是二進位制檔案,但他們仍是一種叫 llvm bitcode 的結構,正因如此,我們能用llvm-bcanalyzer and llvm-strings取得相關資訊。

MyCustomFramework

雖然他被finder標註成Unix executable,但他其實是一個 relocatable shared object file

Resources

本地化的資源, xibs, 檔案, 圖片... 跟其它資源存放在這

圖片裡的箭頭 (Framework版本)

這些箭頭都是 symlink 符號連結(捷徑)。

每當產生一個新版本時,會被放進 Versions/B. Current 的箭頭會被更新成指向 B。

與此同時,原本動態連結到A的程式仍會保持連結A而不是Current,怎麼辦到的?

每當executable 在編譯時, dynamic linker會紀錄與自己有相容的Framework路徑。

But

現今Apple已鮮少使用這個功能了

Xcode 裡 Linking vs. Embedding Frameworks

這裡我們來討論Xcode裡一些常見的設定,Target的 general settings.

Xcode's Target General Settings View

通常需要使用Framework時,都會選擇 “Linked Frameworks and Libraries”,除非你打算在 runtime的時候才做連結跟載入(前面提及的dlopen)。

Linking 和 Embedding 的差別是什麼?

為什麼當你Embed一個Framework時,Xcode自動將Framework加到Link區?

想一下剛剛在讀過的,dylibs儲存在哪?當你想要用自己的dylib時該做什麼?


是的!Embedding 實際上將Framework拷貝一份到你的application bundle裡Frameworks資料夾下,

系統庫是 iOS 和 macOS 自帶的,你可以放心的連結他們,但是你自己的Framewroks並不是系統自帶的,因此你必須要embedded 到 application bundle裡。

而你的應用若是沒有Linking它也沒有任何用,所以Xcode自動幫你做了連結。

★★ Linking and embedding間接的暗示 動態 或是 靜態連結,

我們現在知道embedding一個靜態庫是不合理的,因為靜態庫的符號已經被編譯進executable,所以 Xcode不會應讓你將static library放到 Embed

(但其實Xcode會讓你放,然而這是很沒效率的使用,所以你不該這麼做)

技術上來講,macOS是可以讓你在runtime時去載入一個靜態的 .a file的,並標記一塊記憶體當作是 executable,

這樣做能有效的載入靜態庫,然而這技術是由 JIT compilers完成的,iOS並沒有標記記憶體的能力。

★★

聽說你想要使用 Framework

在你使用Framework或是將自己的程式碼做成Framework時,有哪些因素要考慮的?

  • 你要開發一組應用嗎?如果是,這就是Framework本質的一部分 - 分享 Frameworks,你需要去建一個包安裝器把 shared frameworks放進 /Library/Frameworks or ~/Library/Frameworks.
  • 你的多個apps共享著些許的重複程式碼嗎?使用framework或許不是最好的選擇,如果只是要共享一個非常小量的程式碼,為了保持同步的 打包和版本控制反而會成為負擔。
  • 你想試著模組化你的程式碼嗎?如果你想要將程式碼移進Framework並將它變成模組,最好注意 Swift’s access control.
  • 別將Framework當作能不重新編譯的萬靈丹,有些專案會將程式碼中極大量快完成的部分移動到Framework以提升編譯速度,這樣做的確能降低時間,如我們所知Framework不需重新編譯,然而這樣的方式會使模組更復雜,Debug, 測試難度提升。
  • 你要將程式碼做成第三方包給其他人使用嗎?這情況下,你可以選擇使用動態庫 靜態庫 或Framework。macOS, iOS已逐漸習慣動態Framewrok了,因為他的輕便性、普遍以及易用性。

Sources

相關文章