天天看點

元件化-動态庫實戰

原文位址 ,此簡書隻做備份,強烈推薦原文,畢竟格式比簡書好看,還清晰

起因

去年,鍊家網iOS端,之前由于所有的業務端代碼都是混亂管理,造成開發有很多痛點<code>無法單測</code> <code>團隊成員送出代碼沖突機率大</code> <code>CI配合效果差</code> <code>功能性代碼多端無法複用</code> <code>單倉庫代碼量大</code> <code>編譯時間長</code> 等等痛點,上司群組内多次溝通開始着手元件化開發,希望能改進這些開發中的痛點,成立元件化團隊。 元件化的方案大同小異,基礎性代碼封裝私有庫,業務元件互動交由中間件負責,項目依賴工具用 iOS項目事實上的标準 <code>CocoaPods</code> 前期的基礎性元件拆分都較為順利,從依賴樹的葉子節點開發是最合适的方案。 随着元件抽離的越來越多,私有庫的依賴體系也越來越複雜,慢慢過渡到了業務元件。業務元件用了Swift的第三方元件,用了Swift庫的同學都知道必須加上<code>use_frameworks!</code>,這個标記是說Pod管理的依賴全部編譯為<code>動态庫</code>,然後呢我們的很多元件又依賴了諸如百度地圖,微信分享等<code>靜态庫</code>,于是我在執行 <code>pod install</code> 報了一個沒有碰見過的錯誤
元件化-動态庫實戰

installError

這就尴尬了,于是一陣瘋狂的搜尋 google stackoverflow 等,然而并沒有什麼卵用,而且上面催得急,根本沒時間處理這些<code>小問題</code> 業務重構是最主要的,以至于我們的業務元件沒有做到獨立倉庫拆分。 直到最近終于找到了解決辦法:( 主要是自己的功力不夠深厚

理論功底

首先靜态庫和動态庫都是以二進制提供代碼複用的代碼庫

靜态庫 常見的是 <code>.a</code>

動态庫常見的是 <code>.dll(windows)</code> <code>.dylib(mac)</code> <code>so(linux)</code>

framework(in Apple): Framework是Cocoa/Cocoa Touch程式中使用的一種資源打包方式,可以将代碼檔案、頭檔案、資源檔案、說明文檔等集中在一起,友善開發者使用。

也就是說我們的framework其實是資源打包的方式,和靜态庫動态庫的本質是沒有關系的

靜态庫: 連結時會被完整的複制到可執行檔案中,是以如果兩個程式都用了某個靜态庫,那麼每個二進制可執行檔案裡面其實都含有這份靜态庫的代碼

動态庫: 連結時不複制,在程式啟動後用dyld加載,然後再決議符号,是以理論上動态庫隻用存在一份,好多個程式都可以動态連結到這個動态庫上面,達到了節省記憶體(不是磁盤是記憶體中隻有一份動态庫),還有另外一個好處,由于動态庫并不綁定到可執行程式上,是以我們想更新這個動态庫就很容易,windows 和linux上面一般插件和子產品機制都是這樣實作的。

But我們的蘋果爸爸在iOS平台上規定不允許存在動态庫,并且所有的IPA都需要經過蘋果爸爸的私鑰加密後才能用,基本你用了動态庫也會因為簽名不對無法加載,(越獄和非APP store除外)。于是就把開發者自己開發動态庫掐死在幻想中。

直到有一天,蘋果爸爸的iOS更新到了8,iOS出現了<code>APP Extension</code>,<code>swift</code>程式設計語言也誕生了,由于iOS 主APP需要和Extension共享代碼,Swift語言的機制也隻能有動态庫,于是蘋果爸爸尴尬了,不過這難不倒我們的蘋果爸爸,畢竟我是爸爸,規則是我來定,我想怎樣就怎樣,于是提出了一個概念<code>Embedded Framework</code>,這種動态庫允許<code>APP</code> 和 <code>APP Extension</code>共享代碼,但是這份動态庫的生命被限定在一個APP程序内。簡單點可以了解為 被閹割的動态庫。

如果你把某個自己開發的動态庫(系統的不算,畢竟蘋果是爸爸)放在了<code>Linked Frameworks and Libraries</code>裡面,程式一啟動就會報<code>Reason: Image Not Found</code>,你隻能把它放在<code>Embeded Binaries</code>裡面才能正常使用,

看圖:

元件化-動态庫實戰

簡單點,說話的方式簡單點~~

上面的介紹貌似有點抽象啊 套用在美團技術分享大會上的話就是:

靜态庫: 一堆目标檔案(.o/.obj)的打包體(并非二進制檔案)

動态庫: 一個沒有main函數的可執行檔案

這裡我們來複習下C語言的基本功,編譯和連結

編譯:将我們的源代碼檔案編譯為目标檔案

連結:将我們的各種目标檔案加上一些第三方庫,和系統庫連結為可執行檔案。

由于某個目标檔案的符号(可以了解為變量,函數等)可能來自其他目标檔案,其實連結這一步最主要的操作就是 決議符号的位址。

若符号來⾃靜态庫(本質就是.o的集合包)或 .o,将其納⼊連結産物,并确定符号位址

若符号來⾃動态庫,打個标記,等啟動的時候再說---交給dyld去加載和連結符号

于是連結加裝載就有了不同的情況

Load 裝載:将庫⽂件載⼊記憶體

Static Loading:啟動時

Dynamic Loading:啟動後(使⽤時)

Link 連結:決議符号位址

Static Linking:建構(連結)時

Dynamic Linking:運⾏時(啟動時或使⽤時)

然後組合起來就是2*2 = 4了

Static Loading + Static Linking

Static Loading + Dynamic Linking

Dynamic Loading + Dynamic Linking

Dynamic Loading + Static Linking

第一種是純靜态庫相關了

第二種就是靜态加載(啟動時),動态連結 ,連結時,動态庫參與連結,但是這時候隻是給符号打了标記告訴我這個符号來自與動态庫,程式啟動時,iOS或者Mac OS作業系統的dyld自動 load+link。

既然全部都是自動的。那麼符号的調用方完全不知道你到底是源碼還是靜态庫,動态庫 。

第三種收到調用dlopen + performSelector 通常iOS的APP不适用這裡不讨論

第四種,沒見過,個人也不是特别懂

有需求請參看文後的<code>程式員的自我修養</code>一書

既然有2種庫,那麼依賴關系又是2*2喽

libA.a dependency libB.a

UIKit.dylib dependency Foundation.dylib

libA.a dependency Foundation.dylib

MyXX.dylib dependency libA.a

第一種 靜态庫互相依賴,這種情況非常常見,制作靜态庫的時候隻需要有被依賴的靜态庫頭檔案在就能編譯出來。但是這就意味者你要收到告訴使用者你的依賴關系

幸運的是 <code>CocoaPod</code>就是這樣做的

第二種動态庫依賴動态庫,兩個動态庫是互相隔離的具有<code>隔離性</code>,但是制作的靜态庫的時候需要被依賴動态庫參與連結,但是具體的符号決議交給<code>dyld</code>來做。

第三種,靜态庫依賴動态庫,也很常見,靜态庫制作的時候也需要動态庫參與連結,但是符号的決議交給dyld來做。

第四種,動态庫依賴靜态庫,這種情況就有點特殊了。首先我們設想動态庫編譯的時候需要靜态庫參與編譯,但是靜态庫交由dyld來做符号決議,but 這和我們前面說的就沖突了啊。靜态庫本質是一堆.o的打包體,首先并不是二進制可執行檔案,再者你無法保證主程式把靜态庫參與連結共同生成二進制可執行檔案。這就尴尬了。

怎麼辦?

目前的編譯器的解決辦法是,首先我無法保證主程式是否包含靜态庫,再者靜态庫也無法被<code>dyld</code>加載,那麼我直接把你靜态庫的.o偷過來,共同組成一個新的二進制。也被稱做<code>吸附性</code>

那麼我有多份動态庫都依賴同樣的靜态庫,這就尴尬了,每個動态庫為了保證自己的正确性會把靜态庫吸附進來。然後兩個庫包含了同樣的靜态庫,于是問題就出現了。 看到這裡想必前面出現的錯誤你已經能猜出來了把_

後面再詳細解釋

先來個總結

可執⾏⽂件(主程式或者動态庫)在建構的連結階段

遇到靜态庫,吸附進來

遇到動态庫,打标記,彼此保持獨⽴

target-對于一個産物(app,.a ,.framework)

project-一個項目包含多個target

workspace:一個包含多個target

schema: 指定了一個産物是按照何種的依賴關系,編譯-連結到最終的一個産物

這麼多年,Apple的部落格和文檔也就告訴了我們什麼是靜态庫 什麼是動态庫,如何制作等。但是并沒有給我們提供一系列的依賴管理工具。是以CocoaPods成了事實上的标準。

通常CocoaPods管理的工程結構如下:

那麼當我們按下CMD+B的時候,整個項目按照先編譯被依賴Pod,然後依賴其他Pod的Pod也被建構出來,最終所有的元件被編譯為一個<code>lib-Pods-XXXAPP.a</code>被添加進項目進去。資源通過CocoaPods提供的腳本也一并被複制進去。想了解CocoaPods做了什麼的讀者可以參看後面的連結

這麼多理論功底的建立,相信我們已經能分析出來之前<code>pod install</code>的原因了。就是用了<code>use_framework</code>那麼我們的所有Pod都會以動态庫(Embeded Framework)的形式去建構,于是那些非開源的庫(如 百度地圖,微信分享)如果被多個Pod依賴(元件化開發中太常見了)于是被吸附到動态庫裡面,是以CocoaPod直接就不讓我們install成功。因為你現在的依賴管理就是錯誤的。

在聽取美團葉樉老師分享的時候 他們的出發點是因為要繞過蘋果爸爸在iOS9以下對__text段60M的限制使用了動态庫方案,我們是因為某些swift庫必須要用到(曆史遺留原因)動态庫。美團的做法是摘除依賴關系,自定義CocoaPods(開源的本來就是用着不爽我就改)。但是我是個小菜雞啊。我也不會ruby(以後會學的),但是葉樉老師給我提了别的idea。 前面我們知道 動态庫和動态庫是<code>隔離性</code>,動态庫依賴靜态庫具有<code>吸附性</code>,那麼我們可以自定義一個動态庫把百度地圖這種靜态庫吸附進來。對外整體呈現的是動态庫特性。其他的元件依賴我們自定義的動态庫,由于<code>隔離性</code>的存在,不會出現問題。

1 建立動态庫項目這裡以wx舉例

元件化-動态庫實戰

2 按照微信的官方文檔。添加依賴庫(我是因為pod install巨慢 是以我直接拽進來了)

元件化-動态庫實戰

3 将wx的PublicHeader暴露出來,注意由于我并沒有使用到wx相關API是以連結器幫我們連結動态庫 的時候可能并不會把wx靜态庫吸附進來。我們手動在build Setting的other link flags加上<code>-all_load</code>标記

元件化-動态庫實戰

4.在Schema裡面跳轉編譯配置為Release ,并且選擇所有的CPU架構

元件化-動态庫實戰
元件化-動态庫實戰

5 然後選擇模拟器或者Generic iOS Device運作編譯就會生成對應版本的Framework了。

元件化-動态庫實戰

6.但是為了保證開發者使用的時候是真機模拟器都能正常使用,我們需要合并不同架構

這裡在<code>Build Phases</code>裡添加以下腳本,真機和模拟器都Build一遍之後就會在工程目錄下生成Products檔案夾,

元件化-動态庫實戰

于是我們有了我們自己的私有動态庫LJWXSDK,那麼我們來驗證我們之前的問題

首先指定一個LJWXSDK.podspec這裡我直接傳到了我的Github上面

注意上面我是把二進制壓縮丢進了七牛的oss檔案存儲。畢竟免費還快。

然後通過pod lib create建立了一個pod用來驗證之前我們的傳遞性依賴問題,

檔案夾結構如下

元件化-動态庫實戰
元件化-動态庫實戰
元件化-動态庫實戰

測試工程我也丢在7牛上面。下載下傳測試即可

編譯運作。完美。我們又可以愉快的和swift第三方庫配合使用。

很多人可能會問 諸如百度地圖 微信這種sdk為什麼官方不支援動态庫版(所說的都是embeded Framework),猜測是為了相容更低iOS7版本吧

很多人會覺得麻煩的要死。首先每個公司多多少少都有曆史包袱,麻煩也要做,再者這是一次對基本功的補充,即便你們沒有用到,但是為了學習,這篇教程所做的也值得你嘗試一次。

上述解決了我們一開始遇到的問題。but既然動态庫和靜态庫壓根就不一回事,是以裡面還是有很多細節值得我們去了解的。

首先我們之前記得如果一個動态庫加在<code>LinkedFrameworksand Libraies</code>程式啟動就會報ImageNotFound,如果放在<code>EmbededBinaries</code>裡面就可以。這是為什麼呢。我們拿MacoView來看下兩種情況下可執行檔案的細節

元件化-動态庫實戰
元件化-動态庫實戰
元件化-動态庫實戰

其中@rpth這個路徑表示的位置可以檢視Xcode 中的連結路徑問題

這樣我們就知道了其實加在<code>EmbededBinaries</code>裡面的東西其實會被複制一份到xx.app裡面,是以這個名字起得還是不錯的直譯就是<code>嵌入的架構</code>

造成這個的主要原因是Swift的運作時庫(不等同于OC的runtime概念),由于Swift的ABI不穩定,靜态庫會導緻最終的目标程式中包含重複的運作庫,相關可以看下最後的參考文章SwiftInFlux#static-libraries。等到我們的SwiftABI穩定之後,我們的靜态庫支援可能就又會出現了。當然也可能不出,Swift伴随誕生的SPM(Swift,Package Manager),可能有更好的<code>官方的</code>包依賴管理工具。讓我們期待吧。

既然加了Swift的第三方庫之後就需要在<code>Podfile</code>裡面加上<code>use_framework!</code> 那麼CocoaPods就會幫我們生成動态庫,但是奇怪的是,我們并沒有在主工程的<code>embeded binaries</code>看到這個動态庫,這又是什麼鬼。其實是CocoaPods使用腳本幫我們加進去了。腳本位置在主工程的 <code>build Phase</code>下的 <code>Emded Pods frameworks</code>

Headers 一般是頭檔案。非private裡面的頭檔案都會在裡面

info.plist 配置資訊,不深究

Modules 這個檔案夾裡有個module.modulemap檔案,後面在講解

二進制檔案,這就是上面提到的<code>不帶 main的二進制檔案了</code>,.o的打包體

_codeSignature 簽名檔案 (蘋果爸爸的限制)

more 資源檔案。這裡暫時沒用到,是以沒有 ,但是這個也是個大坑

更愉快的導入檔案

<code>@class,@protocol</code> 不說了就是聲明一個類,并不導入。

<code>#import &lt;&gt;, #import""</code>是加強版的<code>#include&lt;&gt;,#include""</code> 防止重複導入的。

<code>#import&lt;&gt;</code> : 通過 build setting裡面中的 header Search Path裡面去找

<code>#import"" :</code> 第一步先搜尋user Header search Path 再搜尋 header search Path 。是以對我們的framework來說,<code>CocoaPod</code> 幫我們加到了 Header search Path 目前2種導入方式都是可以支援的。

上面的導入方式都帶了 某個framework的路徑 &lt;XX/xx.h&gt; "xx/xx.h" ,我們在開發自己主工程的時候會發現我們導入主工程其他類是不需要導入字首的。 這又是怎麼回事。

看下面的配置

元件化-動态庫實戰

目前的配置是non-recursive。如果把non去掉意思就是我可以遞歸的去查找某些framework下面的頭檔案了。 但是Xcode的效率肯定就會有影響。

還是不建議修改的好。

大家都知道iOS7之後多了@import,這又是什麼鬼。

簡單了解這個方式叫做Module導入,好處就是使用了@import之後不需要在project setting手動添加 framework,系統會自動加載,而且效率更高。

最主要的是swift也隻能這樣用。

導入的時候系統會查找如果有子產品同名的檔案就會導入這個檔案。如果沒有CocoaPods幫我們生成一個<code>module-umbrela.hl</code>檔案,然後就是導入的這個檔案。

回過頭來看我們的framework的結構 裡面有個<code>Modules</code>檔案夾,裡面有個檔案<code>module.modulemap</code>

我們可以看到其實被暴露的header就是這個檔案,之前我在按照<code>#import "/"</code>的時候有個警告

元件化-動态庫實戰

而且按照@import導入的東西發現沒有導入可用的頭檔案就是因為并沒有在 umbrella header的頭檔案中加入其他頭檔案。

加入之後我們就可以完美的使用<code>@import</code> ,并且<code>#import"/"</code> 也不會報warning

更多關于<code>umbrella Header</code> 參看文後參考

首先我們來看常見的資源檔案:主要分為圖檔和其他類資源那麼加載圖檔和加載其他資源都是怎麼做的?

1: <code>[UIimage imageNamed:]</code>

2: <code>[NSbundle bundleForclass[XXX class]]</code>

其實方式1去本質就是去<code>mainBundle</code>去拿資源,方式2從<code>XXX</code>所在的架構裡面去拿。

前面也說道framework隻是資源的打包方式,本質上是有兩種的。

我們這個framework如果本質是靜态庫,那麼無需改變使用方式,資源最終都會打包到<code>Main Bundle</code>裡面

如果我們這個framework本質是動态庫,那麼我們的資源就發生了變化,資源就會被存放在 framework裡面。是以我們需要使<code>[NSbundle bundleForclass[XXX class]]</code>。需要注意的是很多人為了簡單,下意識的使用<code>self class</code> 傳遞,但是有可能這個<code>self執行個體</code>不在資源所屬的framework。是以會出現資源加載 失敗。一定要謹慎使用。

參考

程式員的自我修養,連結,裝載 和庫

iOS裡的動态庫和靜态庫

Systems Programming: What is the exact difference between Dynamic loading and dynamic linking?

CocoaPods 都做了什麼?

Dynamic Linking of Imported Functions in Mach-O

OS裡的導入頭檔案

iOS - Umbrella Header在framework中的應用

SwiftInFlux#static-libraries

iOS裡的導入頭檔案

@import vs #import - iOS 7

作者:南栀傾寒

連結:https://www.jianshu.com/p/7f6a7e1b3235

來源:簡書

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

繼續閱讀