資源問題 起因 去年,公司iOS端,之前由于所有的業(yè)務(wù)端代碼都是混亂管理,造成開發(fā)有很多痛點(diǎn)無法單測,團(tuán)隊(duì)成員提交代碼沖突機(jī)率大,CI配合效果差,功能性代碼多端無法復(fù)用,單倉庫代碼量大,編譯時(shí)間長 等等痛點(diǎn),領(lǐng)導(dǎo)和組內(nèi)多次溝通開始著手組件化開發(fā),希望能改進(jìn)這些開發(fā)中的痛點(diǎn),成立組件化團(tuán)隊(duì)。 組件化的方案大同小異,基礎(chǔ)性代碼封裝私有庫,業(yè)務(wù)組件交互交由中間件負(fù)責(zé),項(xiàng)目依賴工具用iOS項(xiàng)目事實(shí)上的標(biāo)準(zhǔn)CocoaPods 前期的基礎(chǔ)性組件拆分都較為順利,從依賴樹的葉子節(jié)點(diǎn)開發(fā)是最合適的方案。 隨著組件抽離的越來越多,私有庫的依賴體系也越來越復(fù)雜,慢慢過渡到了業(yè)務(wù)組件。業(yè)務(wù)組件用了Swift的第三方組件,用了Swift庫的同學(xué)都知道必須加上use_frameworks!,這個(gè)標(biāo)記是說Pod管理的依賴全部編譯為動(dòng)態(tài)庫,然后呢我們的很多組件又依賴了諸如百度地圖,微信分享等靜態(tài)庫,于是我在執(zhí)行 pod install 報(bào)了一個(gè)沒有碰見過的錯(cuò)誤。
這就尷尬了,于是一陣瘋狂的搜索google stackoverflow等,然而并沒有什么卵用,而且上面催得急,根本沒時(shí)間處理這些小問題 業(yè)務(wù)重構(gòu)是最主要的,以至于我們的業(yè)務(wù)組件沒有做到獨(dú)立倉庫拆分。 直到最近終于找到了解決辦法:( 主要是自己的功力不夠深厚) 理論功底 動(dòng)態(tài)庫和靜態(tài)庫 介紹 首先靜態(tài)庫和動(dòng)態(tài)庫都是以二進(jìn)制提供代碼復(fù)用的代碼庫
靜態(tài)庫和動(dòng)態(tài)庫的區(qū)別 靜態(tài)庫: 鏈接時(shí)會(huì)被完整的復(fù)制到可執(zhí)行文件中,所以如果兩個(gè)程序都用了某個(gè)靜態(tài)庫,那么每個(gè)二進(jìn)制可執(zhí)行文件里面其實(shí)都含有這份靜態(tài)庫的代碼 動(dòng)態(tài)庫: 鏈接時(shí)不復(fù)制,在程序啟動(dòng)后用dyld加載,然后再?zèng)Q議符號,所以理論上動(dòng)態(tài)庫只用存在一份,好多個(gè)程序都可以動(dòng)態(tài)鏈接到這個(gè)動(dòng)態(tài)庫上面,達(dá)到了節(jié)省內(nèi)存(不是磁盤是內(nèi)存中只有一份動(dòng)態(tài)庫),還有另外一個(gè)好處,由于動(dòng)態(tài)庫并不綁定到可執(zhí)行程序上,所以我們想升級這個(gè)動(dòng)態(tài)庫就很容易,windows和linux上面一般插件和模塊機(jī)制都是這樣實(shí)現(xiàn)的。 But我們的蘋果爸爸在iOS平臺(tái)上規(guī)定不允許存在動(dòng)態(tài)庫,并且所有的 IPA 都需要經(jīng)過蘋果爸爸的私鑰加密后才能用,基本你用了動(dòng)態(tài)庫也會(huì)因?yàn)楹灻粚o法加載,(越獄和非 APP store 除外)。于是就把開發(fā)者自己開發(fā)動(dòng)態(tài)庫掐死在幻想中。 直到有一天,蘋果爸爸的iOS升級到了8,iOS出現(xiàn)了APP Extension,swift編程語言也誕生了,由于iOS主APP需要和Extension共享代碼,Swift語言的機(jī)制也只能有動(dòng)態(tài)庫,于是蘋果爸爸尷尬了,不過這難不倒我們的蘋果爸爸,畢竟我是爸爸,規(guī)則是我來定,我想怎樣就怎樣,于是提出了一個(gè)概念Embedded Framework,這種動(dòng)態(tài)庫允許APP和APP Extension共享代碼,但是這份動(dòng)態(tài)庫的生命被限定在一個(gè)APP進(jìn)程內(nèi)。簡單點(diǎn)可以理解為被閹割的動(dòng)態(tài)庫。 舉個(gè)例子,iOS項(xiàng)目中使用Embeded Framework 如果你把某個(gè)自己開發(fā)的動(dòng)態(tài)庫(系統(tǒng)的不算,畢竟蘋果是爸爸)放在了Linked Frameworks and Libraries里面,程序一啟動(dòng)就會(huì)報(bào)Reason: Image Not Found,你只能把它放在Embeded Binaries里面才能正常使用, 看圖: 靜態(tài)庫和動(dòng)態(tài)庫如何構(gòu)建和加載 簡單點(diǎn),說話的方式簡單點(diǎn)~~ 上面的介紹貌似有點(diǎn)抽象啊套用在美團(tuán)技術(shù)分享大會(huì)上的話就是:
這里我們來復(fù)習(xí)下C語言的基本功,編譯和鏈接
由于某個(gè)目標(biāo)文件的符號(可以理解為變量,函數(shù)等)可能來自其他目標(biāo)文件,其實(shí)鏈接這一步最主要的操作就是決議符號的地址。
于是鏈接加裝載就有了不同的情況
Static Loading:啟動(dòng)時(shí) Dynamic Loading:啟動(dòng)后(使用時(shí))
Static Linking:構(gòu)建(鏈接)時(shí) Dynamic Linking:運(yùn)行時(shí)(啟動(dòng)時(shí)或使用時(shí)) 然后組合起來就是 2 * 2 = 4 了
第一種是純靜態(tài)庫相關(guān)了 第二種就是靜態(tài)加載(啟動(dòng)時(shí)),動(dòng)態(tài)鏈接,鏈接時(shí),動(dòng)態(tài)庫參與鏈接,但是這時(shí)候只是給符號打了標(biāo)記告訴我這個(gè)符號來自與動(dòng)態(tài)庫,程序啟動(dòng)時(shí),iOS或者M(jìn)ac OS操作系統(tǒng)的dyld自動(dòng)load + link。 既然全部都是自動(dòng)的。那么符號的調(diào)用方完全不知道你到底是源碼還是靜態(tài)庫,動(dòng)態(tài)庫 。 第三種收到調(diào)用dlopen + performSelector通常iOS的APP不適用這里不討論 第四種,沒見過,個(gè)人也不是特別懂 有需求請參看文后的程序員的自我修養(yǎng)一書 靜態(tài)庫和動(dòng)態(tài)庫依賴關(guān)系 既然有 2 種庫,那么依賴關(guān)系又是 2 * 2 嘍
第一種 靜態(tài)庫互相依賴,這種情況非常常見,制作靜態(tài)庫的時(shí)候只需要有被依賴的靜態(tài)庫頭文件在就能編譯出來。但是這就意味者你要收到告訴使用者你的依賴關(guān)系 幸運(yùn)的是CocoaPod就是這樣做的 第二種動(dòng)態(tài)庫依賴動(dòng)態(tài)庫,兩個(gè)動(dòng)態(tài)庫是相互隔離的具有隔離性,但是制作的靜態(tài)庫的時(shí)候需要被依賴動(dòng)態(tài)庫參與鏈接,但是具體的符號決議交給dyld來做。 第三種,靜態(tài)庫依賴動(dòng)態(tài)庫,也很常見,靜態(tài)庫制作的時(shí)候也需要?jiǎng)討B(tài)庫參與鏈接,但是符號的決議交給dyld來做。 第四種,動(dòng)態(tài)庫依賴靜態(tài)庫,這種情況就有點(diǎn)特殊了。首先我們設(shè)想動(dòng)態(tài)庫編譯的時(shí)候需要靜態(tài)庫參與編譯,但是靜態(tài)庫交由dyld來做符號決議,but這和我們前面說的就矛盾了啊。靜態(tài)庫本質(zhì)是一堆.o 的打包體,首先并不是二進(jìn)制可執(zhí)行文件,再者你無法保證主程序把靜態(tài)庫參與鏈接共同生成二進(jìn)制可執(zhí)行文件。這就尷尬了。 怎么辦? 目前的編譯器的解決辦法是,首先我無法保證主程序是否包含靜態(tài)庫,再者靜態(tài)庫也無法被dyld加載,那么我直接把你靜態(tài)庫的.o 偷過來,共同組成一個(gè)新的二進(jìn)制。也被稱做吸附性 那么我有多份動(dòng)態(tài)庫都依賴同樣的靜態(tài)庫,這就尷尬了,每個(gè)動(dòng)態(tài)庫為了保證自己的正確性會(huì)把靜態(tài)庫吸附進(jìn)來。然后兩個(gè)庫包含了同樣的靜態(tài)庫,于是問題就出現(xiàn)了。 看到這里想必前面出現(xiàn)的錯(cuò)誤你已經(jīng)能猜出來了把~_~ 后面再詳細(xì)解釋 先來個(gè)總結(jié) 可執(zhí)文件(主程序或者動(dòng)態(tài)庫)在構(gòu)建的鏈接階段
Xcode 項(xiàng)目結(jié)構(gòu)
iOS 依賴管理事實(shí)上的標(biāo)準(zhǔn) 這么多年,Apple的博客和文檔也就告訴了我們什么是靜態(tài)庫什么是動(dòng)態(tài)庫,如何制作等。但是并沒有給我們提供一系列的依賴管理工具。所以CocoaPods成了事實(shí)上的標(biāo)準(zhǔn)。 通常CocoaPods管理的工程結(jié)構(gòu)如下: 那么當(dāng)我們按下CMD + B的時(shí)候,整個(gè)項(xiàng)目按照先編譯被依賴Pod,然后依賴其他Pod的Pod也被構(gòu)建出來,最終所有的組件被編譯為一個(gè)lib-Pods-XXXAPP.a被添加進(jìn)項(xiàng)目進(jìn)去。資源通過CocoaPods提供的腳本也一并被復(fù)制進(jìn)去。想了解CocoaPods做了什么的讀者可以參看后面的鏈接 解決問題 這么多理論功底的建立,相信我們已經(jīng)能分析出來之前pod install的原因了。就是用了use_framework那么我們的所有Pod都會(huì)以動(dòng)態(tài)庫(Embeded Framework)的形式去構(gòu)建,于是那些非開源的庫(如百度地圖,微信分享)如果被多個(gè)Pod依賴(組件化開發(fā)中太常見了)于是被吸附到動(dòng)態(tài)庫里面,所以CocoaPod直接就不讓我們install成功。因?yàn)槟悻F(xiàn)在的依賴管理就是錯(cuò)誤的。 在聽取美團(tuán)葉樉老師分享的時(shí)候 他們的出發(fā)點(diǎn)是因?yàn)橐@過蘋果爸爸在iOS9以下對__text 段60M的限制使用了動(dòng)態(tài)庫方案,我們是因?yàn)槟承﹕wift庫必須要用到(歷史遺留原因)動(dòng)態(tài)庫。美團(tuán)的做法是摘除依賴關(guān)系,自定義CocoaPods(開源的本來就是用著不爽我就改)。但是我是個(gè)小菜雞啊。我也不會(huì) ruby(以后會(huì)學(xué)的),但是葉樉老師給我提了別的idea。前面我們知道 動(dòng)態(tài)庫和動(dòng)態(tài)庫是隔離性,動(dòng)態(tài)庫依賴靜態(tài)庫具有吸附性,那么我們可以自定義一個(gè)動(dòng)態(tài)庫把百度地圖這種靜態(tài)庫吸附進(jìn)來。對外整體呈現(xiàn)的是動(dòng)態(tài)庫特性。其他的組件依賴我們自定義的動(dòng)態(tài)庫,由于隔離性的存在,不會(huì)出現(xiàn)問題。 制作動(dòng)態(tài)庫 1 創(chuàng)建動(dòng)態(tài)庫項(xiàng)目這里以 wx 舉例 2 按照微信的官方文檔。添加依賴庫(我是因?yàn)閜od install巨慢所以我直接拽進(jìn)來了) 3 將wx的PublicHeader暴露出來,注意由于我并沒有使用到wx相關(guān)API所以鏈接器幫我們鏈接動(dòng)態(tài)庫的時(shí)候可能并不會(huì)把wx靜態(tài)庫吸附進(jìn)來。我們手動(dòng)在build Setting的other link flags加上-all_load標(biāo)記 4.在Schema里面跳轉(zhuǎn)編譯配置為Release,并且選擇所有的CPU架構(gòu) 5 然后選擇模擬器或者 Generic iOS Device 運(yùn)行編譯就會(huì)生成對應(yīng)版本的 Framework 了。 6.但是為了保證開發(fā)者使用的時(shí)候是真機(jī)模擬器都能正常使用,我們需要合并不同架構(gòu) 這里在Build Phases里添加以下腳本,真機(jī)和模擬器都Build一遍之后就會(huì)在工程目錄下生成Products文件夾,
于是我們有了我們自己的私有動(dòng)態(tài)庫LJWXSDK,那么我們來驗(yàn)證我們之前的問題 首先指定一個(gè)LJWXSDK.podspec這里我直接傳到了我的Github上面
注意上面我是把二進(jìn)制壓縮丟進(jìn)了七牛的 oss 文件存儲(chǔ)。畢竟免費(fèi)還快。 然后通過 pod lib create 創(chuàng)建了一個(gè) pod 用來驗(yàn)證之前我們的傳遞性依賴問題, 文件夾結(jié)構(gòu)如下
測試工程我也丟在7牛上面。下載測試即可 編譯運(yùn)行。完美。我們又可以愉快的和swift第三方庫配合使用。 很多人可能會(huì)問 諸如百度地圖 微信這種sdk為什么官方不支持動(dòng)態(tài)庫版(所說的都是embeded Framework),猜測是為了兼容更低iOS7版本吧 很多人會(huì)覺得麻煩的要死。首先每個(gè)公司多多少少都有歷史包袱,麻煩也要做,再者這是一次對基本功的補(bǔ)充,即便你們沒有用到,但是為了學(xué)習(xí),這篇教程所做的也值得你嘗試一次。 剖析下動(dòng)態(tài)庫 Framework 吧 上述解決了我們一開始遇到的問題。but既然動(dòng)態(tài)庫和靜態(tài)庫壓根就不一回事,所以里面還是有很多細(xì)節(jié)值得我們?nèi)チ私獾摹?/p> 回過頭來看 Embened Framework 首先我們之前記得如果一個(gè)動(dòng)態(tài)庫加在LinkedFrameworksand Libraies程序啟動(dòng)就會(huì)報(bào)ImageNotFound,如果放在EmbededBinaries里面就可以。這是為什么呢。我們拿MacoView來看下兩種情況下可執(zhí)行文件的細(xì)節(jié) 其中@rpth 這個(gè)路徑表示的位置可以查看Xcode中的鏈接路徑問題 這樣我們就知道了其實(shí)加在EmbededBinaries里面的東西其實(shí)會(huì)被復(fù)制一份到xx.app里面,所以這個(gè)名字起得還是不錯(cuò)的直譯就是嵌入的框架 Why Swift does not Support Staic Libraies 造成這個(gè)的主要原因是Swift的運(yùn)行時(shí)庫(不等同于OC的runtime概念),由于Swift的ABI不穩(wěn)定,靜態(tài)庫會(huì)導(dǎo)致最終的目標(biāo)程序中包含重復(fù)的運(yùn)行庫,相關(guān)可以看下最后的參考文章SwiftInFlux#static-libraries。等到我們的SwiftABI穩(wěn)定之后,我們的靜態(tài)庫支持可能就又會(huì)出現(xiàn)了。當(dāng)然也可能不出Swift伴隨誕生的SPM(Swift,Package Manager),可能有更好的官方的包依賴管理工具。讓我們期待吧。 CocoaPods使用Use_framework! 既然加了Swift的第三方庫之后就需要在Podfile里面加上use_framework! 那么CocoaPods就會(huì)幫我們生成動(dòng)態(tài)庫,但是奇怪的是,我們并沒有在主工程的embeded binaries看到這個(gè)動(dòng)態(tài)庫,這又是什么鬼。其實(shí)是CocoaPods使用腳本幫我們加進(jìn)去了。腳本位置在主工程的build Phase下的Emded Pods frameworks
動(dòng)態(tài)庫Framework的文件結(jié)構(gòu)
更愉快的導(dǎo)入文件
上面的導(dǎo)入方式都帶了 某個(gè)framework的路徑 看下面的配置 目前的配置是non-recursive。如果把non去掉意思就是我可以遞歸的去查找某些framework下面的頭文件了。 但是Xcode的效率肯定就會(huì)有影響。 還是不建議修改的好。 大家都知道iOS7之后多了@import,這又是什么鬼。 簡單理解這個(gè)方式叫做Module導(dǎo)入,好處就是使用了@import 之后不需要在project setting手動(dòng)添加framework,系統(tǒng)會(huì)自動(dòng)加載,而且效率更高。 最主要的是swift也只能這樣用。 導(dǎo)入的時(shí)候系統(tǒng)會(huì)查找如果有模塊同名的文件就會(huì)導(dǎo)入這個(gè)文件。如果沒有CocoaPods幫我們生成一個(gè)module-umbrela.hl文件,然后就是導(dǎo)入的這個(gè)文件。 回過頭來看我們的framework的結(jié)構(gòu) 里面有個(gè)Modules文件夾,里面有個(gè)文件module.modulemap
我們可以看到其實(shí)被暴露的header就是這個(gè)文件,之前我在按照#import "/"的時(shí)候有個(gè)警告 而且按照@import 導(dǎo)入的東西發(fā)現(xiàn)沒有導(dǎo)入可用的頭文件就是因?yàn)椴]有在umbrella header的頭文件中加入其他頭文件。 加入之后我們就可以完美的使用@import ,并且#import"/" 也不會(huì)報(bào)warning 更多關(guān)于umbrella Header 參看文后參考 資源問題 首先我們來看常見的資源文件: 主要分為圖片和其他類資源那么加載圖片和加載其他資源都是怎么做的? 1: [UIimage imageNamed:] 2: [NSbundle bundleForclass[XXX class]] 其實(shí)方式1去本質(zhì)就是去mainBundle去拿資源,方式2從XXX所在的框架里面去拿。 前面也說道framework只是資源的打包方式,本質(zhì)上是有兩種的。 我們這個(gè)framework如果本質(zhì)是靜態(tài)庫,那么無需改變使用方式,資源最終都會(huì)打包到Main Bundle里面 如果我們這個(gè)framework本質(zhì)是動(dòng)態(tài)庫,那么我們的資源就發(fā)生了變化,資源就會(huì)被存放在framework里面。所以我們需要使[NSbundle bundleForclass[XXX class]]。需要注意的是很多人為了簡單,下意 的使用self class傳遞,但是有可能這個(gè)self實(shí)例不在資源所屬的framework。所以會(huì)出現(xiàn)資源加載失敗。一定要謹(jǐn)慎使用。 參考 |
|