文章摘要: 方便在模組聯調的時候可以很容易的修改多模組的程式碼而模組間的跳轉或一些小模組內部的則使用 Router 去做
張濤,餓了麼資深Android工程師,「開源實驗室」博主,Kotlin 技術推廣者。2013年開始從事Android開發,帶過團隊,做過架構,寫過應用,做過開源社羣。目前在餓了麼商戶端負責應用模組化平臺與外掛化平臺的設計和開發。本文來自張濤在「 攜程技術沙龍——無線技術工程化 」上的分享。
本文的主題是基於專案模組化來說的,模組化其實跟專案重構很像,只是側重點不同,分別是:刪除、組織、降級、解耦。接下來將跟大家分享我是如何理解這四大塊的。
模組化重構
刪除:刪除不必要的檔案,儘可能減小工程體積。這裏有一組資料,是餓了麼一款 APP 在模組化前後一些檔案的數量。
可以看到, .java
檔案從1677個減少到了1543個。其實這不是重點,重點是下面的 drawable
,這裏 drawable
只包含圖片、和 xml
佈局,當經過模組化重構後文件數從 693 減少到 538 個。圖片資源減少接近 200 個,apk 的大小也會隨之降低。
而組織呢,指的是:按照有意義的標準將程式碼分組。這其實也是 java
的包所存在的目的之一。
但是隨著專案的不斷迭代,需求很緊的情況下是很難有時間去真正規範的將類分組的。看到圖中,我們之前的結構很亂,就是因為專案快速迭代和人員更替的過程中,難免會有這樣的現象。所以這也是模組化重構時的一件大事。
接下來就是我們經常說的內聚和耦合了,降級。我們之前有一個類叫: Navigator
,它負責幾乎所有 Activity
直接跳轉。我們會把所有的 startActivity()
的跳轉放到這個類裏面去寫。少的時候還好,等我看到這個類的時候,已經有 200 多個方法了,全是 Activity
跳轉的方法。
在做模組化重構時,首先觀察自己的專案,這是很重要的一步,要結合自身。把這個類拆分成三大部分,我們有兩塊業務是會頻繁跳轉的,但這兩個業務跳轉的頁面又都是在自身的模組內,分別是使用者模組和商戶模組。因此將這兩個模組中分別建立兩個用於模組自己內部的跳轉叫 UserNavigator
和 ShopNavigator
,而模組間的跳轉或一些小模組內部的則使用 Router
去做。
之後解耦,如何優雅移除模組間的耦合。 到目前為止,我們能夠做到讓所有不包含業務狀態介面的模組的增刪,不需要改動任何一行程式碼。 一個示例:
或者,也可以是這樣:
這兩個段程式碼的區別,一個是手動管理 Debug
的狀態,另一個是交給 Gradle
的編譯任務去控制,原理上是一樣的。
而這麼做是如何實現的呢?其思路:一個模組就是一個功能,你想要讓你的 apk 具備這個功能,就新增這個模組一起編譯就可以了。這纔是我們說的真正的元件化,模組之間零耦合,增減模組零改動。
例如圖中: debug
這個模組,肯定不會用在正式的生產環境;而相反的 tinker
這個模組,熱補丁肯定也不會用於除錯階段。所以在開發時就可以不使用這個模組相關的程式碼。
再舉個使用的例子:我有一個訂單模組,訂單模組需要播放鈴聲,比如大家在飯店經常聽到「您有新的餓了麼訂單,請及時處理」。但在開發訂單模組的時候,如果已經確定鈴聲播放是沒有問題的,那可以選擇開發階段不打鈴聲的包,直到釋出到線上了,再去加上鈴聲的包。
那沒有新增鈴聲模組的時候,就預設不具備播放鈴聲的功能,但完全不影響其他的訂單模組的業務功能,而這個鈴聲模組的增刪,是不需要修改任何程式碼的。
聽到這裏,相信大家都很好奇是怎麼實現的。接下來就跟大家分享下內部的原理。
鐵金庫解耦
所有的核心功能都來自我們自己寫的一個庫: IronBank
。取《自冰與火之歌》中的【鐵金庫】,叫鐵金庫不容拖欠。
鐵金庫的內部實現,其實是使用了 APT 註解處理器,在編譯時解析註解生成一個類,讓這個類去生成跨模組的物件。鐵金庫使用了與後端 SOA 設計思路類似的方式:將模組之間的主動依賴倒置,變為功能的提供與使用。
例如圖上左邊有一個對外提供媒體功能的服務提供者,他告知 IronBank
我提供媒體服務:「嘿,老鐵,我這有個媒體服務,你那邊有誰要用的時候可以用我的。」
到了另一邊,如果此刻有模組說是,我需要媒體服務:「老鐵,你那有沒有媒體服務,我這邊需要播一個鈴聲啊!」。
「有的,給你。」
IronBank
就會將之前服務提供者提供給他的媒體物件交給服務使用者。
接下來我們來看具體到程式碼上是如何使用的:首先是作為服務使用方,也就是上一張圖右半部分,傳統的做法是先宣告一個介面型別,然後 new
出介面的實現類給他賦值。
而使用了 IronBank
的時候,你是不需要關心介面的實現類到底是誰的。這就是 IronBank
唯一的用處,隱藏實現類,做到徹底的面相介面程式設計。
IronBank
將模組之間依賴倒置,由之前的服務提供方被動的接受呼叫方呼叫變為,服務方主動提供服務給呼叫方。
那作為服務提供方需要做些什麼事呢?非常簡單,你只需要給你的物件提供 public static
方法,並加上一個 @Creator
註解,告訴 IronBank
這是一個建立器方法就可以了,其他任何事情,都不需要考慮。
這裏的建立器方法是可以有引數的,在接收時實際是使用另一個變長 Object
引數來接收。
而相對於繁雜的應用場景,也有對應的解決辦法,例如這裏的建立器方法是含引數的。看到示例第一個引數是 tag,第二個是 context 。但是你希望呼叫者在傳的時候將Context作為第一個引數,tag作為第二個引數。
那你在宣告的時候,需要顯示的宣告引數,加一個 params,然後寫上你希望的引數順序。
這個@Creator註解裏面還有很多引數,比如這裏返回的是IMedia型別的物件,如果IMedia介面還繼承了一個A介面,這裏我雖然返回的是IMedia,但我不想外部知道,我就想外部知道我返回的只是個A,這樣也是可以顯示的,在註解引數中宣告就行了。以及還有方法的類所在檔案自定義等等等等…… 就不一一列舉了。
在使用上,爲了接入方使用方便,我們也對IronBank做了非常多的體驗優化。
我們通過自定義lint來使 IDE 可以檢查引數型別是否正確。比如前面舉的例子,如果宣告的時候第一個引數是String,第二個引數是Context,如果你傳錯了,IDE 直接就報紅了。
還有前面我們看到了,IronBank提供了一種類似依賴注入的方式去建立物件,既然是類似依賴注入,一定會碰上迴圈引用問題。我們自定義的Lint,也完美通過靜態程式碼分析,在編譯前就避免了這個問題。
同時在開發的時候還提供了一個Android Studio IDE外掛,可以用來幫你把引數智慧補全,自動生成程式碼。前面看到,在寫IronBank.get()方法的時候得寫很多字,如果有智慧補全會少寫很多。
業務狀態解耦
前面講的 IronBank
適用的場景是無狀態的服務,而做業務APP開發的時候,更多的是有業務狀態的物件。比方說通常長鏈與推送功能是等到使用者登入了以後纔會去啟動,但具體到程式碼上,推送模組是根本不知道使用者什麼時候登入的,這就是一個業務狀態的問題。
對此我們引入了BizLifecycle的介面,它與Android上的Application物件功能類似。只不過它用來管理的是業務的生命週期,而不是應用的。
在程式碼邏輯上,每個模組如果關心你所需要的業務生命週期,只需要註冊一個Lifecycle就行了,同時註冊的過程也只需要一個註解,由編譯外掛解決了。
可以看到,其實這樣的一種能力用事件通知也可以做到,比方說廣播或者EventBus,但是我們刻意遮蔽了這種方式,就是因為事件通知這種功能你是很難去追蹤的。你不知道一個訊息傳送了以後,它的接受者是在哪裏。
相信大家也能夠想象到,一個應用如果廣播氾濫,到處都是事件接收事件傳送,專案程式碼會變得多麼嚇人。
講到這裏,整個模組化解耦的全部能力就介紹完了。
接下來,我們再從巨集觀角度去看一下整個專案的結構,分為三級,最上層是業務模組,緊接著是一些可選的功能元件,最底層則是與專案無關的公共依賴。
最終,專案結構就是如圖中所示的這樣。但如果你真直接這麼做,你一定是會煩死的。
為什麼?
第一:這麼多的模組,直接用原始碼依賴去編譯,編譯時間至少在10分鐘以上;
第二:模組的隔離幾乎為0,任何一個人依舊可以修改任何一個模組的程式碼,並且很容易;
第三:在發版本以後,如果某一個模組有BUG,再去修復,缺乏一個版本的概念,尤其是在跨團隊的時候,最終一定會出現版本分裂問題。
平臺化支援
解決辦法我想大家都知道,就是將模組引用改為aar引用。aar引用最大的優勢就在於模組版本的管理與跨團隊的協作。
目前國內對Android領域的探索越來越深,應用規模也越來越大,爲了降低大型專案的複雜性和耦合度,同時也爲了適應模組重用、多團隊並行開發測試等等需求,必須有一套合適的模組化平臺。
這裏是餓了麼目前使用的模組化平臺,大家可以從這張圖中感受一下。
模組化平臺,主要的功能是很明顯的,就是用於構建模組,在這之上,還有隱含的功能,就是集中了構建模組的許可權,可以更便於統一管理;
最重要的優勢在於模組版本的管理,你可以很清晰的知道當前主應用所接入的模組的版本是哪個,當前最新構建的SNAPSHOT是哪個,以及每個版本的更新日誌;
這樣做了以後,在跨團隊協作上的溝通就大大降低了,如果你已經接入或者即將接入的模組是另一個團隊開發的模組元件,那你可以直接關注它,它的所有版本變動日誌,最新版本全都一目瞭然;
並且可以通過平臺簡化模組的測試與模組釋出的流程,比如提測的時候,如果是一次相容版本的釋出,你只需要告訴測試提測分支,測試可以自己根據現線上上應用的tag,同時引入當前提測的模組替換老版本的模組重新編譯,很容易就能控制變數。
引入了平臺化以後,我們再從工程結構的角度看一下:就目前嘗試下來,這兩種結構是最合適 Android
工程模組化的。一種是 submodule
,一種是 multi-project
。
首先看 submodule
:這種結構是 Android
預設的多模組結構,在一個工程下面有多個模組。圖上每個綠色的方塊都代表了一個git倉庫,所有子模組都包含在主工程模組內。這種結構也是git預設支援的 submodule
結構,你只需要用最下面的這句git命令就可以將他們關聯在一起。
它的好處就是所有都是預設的,任何一個人理解起來都是很直觀。當然,它也有不適合的,就是協作開發的時候,所有人都在 app module
上測試自己的模組,很容易互相影響,主工程的 git
分支也會非常繁雜。
與之對應的, multi-project
能很好的解決這個問題:所有模組都是一個獨立的工程,他們在檔案系統上是並列關係,每個模組所在的工程纔是一個git倉庫。
對於單模組的操作達到最大化的遍歷當然也是有犧牲的,就是這種結構很不利於全倉庫整體的管理,對新人很不友好。比如我想所有模組同時初始化,同時切換到 develop
分支,對此,我們內部的處理方式是通過 shell
指令碼達到全倉庫批量處理。
同時還會對工程名會有一定的規範要求(非必須),主要原因是在模組聯調的時候。
我們看到這段程式碼是寫在 setting.gradle
檔案中的,他根據讀取本地的 local.properties
檔案,來 include
一個模組的原始碼,方便在模組聯調的時候可以很容易的修改多模組的程式碼。
但是要求每個模組工程的資料夾名稱是以模組名加上 Project
這樣來命名,比如 order
模組所在的工程資料夾名就叫 OrderProject。
當然,你也可以不遵守,只不過不遵守就得寫更多程式碼,我這裏是直接用了迴圈,不遵守的話可能就需要把迴圈拆開手敲了。
以上兩種工程結構各有各的好處,沒有好壞,只有合不合適,我們內部兩種結構也都有團隊在用。
這裏是模組聯調的注意事項,如果你模組是以原始碼引入的,可能還有其他模組引用了同樣模組的aar,就會造成衝突,需要自己判斷一下,加個自定義方法也好,用編譯外掛也可以,都能做到讓原始碼引用與aar引用互斥。
模組化架構主要思路就是分而治之,在拆分的時候最重要的是,把依賴整理清楚,哪些是業務模組,哪些是可選的功能元件。最後爲了團隊方便以及更快的適應,還需要開發一些輔助工具,比如前面說的IronBank、BizLifecycle、初始化指令碼等等,都是必不可少的。
點選「閱讀原文」可觀看講師現場分享視訊。
【推薦閱讀】