Android工程模組化平臺設計-講稿

kymjs張濤發表於2018-04-22

這篇文章是我在 2018【攜程技術沙龍移動技術工程化】技術分享時所講內容的文字版本,修改刪減了演講時的冗餘言語。 釋出在【掘金專欄】,希望能給買不到票參加大會的朋友帶來幫助。

https://kymjs.com/code/2018/04/22/01/

大家好,今天跟大家分享的主題是《Android工程模組化平臺的設計》

https://kymjs.com/code/2018/04/22/01/

首先自我介紹一下:我叫張濤,目前就職於餓了麼移動技術部。可能有些朋友認識我,我之前也會在我部落格【開源實驗室】寫一些Android相關的技術點,如果對今天講的模組化設計,你覺得有什麼問題或者可以深入探討的,也歡迎加我微信kymjs123詳聊。

https://kymjs.com/code/2018/04/22/01/

今天我們講的主題是基於專案模組化來說的,模組化是什麼大家肯定都是知道了的,這裡問一下大家,有多少人在此之前有做過模組化的,舉個手我看一下;瞭解過聽說過模組化的呢?這次比較多。
我們說,做模組化其實跟專案重構很像,都是從這幾個點來做的,只是側重點不同。分別是:刪除、組織、降級、解耦。那麼這四點是什麼意思呢,那麼接下來跟大家分享一下我是如何理解這四大塊的:

https://kymjs.com/code/2018/04/22/01/

刪除:刪除不必要的檔案,儘可能減小工程體積。這裡有一組資料,是我統計我們餓了麼的一款 APP 在模組化前後一些檔案的數量。
可以看到,.java檔案從1677個減少到了1543個。其實這不是重點,重點是下面的drawable,這裡drawable只包含圖片、和xml佈局,當經過模組化重構後檔案數從 693 減少到 538 個。圖片資源減少接近 200 個,apk 的大小也會隨之降低。

https://kymjs.com/code/2018/04/22/01/

而組織呢,指的是:按照有意義的標準將程式碼分組。這其實也是java的包所存在的目的之一。
但是隨著專案的不斷迭代,需求很緊的情況下是很難有時間去真正規範的將類分組的。看到圖中,我們之前的結構很亂,就是因為專案快速迭代和人員更替的過程中,不免會有這樣的現象。所以這也是模組化重構時所作的一件大事。

https://kymjs.com/code/2018/04/22/01/

接下來就是我們經常說的內聚和耦合了,降級。我們之前有一個類叫:Navigator,它是負責幾乎所有Activity直接跳轉的。就是我們會把所有的startActivity()的跳轉放到這個類裡面去寫。之前少的時候還好,結果等我看到這個類的時候,這個類已經有 200 多個方法了,全是Activity跳轉的方法,其中還有重複的,就是很早之前有人寫了一個跳到某個介面,結果之後來了個人,他不知道又寫一個。

而我們在做模組化重構時的做法就是,首先觀察自己的專案,這是重構很重要的一步,就是要結合自身。把這個類拆分成了三大部分,我們有兩塊業務是會頻繁跳轉的但這兩個業務跳轉的頁面又都是在自身的模組內,分別是使用者模組和商戶模組。因此我們將這兩個模組中分別建立兩個用於模組自己內部的跳轉叫UserNavigatorShopNavigator,而模組間的跳轉或一些小模組內部的則使用Router去做,我們自己定義了一個路由庫,其實實現跟現在開源的區別不大。

https://kymjs.com/code/2018/04/22/01/

最後解耦,也是今天的重點,如何優雅移除模組間的耦合。 到目前為止,我們已經能夠做到讓所有不包含業務狀態介面的模組的增刪,不需要改動任何一行程式碼。 具體到一個示例就是這樣:

https://kymjs.com/code/2018/04/22/01/

或者,也可以是這樣:

https://kymjs.com/code/2018/04/22/01/

這兩個段程式碼的區別就是一個是手動管理Debug的狀態,另一個是交給Gradle的編譯任務去控制,原理上是一樣的。
而這麼做是如何實現的呢,其本質就是:一個模組就是一個功能,你想要讓你的 apk 具備這個功能,就新增這個模組一起編譯就可以了。這才是我們說的真正的元件化,模組之間零耦合,增減模組零改動。
例如圖中:debug這個模組,肯定不會用在正式的生產環境;而相反的tinker這個模組,熱補丁肯定也不會用於除錯階段。所以我在開發時就可以不使用這個模組相關的程式碼。
另外再舉個使用的例子:我有一個訂單模組,訂單模組需要播放鈴聲,比如大家在飯店經常聽到“您有新的餓了麼訂單,請及時處理”。但我在開發訂單模組的時候,如果我已經確定鈴聲播放是沒有問題的,那我可以選擇開發階段不打鈴聲的包,直到釋出到線上了再去加上鈴聲的包。那我沒有新增這個鈴聲模組的時候,我就預設不具備播放鈴聲的功能,但完全不影響其他的訂單模組的業務功能,而這個鈴聲模組的增刪,是不需要修改任何程式碼的。
聽到這裡相信大家都很好奇這是怎麼實現的。接下來就跟大家講講內部的原理。

https://kymjs.com/code/2018/04/22/01/

所有的核心功能都來自我們自己寫的一個庫:IronBank。取《自冰與火之歌》中的【鐵金庫】,叫鐵金庫不容拖欠。
鐵金庫的內部實現,其實是使用了 APT 註解處理器,去在編譯時解析註解生成一個類,讓這個類去生成跨模組的物件。鐵金庫使用了與後端 SOA 設計思路類似的方式:將模組之間的主動依賴倒置,變為功能的提供與使用。
那什麼是 SOA 的設計思路呢,我們看到一張我畫的漫畫圖:SOA 它是一種面向服務的架構模型。

https://kymjs.com/code/2018/04/22/01/

例如圖上左邊有一個對外提供媒體功能的服務提供者,他告知IronBank我提供媒體服務:“嘿,老鐵,我這有個媒體服務,你那邊有誰要用的時候可以用我的。”
到了另一邊,如果此刻有模組說是,我需要媒體服務:“老鐵,你那有沒有媒體服務,我這邊需要播一個鈴聲啊!”。
“有的,給你。”
IronBank就會將之前服務提供者提供給他的媒體物件交給服務使用者。

https://kymjs.com/code/2018/04/22/01/

接下來我們來看具體到程式碼上是如何使用的:首先是作為服務使用方,也就是上一張圖右半部分。我們看到傳統的做法是首先宣告一個介面型別,然後new出介面的實現類給他賦值。
而使用了IronBank的時候,你是不需要關心介面的實現類到底是誰的。這就是IronBank唯一的用處,隱藏實現類,做到徹底的面相介面程式設計。

https://kymjs.com/code/2018/04/22/01/

之前說過,IronBank將模組之間依賴倒置,由之前的服務提供方被動的接受呼叫方呼叫變為,服務方主動提供服務給呼叫方。
那作為服務提供方需要做些什麼事呢,非常簡單,你只需要給你的物件提供public static方法,並加上一個@Creator註解,告訴IronBank這是一個建立器方法就可以了,其他任何事情,都不需要考慮。

https://kymjs.com/code/2018/04/22/01/

前面講的IronBank適用的場景是無狀態的服務,而我們做業務APP開發的時候更多的是有業務狀態的物件,比方說我們通常長鏈與推送功能是等到使用者登入了以後才會去啟動,但具體到程式碼上,推送模組是根本不知道使用者什麼時候登入的,這就是一個業務狀態的問題。 而對此我們引入了一個BizLifecycle的介面,他其實與Android上的Application物件功能類似。只不過他用來管理的是業務的生命週期,而不是應用的。
那麼在程式碼邏輯上,每個模組如果關心你所需要的業務生命週期,只需要註冊一個Lifecycle就行了,同時註冊的過程也只需要一個註解,由編譯外掛解決了。

https://kymjs.com/code/2018/04/22/01/

可以看到,其實這樣的一種能力用事件通知也可以做到,比方說廣播或者EventBus,但是我們刻意遮蔽了這種方式,就是因為事件通知這種功能你是很難去追蹤的,你不知道一個訊息傳送了以後,他的接受者是在哪裡。相信大家也能狗想象得到,一個應用如果廣播氾濫,到處都是事件接收事件傳送會專案程式碼會變得多麼嚇人。

https://kymjs.com/code/2018/04/22/01/

講到這裡,整個模組化解耦的全部能力就跟大家介紹完了。接下來,我們再從巨集觀角度去看一下整個專案的結構,分為三級,最上層是業務模組,緊接著是一些可選的功能元件,最底層則是與專案無關的公共依賴。

https://kymjs.com/code/2018/04/22/01/

最終,專案結構就是如圖中所示的這樣。但如果你真直接這麼做,你一定是會煩死的。
為什麼?
第一:這麼多的模組,直接用原始碼依賴去編譯,編譯時間至少在10分鐘以上;
第二:模組的隔離幾乎為0,任何一個人依舊可以修改任何一個模組的程式碼,並且很容易;
第三:在發版本以後,如果某一個模組有BUG,再去修復,缺乏一個版本的概念,尤其是在跨團隊的時候,最終一定會出現版本分裂問題。

https://kymjs.com/code/2018/04/22/01/

解決辦法我想大家都知道,就是將模組引用改為aar引用。aar引用最大的優勢就在於模組版本的管理與跨團隊的協作。
目前國內對Android領域的探索越來越深,應用規模也越來越大,為了降低大型專案的複雜性和耦合度,同時也為了適應模組重用、多團隊並行開發測試等等需求,你必須有一套合適的模組化平臺。

https://kymjs.com/code/2018/04/22/01/

這裡是我們餓了麼目前使用的模組化平臺,大家可以從這張圖中感受一下。
模組化平臺,主要的功能是很明顯的,就是用於構建模組,在這之上,還有隱含的功能,就是集中了構建模組的許可權,可以更便於統一管理;
當然還有最重要的優勢就在於模組版本的管理,你可以很清晰的知道當前主應用所接入的模組的版本是哪個,當前最新構建的SNAPSHOT是哪個,以及每個版本的更新日誌;
這樣做了以後,在跨團隊協作上的溝通就大大降低了,如果你已經接入或者即將接入的模組是另一個團隊開發的模組元件,那你可以直接關注它,它的所有版本變動日誌,最新版本全都一目瞭然;
並且可以通過平臺簡化模組的測試與模組釋出的流程,比如提測的時候,如果是一次相容版本的釋出,你只需要告訴測試提測分支,測試可以自己根據現線上上應用的tag,同時引入當前提測的模組替換老版本的模組重新編譯,很容易就能控制變數。

https://kymjs.com/code/2018/04/22/01/

引入了平臺化以後,我們再從工程結構的角度看一下:就目前我們嘗試下來,這兩種結構是最合適Android工程模組化的。一種是submodule,一種是multi-project。

https://kymjs.com/code/2018/04/22/01/

首先看submodule:這種結構是Android預設的多模組結構,在一個工程下面有多個模組。圖上每個綠色的方塊都代表了一個git倉庫,然後我們看到所有子模組都包含在主工程模組內。這種結構也是git預設支援的submodule結構,你只需要用最下面的這句git命令就可以將他們關聯在一起。
它的好處就是所有都是預設的,任何一個人理解起來都是很直觀。當然,他也有不適合的,就是協作開發的時候,所有人都在app module上測試自己的模組,很容易互相影響,主工程的git分支也會非常繁雜。

https://kymjs.com/code/2018/04/22/01/

與之對應的,multi-project能很好的解決這個問題:所有模組都是一個獨立的工程,他們在檔案系統上是並列關係,每個模組所在的工程才是一個git倉庫。
但是這種結構就對工程名會有一定的規範要求,主要原因是在模組聯調的時候。

https://kymjs.com/code/2018/04/22/01/

我們看到這段程式碼是寫在setting.gradle檔案中的,他根據讀取本地的local.properties檔案,來include一個模組的原始碼,方便在模組聯調的時候可以很容易的修改多模組的程式碼。
但是他就要求每個模組工程的資料夾名稱是以模組名加上Project這樣來命名,比如order模組所在的工程資料夾名就叫OrderProject
當然,你也可以不遵守,只不過不遵守就得寫更多程式碼,我這裡是直接用了迴圈,不遵守的話可能就需要把迴圈拆開手敲了。
以上兩種工程結構各有各的好處,沒有好壞,只有合不合適,我們內部兩種結構也都有團隊在用。

https://kymjs.com/code/2018/04/22/01/

然後,這裡是模組聯調的注意事項,就是如果你模組是以原始碼引入的,可能還有其他模組引用了同樣模組的aar,就會造成衝突,你需要自己判斷一下,加個自定義方法也好,用編譯外掛也可以,都能做到讓原始碼引用與aar引用互斥。

模組化架構主要思路就是分而治之,在拆分的時候最重要的就是把依賴整理清楚,那些是業務模組,哪些是可選的功能元件。最後為了團隊方便以及更快的適應,還需要開發一些輔助工具,比方說我前面說的IronBank、BizLifecycle、初始化指令碼等等,都是必不可少的。

https://kymjs.com/code/2018/04/22/01/

最後,今天的分享就到這裡,謝謝大家。

相關文章