一、前言
隨着業務的快速發展,現在的互聯網App越來越大,為了提高團隊開發效率,模塊化開發已經成為主流的開發模式。正好最近完成了vivo官網App業務模塊化改造的工作,所以本文就對模塊化開發模式進行一次全面的介紹,並總結模塊化改造經驗,幫助兄弟項目避坑。
二、什么是模塊化開發
首先我們搞清兩個概念,Android客戶端開發目前有兩種模式:單工程開發模式和模塊化開發模式。
-
單工程開發模式:早期業務少、開發人員也少,一個App對應一個代碼工程,所有的代碼都集中在這一個工程的一個module里。
-
模塊化開發模式:簡單來說,就是將一個App根據業務功能划分成多個獨立的代碼模塊,整個App是由這些獨立模塊集成而成。
在講什么是模塊化開發前,我們先定義清楚兩個概念:組件和模塊。
-
組件:指的是單一的功能組件,比如登錄組件、分享組件;
-
模塊:廣義上來說是指功能相對獨立、邊界比較清晰的業務、功能等,本文如果單獨出現模塊這個詞一般是該含義。狹義上是指一個業務模塊,對應產品業務,比如商城模塊、社區模塊。
模塊和組件的本質思想是一樣的,都是為了業務解耦和代碼重用,組件相對模塊粒度更細。在划分的時候,模塊是業務導向,划分一個個獨立的業務模塊,組件是功能導向,划分一個個獨立的功能組件。
模塊化開發模式又分為兩種具體的開發模式:單工程多module模式和多工程模式。
單工程多module模式:
所有代碼位於一個工程中,模塊以AndroidStudio的module形式存在,由一個App module和多個模塊module組成。如圖:

多工程模式:
每個模塊代碼位於一個工程中,整個項目由一個主模塊工程和多個子模塊工程組成。其中主模塊工程只有一個App module,用於集成子模塊,進行整體調試、編包。子模塊工程由一個App module和一個Library module組成,App module中是調試、測試代碼,Library module中是業務、功能代碼。如下圖:


下面我們來對比一下單工程多module模式和多工程模式的優缺點:

通過上面的對比,我們可以看出來,多工程模式在代碼管理、開發調試、業務並行等方面有明顯優勢,非常適合像vivo官網這種業務線多、工程大、開發人員多的App,所以vivo官網目前就采用的此模式。本文在講解模塊化開發時,一般也是指多工程模式。
單工程多module模式,更適合開發人員少、業務並行程度低的項目。但是多工程模式也有兩個缺點:代碼倉較多、開發時需要打開多個工程,針對這兩個缺點,我們也有解決方案。
代碼倉較多的問題
要求我們在拆分模塊時粒度不能太細,當一個模塊膨脹到一定程度時再進行拆分,在模塊化帶來的效率提升與代碼倉管理成本增加間保持平衡。
要打開多個工程開發的問題
我們基於Gradle插件開發了代碼管理工具,可以方便的切換通過代碼依賴子模塊或者maven依賴子模塊,實際開發體驗跟單工程多module模式一樣,如下圖;

模塊化開發的流程也很簡單:
-
版本前期,每個模塊由特定的開發人員負責,各子模塊分別獨立開發、調試;
-
子模塊開發完成后,集成到主模塊工程進行整體調試;
-
集成調試成功后,進入測試。
三、模塊化開發
3.1 我們為什么要做模塊化開發呢?
這里我們說說單一工程開發模式的一些痛點。
團隊協作效率低
-
項目早期業務少、開發人員也少,隨着業務發展、團隊擴張,由於代碼都在同一個工程中,雖然各個人開發的功能不同,但是經常會修改同一處的代碼,這時就需要相關開發人員溝通協調以滿足各自需求,增加溝通成本;
-
提交代碼時,代碼沖突也要溝通如何合並(否則可能引起問題),增加合代碼成本;
-
無法進行並行版本開發,或者勉強進行並行開發,代價是各個代碼分支差異大,合並代碼困難。
代碼維護成本高
- 單一工程模式由於代碼都在一起,代碼耦合嚴重,業務與業務之間、業務與公共組件都存在很多耦合代碼,可以說是你中有我、我中有你,任何修改都可能牽一發而動全身,隨着版本的迭代,維護成本會越來越高。
開發調試效率低
- 任何一次的修改,即使是改一個字符,都需要編譯整個工程代碼,隨着代碼越來越多,編譯也越來越慢,非常影響開發效率。
3.2 如何解決問題
說完單一工程開發模式的痛點,下面我們看看模塊化開發模式怎么來解決這些問題的。
提高團隊協作效率
-
模塊化開發模式下,根據業務、功能將代碼拆分成獨立模塊,代碼位於不同的代碼倉,版本並行開發時,各個業務線只在各自的模塊代碼倉中進行開發,互不干擾,對自己修改的代碼負責;
-
測試人員只需要重點測試修改過的功能模塊,無需全部回歸測試;
-
要求產品層面要有明確的業務划分,並行開發的版本必須是不同業務模塊。
降低代碼維護成本
-
模塊化開發對業務模塊會划分比較明確的邊界,模塊間代碼是相互獨立的,對一個業務模塊的修改不會影響其他模塊;
-
當然,這對開發人員也提出了要求,模塊代碼需要做到高內聚。
提高編譯速度
-
開發階段,只需要在自己的一個代碼倉中開發、調試,無需集成完整App,編譯代碼量極少;
-
集成調試階段,開發的代碼倉以代碼方式依賴,其他不涉及修改的代碼倉以aar方式依賴,整體的編譯代碼量也比較少。
當然模塊化開發也不是說全都是好處,也存在一些缺點,比如:
1)業務單一、開發人員少的App不要模塊化開發,那樣反而會帶來更多的維護成本;
2)模塊化開發會帶來更多的重復代碼;
3)拆分的模塊越多,需要維護的代碼倉越多,維護成本也會升高,需要在拆分粒度上把握平衡。
總結一下,模塊化開發就像我們管理書籍一樣,一開始只有幾本書時,堆書桌上就可以了。隨着書越來越多,有幾十上百本時,我們需要一個書櫥,按照類別放在不同的格子里。對比App迭代過程,起步時,業務少,單一工程模式效率最高,隨着業務發展,我們要根據業務拆分不同的模塊。
所有這些目的都是為了方便管理、高效查找。
四、模塊化架構設計
模塊化架構設計的思路,我們總結為縱向和橫向兩個維度。縱向上根據與業務的緊密程度進行分層,橫向上根據業務或者功能的邊界拆分模塊。
下圖是目前我們App的整體架構。

4.1 縱向分層
先看縱向分層,根據業務耦合度從上到下依次是業務層、組件層、基礎框架層。
-
業務層:位於架構最上層,根據業務模塊划分(比如商城、社區等),與產品業務相對應;
-
組件層:App的一些基礎功能(比如登錄、自升級)和業務公用的組件(比如分享、地址管理),提供一定的復用能力;
-
基礎框架層:完全與業務無關、通用的基礎組件(比如網絡請求、圖片加載),提供完全的復用能力。
框架層級從上往下,業務相關性越來越低,代碼穩定性越來越高,代碼入倉要求越來越嚴格(可以考慮代碼權限收緊,越底層的代碼,入倉要求越高)。
4.2 橫向分模塊
-
在每一層上根據一定的粒度和邊界,拆分獨立模塊。比如業務層,根據產品業務進行拆分。組件層則根據功能進行拆分。
-
大模塊可以獨立一個代碼倉(比如商城、社區),小模塊則多個模塊組成一個代碼倉(比如上圖中虛線中的就是多個模塊位於一個倉)。
-
模塊要高內聚低耦合,盡量減少與其他模塊的依賴。
面向對象設計原則強調組合優於繼承,平行模塊對應組合關系,上下層模塊對應繼承關系,組合的優點是封裝性好,達到高內聚效果。所以在考慮框架的層級問題上,我們更偏向前者,也就是拆分的模塊盡量平行,減少層級。
層級多的問題在於,下層代碼倉的修改會影響更多的上層代碼倉,並且層級越多,並行開發、並行編譯的程度越低。
模塊依賴規則:
-
只有上層代碼倉才能依賴下層代碼倉,不能反向依賴,否則可能會出現循環依賴的問題;
-
同一層的代碼倉不能相互依賴,保證模塊間徹底解耦。
五、模塊化開發需要解決哪些問題
5.1 業務模塊如何獨立開發、調試?
方式一:每個工程有一個App module和一個Library module,利用App module中的代碼調試Library module中的業務功能代碼。
方式二:利用代碼管理工具集成到主工程中調試,開發中的代碼倉以代碼方式依賴,其他模塊以aar方式依賴。
5.2 平行模塊間如何實現頁面跳轉,包括Activity跳轉、Fragment獲取?
根據模塊依賴原則,平行模塊間禁止相互依賴。隱式Intent雖然能解決該問題,但是需要通過Manifest集中管理,協作開發比較麻煩,所以我們選擇了路由框架Arouter,Activity跳轉和Fragment獲取都能完美支持。另外Arouter的攔截器功能也很強大,比如處理跳轉過程中的登錄功能。
5.3 平行模塊間如何相互調用方法?
Arouter服務參考——https://github.com/alibaba/ARouter。
5.4 平行模塊間如何傳遞數據、驅動事件?
Arouter服務、EventBus都可以做到,視具體情況定。
六、老項目如何實施模塊化改造
老項目實施模塊化改造非常需要耐心和細心,是一個循序漸進的過程。
先看一下我們項目的模塊化進化史,從單一工程逐步進化成紡錘形的多工程模塊化模式。下圖是進化的四個階段,從最初的單個App工程到現在的4層多倉結構。



注:此圖中每個方塊表示一個代碼倉,上層代碼倉依賴下層代碼倉。
早期項目都是采用單一工程模式的,隨着業務的發展、人員的擴張,必然會面臨將老項目進行模塊化改造的過程。但是在模塊化改造過程中,我們會面臨很多問題,比如:
-
代碼邏輯復雜,缺乏文檔、注釋,不敢輕易修改,害怕引起功能異常;
-
代碼耦合嚴重,你中有我我中有你,牽一發動全身,拆分重構難度大;
-
業務版本迭代與模塊化改造並行,代碼沖突頻繁,影響項目進度;
相信做模塊化的人都會遇到這些問題,但是模塊化改造勢在必行,我們不可能暫停業務迭代,把人力都投入到模塊化中來,一來業務方不可能同意,二來投入太多人反而會帶來更多代碼沖突。
所以需要一個可行的改造思路,我們總結為先自頂向下划分,再自底向上拆分。
自頂向下
- 從整體到細節逐層划分模塊,先划分業務線,業務線再划分業務模塊,業務模塊中再划分功能組件,最終形成一個樹狀圖。

自底向上
-
當我們把模塊划分明確、依賴關系梳理清楚后,我們就需要自底向上,從葉子模塊開始進行拆分,當我們把葉子模塊都拆分完成后,枝干模塊就可以輕松拆分,最后完成主干部分的拆分。
-
另外整個模塊化工作需要由專人統籌,整體規划,完成主要的改造工作,但是有復雜的功能也可以提需求給各模塊負責人,協助完成改造。
下面就講講我們在模塊化改造路上打怪升級的一些經驗。總的來說就是循序漸進,各個擊破。
6.1 業務模塊梳理
這一步是自頂向下划分模塊,也就是確定子模塊代碼倉。一個老項目必然經過多年迭代,經過很多人開發,你不一定要對所有的代碼都很熟悉,但是你必須要基本了解所有的業務功能,在此基礎上綜合產品和技術規划進行初步的模塊划分。
此時的模塊划分可以粒度粗一點,比如根據業務線或者大的業務模塊進行划分,但是邊界要清晰。一個App一般會有多個業務線,每個業務線下又會有多個業務模塊,這時,我們梳理業務不需要太細,保持2層即可,否則過度的拆分會大大增加實施的難度。

6.2 抽取公共組件
划分完模塊,但是如果直接按此來拆分業務模塊,會有很大難度,並且會有很多重復代碼,因為很多公共組件是每個業務模塊都要依賴的(比如網絡請求、圖片加載、分享、登錄)。所以模塊化拆分的第一步就是要抽取、下沉這些公共組件。
在這一步,我們在抽取公共組件時會遇到兩類公共組件,一類是完全業務無關的基礎框架組件(比如網絡請求、圖片加載),一類是業務相關的公共業務組件(比如分享、登錄)。
可以將這兩類公共組件分成兩層,便於后續的整體框架形成。比如我們的lib倉放的是基礎框架組件和core倉放的是業務公共組件。如下圖

6.3 業務模塊拆分
抽取完公共組件后,我們要准備進行業務模塊的拆分,這一步耗時最長,但也是效果最明顯的,因為拆完我們就可以多業務並行開發了。
確定要拆分的業務模塊(比如下圖的商城業務),先把代碼倉拉出來,新功能直接在新倉開發。
那老功能該怎么拆分遷移呢?我們不可能一口吃成大胖子,想一次把一個大業務模塊全部拆分出來,難度太大。這時我們就要對業務模塊內部做進一步的梳理,找出所有的子功能模塊(比如商城業務中的支付、選購、商詳等)。

按照功能模塊的獨立程度,從易到難逐個拆分,比如支付的訂單功能比較獨立,那就先把訂單功能的代碼拆分到新倉。
6.4 功能模塊拆分
在拆分具體功能時,我們依然使用Top-Down的邏輯來實施,首先找到入口類(比如Activity),遷移到新的代碼倉中,此時你會發現一眼望去全是報紅,就像拔草一樣帶出大量根須。依賴的布局、資源、輔助類等等都找不到,我們按照從易到難的順序一個個解決,需要解決的依賴問題有以下幾類:
1)簡單的依賴,比如字符串、圖片。
這類是最容易解決,直接把資源遷移過來即可。
2)較復雜的依賴,比如布局文件、drawable。
這類相對來說也比較容易解決,逐級遷移即可。比如布局依賴各種drawable、字符串、圖片,drawable又依賴其他的drawable等,自頂向下逐個遷移就能解決。
3)更復雜的依賴,類似A->B->C->D。
對於這類依賴有兩種解決方式,如果依賴的功能沒有業務特性或只是簡單封裝系統 API,那可以考慮直接copy一份;如果依賴的代碼是多個功能模塊公用的或者多個功能模塊需要保持一致,可以考慮將該功能代碼抽取下沉到下一層代碼倉。
4)一時難以解決的依賴。
可以先暫時注釋掉,保證可以正常運行,后續理清邏輯再決定是進行解耦還是重構。斬斷依賴鏈非常重要,否則可能堅持不下去。
6.5 代碼解耦
下面介紹一下常用的代碼解耦方法:
公共代碼抽取下沉
比如:基礎組件(eg.網絡請求框架)、各模塊需要保持功能一致的代碼(eg.適配OS的動效);
簡單代碼復制一份
比如簡單封裝系統api(eg.獲取packageName)、功能模塊自用的自定義view(eg.提示彈窗);
三個工具
Arouter路由、Arouter服務、EventBus,能滿足各種解耦場景。
6.6 新老代碼共存
老項目模塊化是一個長期的過程,新老代碼共存也是一個長期的過程。經過上面改造后,一個功能模塊就可以獨立出來了,因為我們都是從老的App工程里拆分出來的,所以App工程依賴新倉后就可以正常運行。當我們持續從老工程中拆分出獨立模塊,最后老工程只需要保留一些入口功能,作為集成子模塊的主工程。
七、總結
本文從模塊化的概念、模塊化架構設計以及老項目如何實施模塊化改造等幾個方面介紹移動應用客戶端模塊化實踐。當然模塊化工作遠不止這些,還包括模塊aar管理、持續集成、測試、模塊化代碼管理、版本迭代流程等,本文就不一一贅述,希望這篇文章能給准備做模塊化開發的項目提供幫助。
作者:vivo互聯網客戶端團隊-Wang Zhenyu
