一、知得失
每一個便捷工具或技術出現(xiàn)在臺前,臺后都躺著一個懶人
在CocoaPods出現(xiàn)之前,iOS項目依賴的第三方庫都是直接拖進項目中的,庫多了之后,有代碼潔癖(或說架構潔癖)的猿就要開始頭皮發(fā)麻了,那么做的好一點就是建立個Workspace,業(yè)務代碼在業(yè)務Project,第三方庫或自己的私有庫放到庫Project,然后實現(xiàn)聯(lián)編。這樣管理起來雖然繁瑣,但是起碼項目架構規(guī)整了很多,而且越來越像后來CocoaPods的做法。
熟悉node的都知道npm,CocoaPods與其很類似,實現(xiàn)了iOS項目的包管理,雖然受限于蘋果搞得iOS項目的條條框框,起碼實現(xiàn)了以下幾點功能:
- 統(tǒng)一的中心庫概念,使我們可以便捷地添加、更新優(yōu)質(zhì)的第三方庫
- 相對完善的版本控制
- 庫Project和業(yè)務Project分離
- 提供創(chuàng)建私有Pod倉庫的能力
于是CocoaPods開始遍地開花,大家用的都很Happy。
但是“可以創(chuàng)建私有Pod倉庫”這一點,隨著時間的推移,會像打開了潘多拉的魔盒一樣,問題開始噴涌而出。
當私有庫越來越多,“依賴泥潭”出現(xiàn)了!相對大一些的公司一般在發(fā)展過程會都會獨立出一個項目組或一部分人去開發(fā)和維護這些私有庫,雖然做這些工作的都是大神,但是人的思維是不一樣的,在私有庫進化過程中,要權衡庫的耦合度和復用性的微妙關系,比如庫A有一個字符串MD5的方法,庫B需要對字符串進行MD5,那么庫B是自己實現(xiàn)一個呢,還是引用庫A的MD5方法,如果說庫B依賴庫A,很清晰啊,那么如果庫A又要依賴庫B中的某個方法呢,或者庫更多一些的情況是庫A依賴庫B,庫B依賴庫C,庫A依賴庫C... 這樣不斷進化,最終“全家桶”出現(xiàn)了:當業(yè)務代碼需要依賴一個庫時,由于復雜的依賴關系,幾乎所有的庫拉下來了!
你可以說這個完全可以通過精細的規(guī)劃和設計就能避免,但是要考慮到開發(fā)這些庫往往是不同的人甚至不同的項目組,而且框架組雖然會盡量考慮到庫使用者的需求,但是他們考慮更多的是跟進整個公司的技術迭代,何況現(xiàn)在講求架構分層,流行將業(yè)務代碼封裝為私有庫。
你也可以說拉下來就拉下來吧,編譯通過運行沒事就OK。但是編譯通過是可以確定知曉的,但是運行沒事怎么保證呢?做開發(fā)最起碼要講求發(fā)生了什么都要知情!
一個最典型的案例,一個iOS項目中,依賴一個用于統(tǒng)計用戶行為的庫,但是這個庫是隱性依賴(我們把沒有直接寫在Podfile中,而是通過其他庫依賴進項目的庫叫隱性依賴庫)進項目的,也就是說每次執(zhí)行pod install都會將最新版本拉下來;這樣用了近兩年,一直沒事,但是某天框架組更新了這個庫,將信息上傳的服務器變得可選,然后提供了一個默認的服務器A,但是舊版本一直是上傳到服務器B;這個變動變得很隱晦,因為我們發(fā)生的這些變動根本“不清楚”。于是發(fā)版后,線上故障出現(xiàn)了,App就像丟到大海的漂流瓶一樣信息全無,基于服務器B而產(chǎn)生的用戶行為統(tǒng)計報表突然都變得一片空白,而直到這時,可能心里還在說,這塊代碼我們沒有動過啊,這個版本我們只是把某個Label字體加大了點...
當然可以找出N種方案來避免這個問題,比如查看Podfile.lock,但我想強調(diào)的是“知情”。
再來看另一個問題,人總是趨于安逸,我們的項目在使用框架組提供的私有庫的某個版本,很穩(wěn)定很Nice,于是我們鎖死了這個版本,用了長時間。但是世界總是在變化的,尤其技術的更迭更是迅雷不及掩耳,當某天我們需要引進某個現(xiàn)成的功能,比如直播,突然發(fā)現(xiàn)我們項目中使用的庫版本太陳舊了,尤其這個功能依賴的某些已存在我們Podfile中的庫,怎么辦呢?升級吧,結(jié)果悲催了,由于全家桶效應,我們Podfile中大部分已存在的庫都要升,而升級有又要引入新的庫,新的庫又要引入新的庫...,于是我們一遍又一遍的執(zhí)行pod install或pod update(出于上文提到的問題,我們都是鎖死某個確定的版本,而不是使用版本條件),一遍又一遍的編譯...
軟件工程強調(diào)項目要為人優(yōu)化,而不是為機器優(yōu)化,因為人的時間比機器時間貴
驕傲的程序猿的時間都是很貴的,為什么不能把這些事情交給機器去做呢?步入正題,如何在存在上述問題的情況下,保證基于CocoaPods的iOS項目架構平滑進化。
二、成方圓
《人月神話》中稱軟件項目像是掉進焦油坑的野獸,體型越大越難以掙扎,那么我們來拆解這個巨獸,讓它變小,分而治之。
我們需要一個清晰的分層架構,用于分解項目,在這個架構上我們先制定總規(guī)則:
- 下層模塊/庫不能對上層進行依賴
- 基礎框架層的模塊/庫之間允許相互依賴,但是不建議不提倡,業(yè)務層和橋接層禁止同層級依賴
- 所有層所包含的模塊模塊/庫使用CocoaPods進行管理,以達到強制解耦的目的(拆分模塊或庫時會強制開發(fā)者考慮庫依賴的問題)

下面開始從下往上進行規(guī)則設定。
2.1 基礎框架層
基礎框架層特點主要有:
- 公用性強
- 包括框架組提供的平臺性公共庫、自封裝的公用庫
- 不包含業(yè)務庫
該層又分為公共框架層和非公共框架層,解釋下:這里“公共”是指多項目的公用性,而非單項目的公用性。
1、公共框架層
公共框架層的模塊/庫除具有基礎框架層的特性外,最主要的特性就是多iOS項目公用,比如我們項目組有多個iOS項目,都需要用到Core庫,那么Core便位于該層。
最重要的一點,該層可獨立編譯。獨立編譯也意味著該層甚至可以編譯為一個“大包”到處使用。基于這個特性,公共框架層會有版本的概念,每當公共框架層的庫有變動時,會上升一個版本,而每個使用該層的項目都可選擇版本,但是版本升級往往意味著技術迭代的發(fā)生,那么為了不讓項目又進入“安逸”狀態(tài),要求每個項目都要跟進公共框架層的版本升級,并適當調(diào)整自己的的非公共框架層和橋接層。
公共框架層升級誘因有兩個:
- 來自上層的功能需求
- 公共框架層各庫的技術更新和功能迭代
公共框架層升級會導致的操作:非公共框架層和橋接層對公共框架層的變動進行適配
2、非公共框架層
非公共框架層的模塊/庫除具有基礎框架層的特性外,最主要的特性就是不是多iOS項目公用,比如庫TGBase,ProjectA依賴,但是ProjectB不依賴,則TGBase位于ProjectA中的該層。
2.2 橋接層
橋接層的主要作用是為上層業(yè)務層和下層基礎框架層做橋接,主要考慮底層業(yè)務層的開放接口變動頻繁以及不適配上層業(yè)務,需滿足以下規(guī)則:
- 使框架層的變動對業(yè)務層透明
- 提供友好的接口給上層業(yè)務層,滿足開閉原則
- 可對基礎框架層的變動進行快速兼容
- 易于維護和擴展
- 可下沉到基礎框架層
2.3 業(yè)務層
業(yè)務層的模塊/庫主要包含業(yè)務庫,業(yè)務層不允許層內(nèi)模塊/庫的相互依賴,當某個模塊/庫確實要依賴另一個模塊/庫的功能,如果該功能不包含業(yè)務邏輯,則拆出該功能并封裝成庫,下沉到下層,若包含業(yè)務邏輯,那么嘗試剝離業(yè)務邏輯后封庫下沉,如果無法剝離業(yè)務邏輯,合并這兩個業(yè)務模塊。
2.4 可編譯性和可移植性
除了公共框架層可獨立編譯外,其他各層由于需要向下依賴,所以不可獨立編譯,但是可以生成依賴圖的最小生成樹,這顆最小生成樹可以進行獨立編譯,這一點的意義在于,當我們要單拎出某個業(yè)務庫時,不必把下層所有庫連根拔起。
使用最小生成樹法移植業(yè)務層庫雖然是最精簡的,但是對于項目組來說,確定一個公共庫簇的意義更大一些,而公共庫簇其實就是公共框架層,它包含了項目組項目甚至平臺項目所需的最基本的庫,所以可以把整個公共框架層可看做依賴圖/樹中的一個節(jié)點。
那么最終可編譯的最小生成樹就可以合并所有位于公共框架層的節(jié)點,形成一個節(jié)點,減少分析和移植的復雜度。
2.5 總結(jié)
分層以及規(guī)則制定,主旨是保證復雜的依賴多的模塊/庫上升,而功能相對單一(或功能統(tǒng)一、明確)同時被依賴多的模塊/庫下沉,減少依賴環(huán),盡量減少拔起蘿卜帶出泥的“全家桶慘劇”發(fā)生,增加可移植性。
三、利其器
CocoaPods確實是一把鋒利的好刀,但不是瑞士軍刀。
針對上文所提的問題及需求,我們來看看我們在iOS項目進化時需要哪些功能:
- 需要知道Podfile真實的依賴庫以及依賴關系
- 需要知道代碼級的依賴關系
- 當要添加或升級一個庫時,需要知道隱性依賴和需要升級的已存在庫
- 當去除一個庫時,需要知道其他庫有哪些可以一并去除
- 需要知道某個庫某個版本的依賴
- 需要知道多個項目的共同依賴庫
- 直觀地實現(xiàn)分層
- 對第二章制定的一些規(guī)則進行強制執(zhí)行
總的來說,就是我們希望CocoaPods能夠自動進行pod的集成,好吧,這些功能CocoaPods不提供或不方便地提供。我們可以人工地一遍又一遍的pod install和編譯來獲取這些信息,但是人生苦短,不能這么干。
那么可以把CocoaPods打造成一把瑞士軍刀,我們可以開發(fā)CocoaPods的插件(ruby實現(xiàn)),也可以依托CocoaPods,開發(fā)適合我們需求的工具來實現(xiàn)上述功能,用什么語言開發(fā)無所謂,達成目標即可,最好是腳本語言,不過不建議用宇宙最強大的PHP,如果不嫌“沉甸甸”的話,Java也是一個選擇。
這里我選擇開發(fā)依托CocoaPods的工具,Go語言實現(xiàn),至于為什么用Go,安利一下:
- 全量靜態(tài)多平臺編譯,無需安裝Go環(huán)境便可直接執(zhí)行,無需繁瑣配置和拉依賴庫,使用成本低
- 腳本語言般的開發(fā)速度,強類型,現(xiàn)代語言特性
- 強大的協(xié)程(比線程更輕量),意味著更小的開銷實現(xiàn)大的并發(fā)
由于CocoaPods是LTS(Long Term Support),那么依托CocoaPods的工具也需要LTS。暫時命名工具名稱為Pandora。
工具的意義不僅僅是提供便捷的功能,更重要的是實現(xiàn)強約束,畢竟人的思維具有不一致性和不穩(wěn)定性,那么規(guī)則就不能依靠開發(fā)人員的自覺性去遵守,而應通過機器來強制執(zhí)行。
三、謀而動
下面開始面對現(xiàn)實,我們有兩個項目,ProjectA和ProjectB,兩個項目當前都在使用穩(wěn)定但陳舊的庫,而我們要實現(xiàn)的最終目標有五個:
- 項目中所有庫都升級到最新穩(wěn)定版本。
- 實現(xiàn)分層。
- 實現(xiàn)強約束和自動集成。
- 庫版本持續(xù)跟進
- 項目可編譯通過并運行OK。
雖然很多工作可以交由工具去做,但是這仍然是個繁瑣而龐大的進程,需要整個團隊配合完成,所以要對這項工作進行任務拆解,分為三期,每期再分幾個步驟來完成。
在開始之前,先介紹下我們項目中存在的幾種類型的庫:
- 第三方公有庫:托管在CocoaPods中的公有庫,如AFNetworking
- 平臺性私有庫:公司框架組開發(fā)的適用于整個技術平臺的庫
- 項目組私有庫:項目自己的私有庫,用于滿足項目組私有業(yè)務或功能需求,或彌補平臺性私有庫的功能空缺
3.1 一期工作
一期工作的核心是分別升級ProjectA和ProjectB項目陳舊的庫到最新穩(wěn)定版本,并編譯通過和可運行。
1、建立空項目
分別建立對應于ProjectA和ProjectB的空項目,目的是防止上層業(yè)務邏輯因為升級了底層庫而不適配,導致編譯不通過,所以要排除這部分的干擾。
空項目只包含對應舊項目的Podfile。
1、確定最新穩(wěn)定版本
當依賴一個別人開發(fā)的庫時,也就意味著引進Bug風險,所以引入的這個庫我們希望是穩(wěn)定無Bug的,如何確定一個穩(wěn)定版本有兩個手段:
- 分析法:人工閱讀分析最新版本的庫源碼,分析Bug風險并得出結(jié)論。
- 參照法:找到一個項目,該項目中使用的庫是最新的或相對比較新,如果該項目線上穩(wěn)定,那么可以以這個項目為參照。
我們使用成本比較低的方案,就是參照法,參照的項目是ProjectMain。升級或添加的庫均為平臺性私有庫和第三方庫。
那么接下來的工作便可交由Pandora工具完成,基本邏輯如下圖。

最終會生成一個包含最新/相對比較新且穩(wěn)定版本庫的Podfile,該Podfile執(zhí)行pod install后所安裝的庫與Podfile指定的庫一致(沒有隱性依賴),但是可能編譯不通過。
2.適配
適配的工作重點是保證新生成的Podfile在執(zhí)行pod install后可在空項目中編譯通過。
這一過程需要修改、拆分項目組私有庫,以兼容平臺性私有庫和第三方公有庫的變動。根據(jù)舊的項目組私有庫的功能,可逐漸拆解為基礎框架層庫、橋接層庫和業(yè)務層庫,這樣形成初級的分層架構。
這部分工作暫時無法由工具來完成,需要團隊小伙伴們分工配合來完成。
3.2 二期工作
二期工作主要是完善架構分層和保證運行無Bug。
1.拆分業(yè)務模塊
將ProjectA和ProjectB舊項目中未拆分為庫的業(yè)務模塊從業(yè)務Project中拆出作為Pod庫,移入Pod Project,然后適配,這個過程需要不斷完善橋接層庫,最終形成穩(wěn)定的業(yè)務層和橋接層。
2.分割基礎框架層
使用Pandora分析最終ProjectA和ProjectB的Podfile,將基礎框架層在分為公共框架層和非公共框架層。
最終,每個項目中非Pod Project變得代碼極精簡,變?yōu)橐粋€殼,而大部分業(yè)務邏輯均拆分到Pod Project中,在第二章分層架構的基礎上形成如下圖所示的架構。

3.約束和規(guī)約
約束:檢查每個層級的庫是否滿足第二章所制定的規(guī)則,不滿足的需要進行修改、拆分和升層降層。這個工作可以由工具分析出這些不滿足規(guī)則的庫,然后團隊小伙伴進行操作。
規(guī)約:目的是精簡庫,需要使用工具對整個項目進行代碼級的依賴分析,目標:
- 每個項目組私有庫的podspec都正確指定其依賴
- 去除沒有真正被依賴的庫
4.測試
測試的最終結(jié)果是程序運行無Bug,這個過程需要持續(xù)修改、優(yōu)化橋接層庫。
測試分兩條并行線:
- 自測:需要模塊/庫的負責人對自己負責的模塊進行詳細測試,必要時需要閱讀和分析代碼
- QA回歸:QA同學需要介入并對整個App進行全量回歸,盡可能發(fā)現(xiàn)隱藏的Bug。
3.3 三期工作
三期工作主要是維護前兩期工作的成果,保證項目組所有項目持續(xù)迭代。
ProjectA和ProjectB將由Pandora工具托管,Pandora構建在CocoaPods和Git之上,提供命令行和可視化界面以使人工干預可以介入,包括:
- 維護公共框架層整層大版本,提供詳細友好的公共框架層升級建議。
- 對平臺性私有庫的變動敏感,持續(xù)跟進最新穩(wěn)定版本。
- 實現(xiàn)項目的代碼畫像,保證分層架構規(guī)則的應用。
- 實現(xiàn)分層可見。
- 實現(xiàn)可人工干預的自動集成。
- 生成更詳細的自動集成結(jié)果的數(shù)據(jù)透視表,實現(xiàn)事事知情,以利于幫助開發(fā)人員修改、擴展橋接層庫。