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、初始化腳本等等,都是必不可少的。
【作者簡介】張濤,餓了么資深Android工程師,“開源實驗室”博主,Kotlin 技術推廣者。2013年開始從事Android開發,帶過團隊,做過架構,寫過應用,做過開源社區。目前在餓了么商戶端負責應用模塊化平臺與插件化平臺的設計和開發。本文來自張濤在“攜程技術沙龍——無線技術工程化”上的分享。
https://zhuanlan.zhihu.com/p/36999401
總結
以上是生活随笔為你收集整理的Android工程模块化平台的设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 长连接/websocket/SSE等主流
- 下一篇: 携程基于大数据分析的实时风控体系