忙了一個多月旋廷,一直沒時間寫文章。終于把項目重構(gòu)完了狈网,借此機(jī)會淺談一下對Android架構(gòu)的見解宙搬。筆者將會把重構(gòu)分為三個部分講解。
本文為全局架構(gòu)拓哺,主要設(shè)計模塊化架構(gòu)開發(fā)勇垛。
上一篇為概述篇
下一篇為組件化+MVP篇
[如有解釋錯誤的地方,歡迎評論區(qū)指正探討]
模塊化能解決什么問題
先來看一下筆者項目的舊版架構(gòu):
是不是很眼熟這樣的架構(gòu)士鸥?整個應(yīng)用即為一個工程闲孤,所有業(yè)務(wù)之間不存在編譯隔離,所以可以互相引用烤礁。對于早期小型的App而言讼积,這樣的架構(gòu)清晰簡單,同時也便于快速開發(fā)脚仔。
不過隨著業(yè)務(wù)的積攢勤众,整個App變得臃腫,這樣的架構(gòu)不僅容易出現(xiàn)模塊耦合問題鲤脏,同時容易造成開發(fā)混亂们颜,改一處地方卻涉及到多個模塊吕朵。
在上一篇文章中,筆者也有提到為什么需要重構(gòu)窥突,并提出使用模塊化進(jìn)行重構(gòu)努溃,那么我們來看看,使用模塊化能解決什么問題:
- 解決由于模塊邊界定義不清而導(dǎo)致的耦合問題
- 統(tǒng)一規(guī)定模塊之間通信方式波岛,去除過分使用EventBus而臃腫的event包
- 隔離各個模塊代碼茅坛,利于并行開發(fā)測試
- 可單獨(dú)編譯打包某一模塊,提升開發(fā)效率
- 模塊實現(xiàn)可復(fù)用则拷,快速集成影子App
- 開發(fā)時,可以進(jìn)行單業(yè)務(wù)編譯曹鸠,避免全量編譯耗時過長
這些問題都是從筆者的項目中反應(yīng)出來的煌茬,也正是解決代碼劣化的關(guān)鍵。
什么是模塊化
講了那么久模塊化彻桃,那么到底什么是模塊化坛善?網(wǎng)上對于模塊化的解釋有很多,基本上每個人的解釋都不太一樣邻眷,往往模塊化和組件化總被混淆在一起眠屎。
這大概是因為組件和模塊在英文翻譯里都被叫為module
,而在AS中lib模塊
都被定義為module
肆饶。所以這些module
都容易被混淆在一起改衩。
組件
這里提到的組件,翻譯成module
并不準(zhǔn)確驯镊,他其實是一個通用的Lib
葫督,只不過組件在AS中的實現(xiàn),多數(shù)以module
的形式實現(xiàn)板惑。在Android App中橄镜,組件應(yīng)該是構(gòu)成業(yè)務(wù)模塊或業(yè)務(wù)功能的基本單位。
舉個例子冯乘,筆者項目中存在類似朋友圈一樣的業(yè)務(wù)洽胶,那么必不可少的就需要一個圖片上傳組件Uploader
。
這里的Uploader
不管是功能上還是業(yè)務(wù)上都無法繼續(xù)拆分裆馒,所以Uploader
組件而并非Uploader
模塊姊氓,朋友圈才可以稱之為模塊。
對于組件化领追,其實也是本次重構(gòu)方案的關(guān)鍵之一他膳,不過筆者將其歸為局部架構(gòu)里的內(nèi)容,所以在這里只簡單介紹一下概念绒窑,不展開過多描述棕孙。
模塊
對于模塊,這才是真正意義上的module
。模塊由多個組件甚至多個模塊構(gòu)成蟀俊,并通過特定的邏輯講這些組件連接起來實現(xiàn)一定的業(yè)務(wù)钦铺。
還是以剛才的朋友圈為例子,朋友圈將網(wǎng)絡(luò)組件肢预,上傳組件矛洞,日志組件,圖片組件通過特定的邏輯構(gòu)成其特定的業(yè)務(wù)烫映。對于微信朋友圈沼本,其內(nèi)部可能還有他特有的廣告模塊,Gps模塊等等锭沟,所以說模塊也可能由多個小模塊構(gòu)成抽兆。
模塊具有可拆分性,正如朋友圈族淮,我們可以將其拆分成多個組件辫红。
模塊一般與業(yè)務(wù)相關(guān)聯(lián)。
一個健康的模塊應(yīng)該具有可復(fù)用性祝辣,要做這點(diǎn)贴妻,必然要和其他模塊保持獨(dú)立。
聽上去可服用性和業(yè)務(wù)相關(guān)聯(lián)蝙斜,似乎互相矛盾名惩,其實不然,如果這里的可復(fù)用性沒有組件的復(fù)用性那么強(qiáng)乍炉,強(qiáng)調(diào)的是與其他模塊保持獨(dú)立绢片,假如兩個app都有朋友圈業(yè)務(wù),那么大可以復(fù)用該模塊岛琼,改改ui即可底循。
區(qū)別
通過上面的介紹,其實也就大致了解了什么模塊槐瑞,什么是組件熙涤。
簡而言之: 模塊 = 組件A + 組件B + …… 組件B
其實歸根結(jié)底,只要目的確定困檩,把臃腫的工程祠挫,拆分為更小的部分,解耦各種復(fù)雜的邏輯悼沿,便于代碼管理等舔。管他叫什么模塊還是什么。
技術(shù)難點(diǎn)
為了實現(xiàn)模塊化糟趾,并使各個模塊達(dá)成上述特性慌植,筆者將整個過程劃分為三個問題甚牲,也是三個技術(shù)難點(diǎn)。
- 隔離模塊邊界
- 模塊間的跳轉(zhuǎn)
- 模塊間的通信
接下來蝶柿,將一一解答這些問題丈钙。
隔離模塊邊界
對于以前的App而言,為了避免耦合問題交汤,采取以包為分界雏赦,同時筆者的團(tuán)隊制定了一系列代碼規(guī)范,然而芙扎,在趕工的情況下星岗,并不是所有人都能遵守這套規(guī)范,尤其是剛進(jìn)來并不熟悉團(tuán)隊的新伙伴戒洼。因此伍茄,要想從根本上隔離代碼,解決耦合問題施逾,在編譯上約束權(quán)限是最佳的方法。
那么如何做到編譯時的約束呢例获?很顯然汉额,這就需要將原本以包為分界的模塊抽出來以AS中的module
形式隔離。
同時制定規(guī)則榨汤,模塊與模塊直接不允許同時直接產(chǎn)生依賴關(guān)系蠕搜。
對于多個模塊通用的組件,應(yīng)該采取先前提及的組件化收壕,同樣以module
的形式隔離妓灌。這一塊將在下一篇文章中敘述。
規(guī)則
對于模塊的劃分蜜宪,需要制定一定的規(guī)則虫埂,如果劃分粒度過小,那么會導(dǎo)致項目Module冗余圃验,如果粒度過大掉伏,那么又會出現(xiàn)耦合問題,與初衷相悖澳窑。錯誤的劃分斧散,將導(dǎo)致項目結(jié)構(gòu)復(fù)雜。
因此摊聋,對于筆者的項目而言鸡捐,指定這幾個規(guī)則來劃分:
- 業(yè)務(wù)之間是否強(qiáng)關(guān)聯(lián)?強(qiáng)關(guān)聯(lián)應(yīng)該合并
- 共用的功能是否可組件化麻裁?可組件化應(yīng)該拆分
- 業(yè)務(wù)是否復(fù)雜箍镜?復(fù)雜應(yīng)該拆分
- 空殼模塊能否與其他空殼合并
舉個例子可能比較好懂源祈,以微信為例,在首頁底部有四個tab:
可能你會這么想鹿寨,底部四個tab就對應(yīng)四個大模塊新博。
如果這么劃分的話,那么又該如何處理朋友圈脚草,搖一搖等功能呢赫悄?都?xì)w于發(fā)現(xiàn)模塊還是單獨(dú)開一個模塊呢?
顯然馏慨,如果都將朋友圈和搖一搖都?xì)w為一個模塊埂淮,那么這個模塊將過度復(fù)雜,這兩者沒有明顯的業(yè)務(wù)關(guān)系写隶,歸于一個模塊倔撞,很容易因為跳轉(zhuǎn)或信息通信產(chǎn)生耦合問題。
如果單獨(dú)開一個模塊慕趴,那么顯然發(fā)現(xiàn)模塊將成為一個空殼痪蝇,而四個tab,就對應(yīng)了四個空殼冕房,這就造成了Module冗余躏啰。
那么應(yīng)該如何處理好呢?
針對筆者定制的規(guī)則耙册,我們一一考慮:
- 朋友圈與搖一搖之間業(yè)務(wù)并不是強(qiáng)關(guān)聯(lián)
- 這里并無復(fù)用功能
- 單純四個tab的業(yè)務(wù)并不復(fù)雜给僵。朋友圈與搖一搖業(yè)務(wù)復(fù)雜
- 四個tab其實都可以作為空殼模塊,僅作為承載體
綜合考慮详拙,我們應(yīng)該合并四個空殼tab帝际,拆分朋友圈與搖一搖。
所以項目結(jié)構(gòu)如下:
對于不同項目饶辙,實際情況可能比這里更復(fù)雜蹲诀,這就需要對業(yè)務(wù)足夠了解,具有一定經(jīng)驗了畸悬。目前筆者團(tuán)隊劃分模塊時需要各業(yè)務(wù)Leader商討決定侧甫。
隔離好各個模塊,就應(yīng)該來考慮模塊間跳轉(zhuǎn)蹋宦,通信的問題了披粟。雖然我們將不存在強(qiáng)關(guān)聯(lián)的模塊隔離開,但模塊之間終究需要通信與跳轉(zhuǎn)冷冗,這由應(yīng)該如何處理呢守屉?
模塊間跳轉(zhuǎn)
在我們隔離完模塊后,跳轉(zhuǎn)的問題也就出來了蒿辙。因為編譯隔離拇泛,我們也就無法直接引用滨巴,不能通過的顯示方式跳轉(zhuǎn)。
隱性跳轉(zhuǎn)
既然顯示跳轉(zhuǎn)不行俺叭,自然而然的我們就想到隱式跳轉(zhuǎn):
Intent intent=new Intent("action");
startActivity(intent);
不過使用隱式跳轉(zhuǎn)存在幾個問題:
- 每個模塊各自管理各自的
AndroidManifest.xml
恭取,這就容易出現(xiàn)action重復(fù)的問題。 - 過多的Activity被導(dǎo)出熄守,容易引發(fā)安全問題
-
可配置性較差蜈垮,
Manifest
限制于xml格式,書寫麻煩裕照,配置復(fù)雜攒发,可以自定義的東西也較少。 - 代碼寫起來繁瑣晋南,出錯時難以定位問題
- 直接通過Intent的方式跳轉(zhuǎn)惠猿,跳轉(zhuǎn)過程開發(fā)者無法干預(yù),一些面向切面的事情難以實施负间,比方說登錄偶妖、埋點(diǎn)這種非常通用的邏輯,在每個子頁面中判斷又很不合理政溃,畢竟activity已經(jīng)實例化了
顯然我們不可能采用難以管理的隱式跳轉(zhuǎn)餐屎。
路由跳轉(zhuǎn)
既然隱式跳轉(zhuǎn)不行,那我們只能另尋他法玩祟。
這里我們參考了路由器工作原理:
很顯然,我們需要在路由器中維護(hù)一個路由表屿聋,也就是Activity
與url
的映射空扎,在我們發(fā)出一個跳轉(zhuǎn)請求時,就由路由器去路由表中尋找映射并跳轉(zhuǎn)润讥。
這樣的操作就類似于我們在瀏覽器中輸入www.baidu.com
转锈,我們本地完全不與百度產(chǎn)生依賴關(guān)系,卻可以跳轉(zhuǎn)訪問百度楚殿。百度是如何實現(xiàn)撮慨,是好是壞,我們完全不懂擔(dān)心脆粥,這樣的流程很適合我們的實現(xiàn)模塊化砌溺。
那么如何實現(xiàn)呢?
顯然路由器的核心是維護(hù)路由表变隔,我們需要做的就是把每個Activity到路由器里规伐。從原理上來看并不難實現(xiàn),關(guān)鍵是如何做到好用易用匣缘。
這里筆者并沒有自己重復(fù)造輪子猖闪,而是選擇了阿里開源的框架ARouter鲜棠。ARouter處理實現(xiàn)基本的路由功能外,還兼?zhèn)?strong>攔截器培慌,降級策略等功能豁陆。
ARouter在實現(xiàn)維護(hù)路由表功能時,借助Annotation Processor
來實現(xiàn)吵护。這樣我們在使用時便十分方便盒音,也不會在代碼中插入生硬的邏輯。
簡單的看一下使用:
添加注解
// 在支持路由的頁面上添加注解
@Route(path = "/baidu/index")
public class BaiDuActivity extend Activity {
}
執(zhí)行跳轉(zhuǎn)
// 實現(xiàn)簡單的跳轉(zhuǎn)
ARouter.getInstance().build("/baidu/index").navigation();
是不是很簡單何址?這樣我們就仿造出了跳轉(zhuǎn)www.baidu.com
的操作了里逆。
簡單看看ARouter的工作流程甚颂,其實跟我們前面的講的原理差不多摔认,需要提一下的是,ARouter在使用注解處理器的同時還使用了反射圾另,經(jīng)過測試偎血,這里的反射很好的解決了模塊之間的耦合問題同時并不會出性能問題诸衔。
解決完跳轉(zhuǎn)問題,還有通信的問題要解決颇玷,比如朋友圈模塊需要使用用戶模塊的用戶信息笨农。那么又該如何解決呢?
模塊間通信
在模塊獨(dú)立之后帖渠,模塊之間沒辦法直接耦合谒亦,所以原先的通信方式(setListener,startActivityForResult)便失效了空郊。所以份招,模塊化的一個關(guān)鍵便是如何實現(xiàn)與其他模塊保持獨(dú)立,又建立良好的通信方式狞甚。
我們需要尋找一種新的方案锁摔。
廣播
作為四大組件之一,我們借助Broadcast
實現(xiàn)模塊之間的通訊哼审,不過我們也知道谐腰,廣播作為一個重量級的通訊工具,并不適合頻繁通信涩盾,同時廣播僅支持基本數(shù)據(jù)類型和可序列化對象十气,傳遞大數(shù)據(jù)時還有限制,可見局限性很大春霍,并不適用桦踊。
EventBus
作為一個輕量級的通訊框架, EventBus
解決了廣播存在的那些問題终畅,同時十分靈活籍胯,**不依賴于上下文v竟闪,任何地方都可以進(jìn)行通訊。重構(gòu)之前的項目也有很多地方利用EventBus
來進(jìn)行通訊杖狼,確實實現(xiàn)了松耦合
不過EventBus
也存在他的弊端:
- 大量的通訊Event沉淀在Common層
- 基于發(fā)布訂閱模式炼蛤,注定無法主動獲取數(shù)據(jù)
這些弊端,讓我們最后放棄使用EventBus
作為模塊之間的通訊工具蝶涩,不過同一模塊內(nèi)的通訊依舊可以選擇EventBus
協(xié)議通信
我們一開始參照了RPC機(jī)制理朋, 也就是通過restful這樣的形式去進(jìn)行通信。
通過訪問定制的協(xié)議绿聘,經(jīng)由路由器訪問相關(guān)的服務(wù)獲取數(shù)據(jù)嗽上,這種方式十分靈活,具備很強(qiáng)的解耦能力熄攘,但也有不可忽視的代價——高度文檔化兽愤。
想必大家都有體驗過,我們開發(fā)時總是需要去翻閱后臺給我們的接口文檔挪圾,這樣的事情我們不想在本地通信時再次發(fā)生浅萧,不僅維護(hù)文檔困難,開發(fā)效率低下哲思,也非常容易出錯洼畅。
我們希望協(xié)議的檢測能夠讓編譯器幫我們分擔(dān),寫錯了編譯器會報錯棚赔,然而協(xié)議通信是依賴于文檔的帝簇,eg:www.baidu.com/getsomethings/id=xxx&passw=xxx
,編譯器無法識別這樣的手寫是否符合協(xié)議靠益,需要運(yùn)行時才能發(fā)現(xiàn)錯誤己儒。
說了好幾種常見的通訊方式都不行,那到底應(yīng)該怎么做呢捆毫?
接口協(xié)議通信
既然協(xié)議通信不好用,那么有沒有辦法解決他高度依賴文檔問題冲甘。
方法是有的绩卤,就是改文檔化為接口化。如果將原本由文檔規(guī)定的協(xié)議江醇,交給接口來規(guī)定濒憋,那么編譯器就可以幫我們檢測協(xié)議是否正確了。
這也就是接口協(xié)議通信的原理陶夜。
簡單的看一下流程:
和上面提到的協(xié)議通信很相似凛驮,多了Provider
這一層次:
- ModuleB 向 Router 注冊 ProviderB 接口服務(wù)
- ModuleA 向 Router 請求 ProviderB 接口服務(wù)
- Router 返回 ProviderB 接口服務(wù)
這樣邊解決了原本的高度文檔化的問題,同時保持原來的靈活性和解耦能力条辟。
剛好ARouter具備這樣的功能黔夭,于是我們也采用了ARouter的實現(xiàn)方案宏胯。使用起來是這樣的:
首先在公共組件(路由組件)中 聲明接口,其他組件通過接口來調(diào)用服務(wù)
public interface IProviderB extends IProvider {
String getUserName();
}
然后在具體模塊中實現(xiàn)接口,并注冊
@Route(path = "/moduleb/providerb", name = "測試服務(wù)")
public class ProviderB implements IProviderB {
@Override
String getUserName(){
}
}
在其他模塊中通過路由去尋找相關(guān)服務(wù)
IProviderB provider = (IPoviderB) ARouter.getInstance().build("/moduleb/providerb").navigation();
是不是同樣和很簡單本姥?而且因為都使用了ARouter肩袍,所以調(diào)用操作與跳轉(zhuǎn)的操作很像,也就便于代碼的閱讀婚惫。
至此氛赐,我們就解決了模塊化的三個關(guān)鍵性問題。
再思考
解決完上述的上的三個關(guān)鍵性問題后先舷,一個基于ARouter的模塊化架構(gòu)也就誕生了艰管,不過還存在一些問題。
app module
在我們隔離完業(yè)務(wù)模塊后蒋川,該如何處理app module
呢牲芋?
在上面的微信的例子中,我們將app module
作為home界面的載體尔破,裝載了主界面的幾個空殼街图。那么app module
就只做這樣的功能了嗎?
并不懒构,app module
作為特殊的一個模塊餐济,鏈接著所以模塊的生命周期,也就包括了模塊的初始化與銷毀胆剧。
同時app module
作為一個中介絮姆,可以實現(xiàn)一些簡單的模塊間通訊。
缺點(diǎn)
那么是否實現(xiàn)模塊化之后就高枕無憂了呢秩霍?并不篙悯。
模塊化很好的解決了模塊之間的耦合問題,同時便于進(jìn)行單業(yè)務(wù)拆分與編譯铃绒。但是也暴露幾個問題:
因為模塊數(shù)量的增加鸽照,全量編譯時間變長
雖然我們平時開發(fā)時做到了單業(yè)務(wù)編譯,加快了編譯速度颠悬,但是最終打包合并的時候需要全量編譯矮燎,事實證明全量編譯的時間將隨著模塊數(shù)量的增加而增加。不過赔癌,這點(diǎn)還能接受诞外。模塊的劃分有時糾結(jié)不清
當(dāng)對模塊進(jìn)行解耦時,即便大體上的業(yè)務(wù)劃分已經(jīng)清晰灾票,但因為業(yè)務(wù)間各種微妙的關(guān)系峡谊,細(xì)節(jié)上仍會遇到糾纏不清的情況。那么這個時候就會出現(xiàn)糾結(jié)于這個模塊到底該不該細(xì)分的問題。
我們能做到的只是盡量讓他更加"面向?qū)ο?既们,同時避免隨意拼湊和單純?yōu)榱祟愋徒怦疃怦畹那闆r濒析。-
模塊劃分粒度容易過細(xì),導(dǎo)致模塊數(shù)劇增
這是筆者項目中實際遇到的問題贤壁,對于部分業(yè)務(wù)悼枢,功能比較零散,如果劃分多一個模塊或組件脾拆,這個模塊或組件又只有這個業(yè)務(wù)在使用馒索。如果不劃分,又容易與這個業(yè)務(wù)里的其他功能耦合名船。
引用微信模塊化的例子绰上,對于Gallery
模塊,內(nèi)部還有存儲渠驼,編輯等小功能蜈块,如果直接與Gallery
揉合,那么很容易就產(chǎn)生耦合問題迷扇,為此微信團(tuán)隊提出了自己的解決方案百揭,構(gòu)建pins工程:
這一塊筆者就不再闡述,可以跳轉(zhuǎn)微信的文章進(jìn)行學(xué)習(xí)蜓席。
總結(jié)
對于中大型App而言器一,往往都積累了一些年份,很多時候厨内,全局架構(gòu)都停留最初的狀態(tài)祈秕,各個業(yè)務(wù)相互交叉耦合,這樣其實并不利于整個App的發(fā)展雏胃。代碼只會逐漸劣化请毛,到最后發(fā)現(xiàn)拓展新業(yè)務(wù)時,需要大規(guī)模修改舊業(yè)務(wù)瞭亮,那就為時已晚了方仿。
所以,一個良好的項目周期统翩,需要適時推動一些重構(gòu)計劃仙蚜,提高代碼質(zhì)量,而并不是只停留在業(yè)務(wù)代碼層次唆缴。
看一下采用模塊化之后的項目架構(gòu),對比一下文章開頭的架構(gòu):
模塊化的架構(gòu)不僅解決了模塊耦合問題黍翎,同時也調(diào)高了整個App的拓展性與維護(hù)性面徽。這樣的重構(gòu),何樂而不為?
最后希望筆者分享的一點(diǎn)經(jīng)驗?zāi)軐Υ蠹姨岣叽a有些幫助趟紊,如有錯誤的地方氮双,歡迎指正探討。