作者:郭孝星
校對:郭孝星
文章狀態:已完成
關於專案
BeesAndroid專案旨在通過提供一系列的工具與方法,降低閱讀Android系統原始碼的門檻,讓更多的Android工程師理解Android系統,掌握Android系統。
文章目錄
- 一 發現問題
- 二 提出方案
- 2.1 模組容器
- 2.2 模組架構
- 2.3 模組通訊
- 2.4 模組生命週期
- 2.5 模組初始化
- 三 解決問題
模組化也是近兩年經常被提及的一個技術點,究其原因,隨著公司業務的逐漸壯大,主應用的工程體積也逐漸變大,管理和編譯都變得十分困難。再加上隨著公司業務的發展,主應用功能拆分和研發團隊的拆分已成必然,這就要求主應用裡的各個模組能夠獨立編譯、獨立執行、不與主工程以及其他模組相互耦合。
模組化的過程其實是一個解決技術債的過程,每個公司的技術債也各不相同,因為模組化的過程是一個因地制宜的過程,沒有放之四海而皆準的方案,一般說來,模組化分為以下三步:
- 發現問題:發現問題就是理清公司現用的技術架構,清理技術債。
- 提出方案:提出方案包含兩個方面,一方面試新的工程架構,另一方面是做好新需求排期的安排(是否會阻塞新需求)。
- 解決問題:新方案的推行也是逐步進行的,新的模組要做好灰度釋出,應用回滾等工作。
而模組化實踐起來並不是一件簡單的事情,每家的應用都有自己的特殊情況,沒有放之四海而皆準的技術方案,整體上來說,模組的拆分牽扯工程框架(MVP)、模組通訊(程式內、跨程式)、Library多端複用、資源拆分等多種情況,那麼模組化最終要達到一個什麼樣的目標呢??
- 主應用的其他模組可以快速移植到其他應用。
- 減少Build時間,各模組交由各團隊獨立負責,程式碼責任制。
- 主應用的各模組可以拆分成獨立的應用,模組功能服務化。
- 模組可以獨立開發、獨立編譯、獨立執行,無需藉助任何主工程環境,模組之間可以快速替換。
- 無侵入式的配置各種獨立服務,例如:賬戶資訊、設定資訊、網路服務、圖片載入服務、埋點服務、下拉重新整理樣式、錯誤狀態等。
- Library可以快速便捷的在多端使用。庫裡功能儘量獨立在View或者Fragment,在使用的時候可以直接新增到宿主Activity裡,宿主Activity可以自己新增下載重新整理樣式、Action bar樣式等。
理解了具體的模組化需求,我們接下來開始真正的開始進行模組化,光說不練假把式,空談沒有任何意義。下面的模組化都是圍繞著我司主應用大風車而展開的。
廣告時間到?
大風車:http://dafengche.souche.com/
一款SaaS產品,提供建站系統、ERP、CRM、微信行銷系統、財務系統等解決方案,旨在幫助車商及4S集團提升運營和管理水平。
在分析方案之前,首先我們要知道我們的應用出了什麼問題,針對大風車這個專案,我們來具體分析下。
一 發現問題
大風車與2015年上線,經過三年的發展,業務有了很大的增長,功能也逐漸完善,大風車裡程碑如下所示:
我們和其他團隊一樣,在業務的發展中,主工程的架構也在不斷的變化,我簡單總結一下:
- 微型專案:早期就是一個工程,幾個人,那個時候也是業務跑量的時候,沒有特別注意架構桑的問題。
- 小型專案:隨著業務的發展,業務種類也逐漸增多,這個時候我們就把一些業務模組拆分成了獨立的Library,體抽了一個Base Library,提供了一些工具庫和樣式上的東西。
- 中型專案:業務進一步增長,單純搞Module Library已經不好用了,這個時間外掛化框架很火,很強大,但是問題也很多,我們最終採用了Router的方式實現了一套偽模組化方案。
- 大型專案:時間來到了現在,公司業務有了爆發式的增長,公司的應用也有原來的2個變成了5個,而且還有很多定製App、影子App,模組App等需求提交給我們,在上一套偽模組化方案的基礎上,我們要實現一套真正的模組化方案。
大風車工程架構如下圖所示:
可以看到整個大風車的主工程可以分為四層:
- 主工程業務層
- 模組業務層
- 公司框架層
- 第三方框架層
所以你可以看到這個工程與模組之間、模組與模組之間的依賴關係真的是美如畫?,相互引用導致擴充套件性和可維護性都很差,而且難以測試。我們來看看這種專案架構的問題在哪裡:
- 模組邊界被破壞,模組之間相互依賴,模組升級複雜,測試困難。
- 基礎工程中心化,類庫積累過重,難以維護。
- 模組依賴主工程,所有模組無法獨立編譯、獨立釋出,編譯耗時,APK體積巨大,多團隊無法並行開發。
二 提出方案
我們先來看一看重構後的架構,如下所示:
重構後的大風車採用多容器架構,我們來看看這套架構是如何實現的。
2.1 模組容器
既然要把業務模組化,那就要有承載模組的容器,目前來說主要用以下三種容器:
- Native容器:Android/iOS原生的容器,承載使用原生實現的業務,例如Android就有Activity容器、Fragment容器以及更加細粒度的View容器。
- H5容器:傳統WebView承載的頁面。
- ReactNative/Weex/Flutter容器:這是自Facebook從15年推出RN方案開始後,流行起來的方案,這套方案的思想就是將JS元件轉義成Native元件,從而實現一套介面,多端執行的效果。
? 注:手淘提供了細粒度的View容器方案:Virtualview-Android,它可以通過下發XML配置檔案,動態的渲染View。
從長遠來看,這三套容器都不是用來相互取代對方,而是會長期並存,取長補短,相互助益。
- Native容器:Native容器適合用來編寫應用的基礎骨架頁面,例如主頁等,這在iOS上也用來避免稽核上的問題。
- H5容器:H5容器適合用來編寫經常需要變化的頁面,例商家活動頁等。
- ReactNative/Weex/Flutter容器:這一類容器就適合用來編寫常規的頁面介面,由於這一類容器也天然帶有熱更新能力,所以它也可以用來解決動態釋出,熱修復等方面的問題。
那如何實現這三套容器呢??
- Native容器:外掛化方案,外掛化方案大體都比較相似,具體可以參見我這一篇文章的討論VirtualAPK。
- H5容器:WebView封裝,Jockey通訊協議封裝。
- ReactNative/Weex/Flutter容器:ReactNative/Weex/Flutter容器工程化體系搭建,事實上,用RN或者Weex寫頁面是十分簡單的,它的複雜性在於工程化體系的搭建。
這三套容器的實現,我們後續都有詳細的文章來討論,我們接著來看看模組架構的實現。
2.2 模組架構
一個良好的系統設計縱向分層,橫向模組化。我們來看看從縱向和橫向的角度如何去設計一個模組。
2.2.1 縱向架構
一般說來,從縱向角度,一個模組一般可以劃分為三個部分:
- Api層:介面部分,提供對外的介面和資料結構。
- Implementation層:實現部分,提供對業務邏輯的實現,它往往和應用的狀態、賬戶資訊等息息相關,library為它提供具體的功能,它決定如何去載入、組織、以及展示這些功能。
- Library層:功能部分,為implementation提供一些具體的功能。
一個模組就這樣可以被劃分為三層,如果是更加複雜的模組,我們還有做好層與層間的解耦與通訊,我們接著來看一下橫向架構如何實現。
2.2.2 橫向架構
橫向架構就是如何去處理檢視、資料與業務邏輯的關係,關於這一塊內容的實踐,從最初的MVC、到MVP、MVVM,各種架構的目的都都是希望模組的耦合性更低、獨立性更強,移植性更好。
Google自己也開了一個Repo來討論這些框架的最佳實踐,如下所示:
- MVC:PC時代就有的架構方案,在Android上也是最早的方案,Activity/Fragment這些上帝角色既承擔了V的角色,也承擔了C的角色,小專案開發起來十分順手,大專案就會遇到耦合過重,Activity/Fragment類過大等問題。
- MVP:為了解決MVC耦合過重的問題,MVP的核心思想就是提供一個Presenter將檢視邏輯I和業務邏輯相分離,達到解耦的目的。
- MVVM:使用ViewModel代替Presenter,實現資料與View的雙向繫結,這套框架最早使用的data-binding將資料繫結到xml裡,這麼做在大規模應用的時候是不行的,不過資料繫結是一個很有用的概念,後續Google又推出了ViewModel元件與LiveData元件。ViewModel元件規範了ViewModel所處的地位、生命週期、生產方式以及一個Activity下多個Fragment共享ViewModel資料的問題。LiveData元件則提供了在Java層面View訂閱ViewModel資料來源的實現方案。
Google官方也提供了MVP的實現,這個MVP框架的核心思想如下所示:
- 使用Contract介面統一管理View介面和Presenter介面的定義,當然這個也不是一定非得這麼寫,並不是每個View介面和Presenter介面都可以成對出現,可能會出現一個VIew介面對應介個Presenter介面或者一個Presenter介面對應幾個View介面的情況。
- 採用Fragment實現View介面,我們知道Presenter介面主要定義的是業務邏輯,例如:載入下一頁、下拉重新整理、編輯、提交、刪除等,這些都是在頁面的生命週期方法或者setXXXListener裡呼叫的,Fragment的生命週期正好可以用的上,而且Fragment還可以獨立的填充到其他Activity裡。
官方的這套框架存在兩個問題:
- 正如上面所說的View介面交由Fragment實現,但是如果一個頁面由多個獨立的子頁面組合而成,那是不是要在這個頁面新增幾個Fragment,這顯示是不合理的,鑑於這種情況,我們可以退而求其次,採用自定義View的方式來實現View介面。
- 當頁面增大到一定的量級的時候,就出出現大量的Presenter實現類,其實大風車現有的工程就有很多的Presenter實現類,Presenter實現類和View實現類需要相互set,以便View可以呼叫Presenter載入資料,Presenter呼叫View重新整理UI,管理這些Presenter類是個很大的問題,而且如果別人要繼承你這個View,你還要告訴它在View的生命週期裡如何去處理Presenter的建立和銷燬,以及何時去載入資料等等。如果出現跨部門甚至跨跨城市的合作時,溝通成本就非常的高。
總的說來,就是當業務量急劇膨脹的時候,就會需要寫大量的View介面和Presenter類,而且這還牽扯到Presenter類與Activity生命週期同步的問題,在大型專案面前,這些操作都會變得十分複雜。
綜上所述,一個理想的方案就是結合ViewModel元件與LiveData元件來實現MVVM框架。
這套框架有兩個重要的原則:
- 任何不處理UI邏輯和使用者互動的程式碼都不應該寫到Activity或者Fragment中,因為Activity或者Fragment是十分脆弱的,低記憶體、配置發生變化、進入後臺等等都可能導致它們的銷燬,應該最大限度的減低對Activity或者Fragment的依賴。
- 應該使用一個持久資料模型來驅動我們的UI,資料可以在該套模型裡進行持久化,一旦Activity或者Fragment被銷燬,使用者資料不會丟失,這套模型專門用來處理資料邏輯,使應用的資料邏輯與檢視邏輯向分離,讓應用變得更易維護。
? 注:這裡可能有人有疑問,非得用Lifecycle元件嗎,利用View的onAttachToWindow()、onDetachToWindow()這些方法來模擬Activity或者Fragment的生命週期不可以嗎,事實上View的生命週期在一些特殊的場景下是不可靠的,例如:RecyclerView、ViewPager,所以我們還是需要利用Lifecycle元件來監聽Activity或者Fragment的生命週期變化。
2.3 模組通訊
解決了模組間的解耦問題,另一個就是模組間的通訊問題。在一個大型的應用裡很多模組都是可以獨立執行甚至獨立成一個App的,這就牽扯到模組間的資料互動和通訊問題,例如:最常見的一種場景就是子模組需要知道主應用裡的登入資訊等等,模組間的通訊業可以分為兩種情況:
- 程式內通訊:模組都執行在同一個程式中。
- 跨程式通訊:模組執行在不同的程式中。
2.3.1 程式內通訊
程式內通訊的手段有很多種,最常見的就是EventBus,
EventBus 用來完成 Activities, Fragments, Threads, Services 之間的資料互動和通訊。
EventBus是早期頁面通訊和模組通訊常見的手段,它的好處是顯而易見的,將事件的釋出者與訂閱者解耦,無需再定義一堆複雜的回撥介面,但是隨著工程的膨脹,它的問題也凸顯出來,具體說來:
- Event並非所有通訊常見的最佳方式,它主要適合一對多的廣播場景,如果業務中的通訊需要一組介面時,就需要定義多個Event,程式碼複雜。
- 大量的Event的類,難以管理,如果應用越來越龐大,模組劃分也越來越多,這個Event就變得難以維護。
但是即便這樣,EventBus還是一個優秀的程式內通訊的方式。
? 注:當然除了EventBus以外,在簡單的通訊場景下,我們還可以選擇LocalBroadcastReceiver。LocalBroadcastReceiver是一個應用內的局域廣播,它也是利用一個Looper Handler維護一個全域性Map進行應用內部通訊,與EventBus不同,它傳送的是字串。LocalBroadcastReceiver在面臨業務膨脹的時候,也會遇到訊息字串的管理問題。
2.3.2 程式間通訊
跨程式通訊可以藉助Content Provider來完成,
Content Provider 底層採用的是Binder機制,用來完成程式間的資料互動和通訊。
模組通訊採用Content Provider的方式來解決,一個比較常見的場景就是多模組共享登入資訊,登入資訊可以用Content Provider來儲存,當登入狀態發生變化時,可以通知到各個模組。
通過上面的分析,我們已經完成了一個設計良好的模組,但是模組的接入仍然面臨著諸多問題,例如:如何界定模組的生命週期,使用者資訊等如何同步,模組如何進行註冊以及初始化等問題。少量的模組,這些都不是問題,但是當模組增長到一定的數量級的時候,這個問題就會變得十分突出。
2.4 模組生命週期
模組生命週期的生命週期可以做如下劃分:
- 程式啟動:執行模組的初始化。
- Account初始化:執行模組使用者資訊同步,告知模組使用者已經登入。
- Account登出:執行模組使用者資訊同步,告知模組使用者已經登出。
- 程式退出:執行模組的退出。
2.5 模組初始化
模組的初始化一般在Application裡進行,當然也有懶載入的模組,模組的初始化一般傳遞應用上下文資訊,使用者資訊,配置引數等資訊,這裡可以考慮對模組進行自動初始化,具體流程如下所示:
- 新增依賴,依賴也分為兩種:編譯期依賴和執行期依賴,
- 配置資料,註冊服務。
- 啟動服務。
三 解決問題
模組化拆分不是一個簡單的事情,沒法一蹴而就,也不可能讓團隊全部停下來去做拆分重構,所以真正實施模組化需要按照以下幾個步驟循序漸進的進行。
- 心態調整
技術上的重構並不能帶來短期上的收益,它是一個長時間才能顯現好處的事情,你往往花費了很多時間來做這些事情,它也非常的有意義,但是老闆看不到,業務上也不會帶來明顯的增長。所以第一件事情,就是做好團隊成員的思想工作。
事實上,大部分研發同學都還是非常有技術追求的,但是我們工程通常有很多歷史遺留問題,也就是所謂的技術債,要去重構這些東西,成本是非常高的,面對這種情況在加上平時業務需求多,時間緊,大家通常都會想:
重構難度這麼大,出了問題怎麼辦,算了,別人怎麼寫,我也怎麼寫好了。
這是一個很普遍的現象,這種情況下就需要有一個有魄力的leader打響第一槍,有了第一個階段的重構,大家看到了曙光,就會開始陸續吐槽原來的設計有多麼爛,應該如何設計等等。
- 模組拆分:對需要重構的模組進行拆分,包括程式碼,資源等等。
- 灰度釋出:對小部分使用者推送重構版本。
- 應用回滾:對git程式碼做好tag,遇到問題時隨時準備迴歸。
附錄
最後囉嗦幾句:
- 能用原生實現的不要用第三方庫實現,如果實在需要第三方庫實現,例如:圖片庫、網路庫,也不要直接使用,要做好封裝和介面隔離,方便以後做替換。
- 頁面間的繼承關係一定要謹慎,除非是專門為繼承而設計的頁面,否則應該考慮使用組合或其他侵入性更低的方式來解決問題。
- 專案中為某個需求提出瞭解決方案時,如果這種需求其他團隊還可能會遇到,就要評估一下這個方案耦合性怎麼樣,以後能否直接給其他團隊使用,較少團隊間的重複勞動。
- 對外提供的功能儘量做好介面封裝,不要直接暴露內部細節,這樣日後也可以直接替換內部邏輯,而不至於影響業務方。
本篇文章到這裡就結束了,歡迎關注我們的BeesAndroid微信公眾平臺,BeesAndroid致力於分享Android系統原始碼的設計與實現相關文章,也歡迎開源愛好者參與到BeesAndroid專案中來。
微信公眾平臺