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

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


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

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

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

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



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

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

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

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

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

技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。
推薦閱讀:
開通專輯 | 細數(shù)那些年寫過的技術(shù)文章專輯
百萬高薪,十萬獎金!網(wǎng)易應(yīng)用創(chuàng)新開發(fā)者大賽正式開賽!
覺得不錯,點個在看唄~

