編譯時(shí)間越來(lái)越長(zhǎng),時(shí)間=生命琼腔,我要救命。
項(xiàng)目框架
最開(kāi)始項(xiàng)目只有一個(gè)app踱葛,項(xiàng)目結(jié)構(gòu)很簡(jiǎn)單丹莲,就是一個(gè)業(yè)務(wù)module加上一個(gè)通用的基礎(chǔ)庫(kù)。
隨著業(yè)務(wù)的開(kāi)展尸诽,有了第二個(gè)第三個(gè)乃至第N個(gè)app甥材,項(xiàng)目結(jié)構(gòu)變成如下樣子。
不同app有公共的功能性含,于是增加一個(gè)業(yè)務(wù)基礎(chǔ)庫(kù)洲赵,將公用部分移到里面。
這個(gè)框架維持了很長(zhǎng)一段時(shí)間商蕴,隨著業(yè)務(wù)的快速發(fā)展叠萍,開(kāi)發(fā)人員的增加,代碼也越來(lái)越臃腫绪商,一些問(wèn)題開(kāi)始出現(xiàn)苛谷。正好由于多個(gè)app不利于推廣,項(xiàng)目開(kāi)始向云平臺(tái)的方向發(fā)展格郁,需要一個(gè)融合的平臺(tái)app腹殿,將原先的app作為業(yè)務(wù)模塊加入到平臺(tái)app中。
問(wèn)題在哪里
舊框架最大問(wèn)題是業(yè)務(wù)基礎(chǔ)庫(kù)在膨脹例书,任意兩個(gè)app需要用到的公用功能锣尉,只能將代碼移入業(yè)務(wù)基礎(chǔ)庫(kù)。長(zhǎng)時(shí)間后决采,這個(gè)庫(kù)無(wú)法看了自沧,里面什么都有,所有app都直接引用树瞭,耦合嚴(yán)重拇厢,簡(jiǎn)單歸納幾個(gè)問(wèn)題:
- 業(yè)務(wù)基礎(chǔ)庫(kù)只會(huì)越來(lái)越大筏勒,功能簡(jiǎn)單地用文件夾區(qū)分,沒(méi)人可以完全掌握旺嬉;
- 代碼只增不減,編譯時(shí)間越來(lái)越長(zhǎng)厨埋;
- 修改一個(gè)功能邪媳,不得不測(cè)試調(diào)用到這個(gè)功能的每個(gè)app,測(cè)試成本高荡陷;
- 多名開(kāi)發(fā)人員對(duì)業(yè)務(wù)基礎(chǔ)庫(kù)進(jìn)行修改雨效,帶來(lái)較多代碼沖突,溝通成本高废赞;
- 直接引用代碼徽龟,缺少接口化和封裝,業(yè)務(wù)迭代不夠靈活唉地。
一句話据悔,整個(gè)工程要拆。
組件化實(shí)踐
今時(shí)今日搜到的就是組件化和插件化兩種耘沼,兩者的討論分析很多极颓,插件化最大的好處是具備動(dòng)態(tài)修改代碼的能力。如果不需要這種動(dòng)態(tài)功能群嗤,建議不要考慮插件化菠隆。官方不推薦的東西,沒(méi)必要蹚渾水狂秘,支持插件化的庫(kù)骇径,都是國(guó)產(chǎn)廠商。
今次組件化的改造者春,最大目標(biāo)是減少代碼間的依賴破衔,讓各業(yè)務(wù)模塊相對(duì)獨(dú)立,原有業(yè)務(wù)app的開(kāi)發(fā)人員可以更加專注于自己的部分钱烟,不需要次次全工程編譯运敢。
拆拆拆,最后拆成這個(gè)樣子:
- 基礎(chǔ)庫(kù)是業(yè)務(wù)無(wú)關(guān)的忠售,可以應(yīng)用到任意項(xiàng)目里传惠;
- 業(yè)務(wù)基礎(chǔ)庫(kù)有個(gè)基礎(chǔ)的base module,引用基礎(chǔ)庫(kù)稻扬。然后base module下卦方,根據(jù)功能劃分幾個(gè)功能module。下一層根據(jù)自身需要泰佳,選擇性包含盼砍;
- 業(yè)務(wù)app層是組件化主要改進(jìn)的地方尘吗,后面分析;
- 平臺(tái)app里只有一個(gè)mainapp module浇坐,這是一個(gè)殼工程睬捶,沒(méi)有任何業(yè)務(wù)代碼,是最終業(yè)務(wù)app集成的載體近刘。
1擒贸、application和library
能夠獨(dú)立運(yùn)行的app,module的屬性是application觉渴,在build.gradle定義為:
apply plugin: "com.android.application"
不能獨(dú)立運(yùn)行介劫,提供其他module依賴的叫l(wèi)ibrary,在build.gradle定義為:
apply plugin: 'com.android.library'
看回上面的結(jié)構(gòu)圖案淋,基礎(chǔ)庫(kù)和業(yè)務(wù)基礎(chǔ)庫(kù)自然都是library座韵,mainapp是application。對(duì)于業(yè)務(wù)app踢京,則要區(qū)分開(kāi)發(fā)階段和集成階段誉碴。在開(kāi)發(fā)階段,希望業(yè)務(wù)app可以單獨(dú)運(yùn)行瓣距;集成階段翔烁,希望業(yè)務(wù)app搖身一變,以library形式整合到平臺(tái)app旨涝。
開(kāi)發(fā)階段和集成階段的切換蹬屹,可以通過(guò)在gradle定義全局變量,提供給module讀取白华。我定義了一個(gè)config.gradle慨默,在根build.gradle引入:
apply from: "config.gradle"
config.gradle的用途是管理配置、版本號(hào)和依賴庫(kù)弧腥,避免散落到各個(gè)module中厦取,方便集中管理。
ext {
buildBizApp = false //是否構(gòu)建單獨(dú)的業(yè)務(wù)app
compileSdkVersion = 24
buildToolsVersion = "26.0.1"
minSdkVersion = 15
targetSdkVersion = 19
versionCode = 1
versionName = "1.0.0"
dependencies = [
supportV4 : 'com.android.support:support-v4:24.2.1',
appcompatV7 : 'com.android.support:appcompat-v7:24.2.1',
recyclerviewV7 : 'com.android.support:recyclerview-v7:24.2.1',
design : 'com.android.support:design:24.2.1'
]
}
在業(yè)務(wù)app的build.gradle管搪,通過(guò)判斷變量buildBizApp虾攻,達(dá)到自由切換的目的。
if (rootProject.ext.buildBizApp) {
apply plugin: "com.android.application"
} else {
apply plugin: 'com.android.library'
}
2更鲁、photo module
下面以拍照和看圖的一個(gè)module為例子霎箍,它提供CameraActivity和PhotoActivity兩個(gè)Activity。這是很基礎(chǔ)的功能澡为,如果是組件化之前的框架漂坏,我會(huì)毫不猶豫將功能扔進(jìn)業(yè)務(wù)基礎(chǔ)庫(kù),因?yàn)樗衋pp都需要用到。
module之間的直接引用用起來(lái)很方便顶别,但不利于長(zhǎng)遠(yuǎn)代碼的維護(hù)谷徙,畢竟只靠著包名劃分功能,代碼邊界很容易破壞驯绎。最好的方法是編譯上的隔離完慧,調(diào)用者和photo module之間沒(méi)有直接引用。
利用application和library切換的方法剩失,我們可以達(dá)到如下效果屈尼。
對(duì)于photo module,除了CameraActivity和PhotoActivity赴叹,還添加了DebugMainActivity。當(dāng)photo module單獨(dú)編譯成photo app時(shí)指蚜,以DebugMainActivity為主頁(yè)乞巧。當(dāng)需要編譯mainapp時(shí),photo module作為library和其他module打包進(jìn)mainapp摊鸡,注意绽媒,這個(gè)時(shí)候DebugMainActivity沒(méi)有用了,只有橙色框部分需要免猾。紅色箭頭跳轉(zhuǎn)不能再使用顯式Intent是辕,可以用隱式Intent跳轉(zhuǎn),或者使用后面介紹的路由跳轉(zhuǎn)猎提。
好處顯而易見(jiàn)获三,模塊間完全解耦了。在開(kāi)發(fā)階段锨苏,可以單獨(dú)編譯photo app疙教,在DebugMainActivity中調(diào)試拍照和看圖,省掉編譯完整app和在app中點(diǎn)擊測(cè)試的時(shí)間伞租。
上面模式的實(shí)現(xiàn)贞谓,需要對(duì)配置進(jìn)行改造,接下來(lái)一步步來(lái)講解葵诈。
3裸弦、AndroidManifest合并
每一個(gè)module都有AndroidManifest.xml,很明顯作喘,當(dāng)module分別處于application和library時(shí)理疙,它需要的AndroidManifest.xml是不同的。
- application:定義CameraActivity泞坦、PhotoActivity沪斟、DebugMainActivity,其中DebugMainActivity定義為啟動(dòng)頁(yè)。
- library:只需要定義CameraActivity主之、PhotoActivity择吊,最終合并到mainapp的AndroidManifest.xml,描述了photo module提供了什么頁(yè)面槽奕。
sourceSets {
main {
if (rootProject.ext.buildBizApp) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//移除debug資源
java {
exclude 'debug/**'
}
}
}
}
AndroidManifest.xml的內(nèi)容無(wú)什么特別几睛,就不貼了,然后配置build.gradle粤攒,區(qū)分開(kāi)發(fā)模式和集成模式對(duì)應(yīng)AndroidManifest.xml文件位置所森。對(duì)于類似DebugMainActivity正式發(fā)布不需要的測(cè)試文件,可以放入java/debug文件夾夯接,然后在集成模式排除焕济。
4、Application處理
類似于AndroidManifest.xml盔几,最終運(yùn)行時(shí)Application只有一個(gè)晴弃,Application類也要區(qū)分開(kāi)發(fā)模式和集成模式。
- 開(kāi)發(fā)模式:photo module有自己的Application逊拍,就叫PhotoApplication上鞠,里面初始化第三方庫(kù)或者添加其他一些操作。對(duì)應(yīng)地PhotoApplication需要定義到debug/AndroidManifest.xml芯丧,PhotoApplication文件放進(jìn)java/debug中芍阎。
- 集成模式:PhotoApplication是photo module單獨(dú)編譯時(shí)才有用的。集成后缨恒,需要在mainapp中定義MainApplication作為最終唯一的Application谴咸。
很容易想到,這個(gè)時(shí)候還需要一個(gè)BaseApplication骗露,作為PhotoApplication和MainApplication的父類寿冕,提供公有的初始化方法和全局Context的獲取。
5椒袍、路由跳轉(zhuǎn)
由于模塊的拆分驼唱,頁(yè)面間無(wú)法使用顯式Intent跳轉(zhuǎn)。隱式Intent可以用驹暑,但是書(shū)寫(xiě)比較麻煩玫恳,一些面向切面的功能難以實(shí)現(xiàn),所以我不用优俘。
我使用了支持路由功能的這個(gè)庫(kù)alibaba/ARouter京办。項(xiàng)目有詳細(xì)的文檔和demo,我就不復(fù)制粘貼了帆焕,下面說(shuō)說(shuō)我怎樣用惭婿。
首先為CameraActivity定義地址不恭,直接對(duì)class添加注解:
@Route(path = "/photo/activity/camera")
然后在需要調(diào)用的地方這樣寫(xiě):
Bundle bundle = new Bundle();
//set bundle
ARouter.getInstance()
.build("/photo/activity/camera")
.with(bundle)
.navigation(activity, BizConstant.RequestCode.TAKE_PHOTO);
定義好路徑、參數(shù)和requestCode财饥,很簡(jiǎn)單地實(shí)現(xiàn)了一次路由跳轉(zhuǎn)换吧。
跳轉(zhuǎn)一定成功嗎?如果在開(kāi)發(fā)階段單獨(dú)編譯一個(gè)業(yè)務(wù)app钥星,photo module不存在沾瓦,前置登錄的login module也不存在,如下圖所示:
photo module不存在比較好辦谦炒,Arouter提供了一個(gè)Callback函數(shù):
public abstract class NavCallback implements NavigationCallback {
public NavCallback() {
}
public void onFound(Postcard postcard) {
}
public void onLost(Postcard postcard) {
}
public abstract void onArrival(Postcard var1);
public void onInterrupt(Postcard postcard) {
}
}
- 跳轉(zhuǎn)失敗時(shí)贯莺,可以直接返回一些測(cè)試數(shù)據(jù),photo module應(yīng)該是他人維護(hù)的穩(wěn)定組件宁改,不需要在開(kāi)發(fā)階段浪費(fèi)點(diǎn)擊時(shí)間缕探。
- 如果需要測(cè)試調(diào)用photo module,只能啟用集成模式还蹲,不過(guò)可以手動(dòng)修改mainapp的配置爹耗,因?yàn)闃I(yè)務(wù)app層的module可以任意組合,只需要包括用到的秽誊,最大限度減少編譯時(shí)間鲸沮。
至于前置的login琳骡,那是必須得有锅论,要輸入賬號(hào)密碼好煩啊,而且login module在開(kāi)發(fā)階段我不想集成楣号,有什么辦法最易?
回想之前每個(gè)業(yè)務(wù)app層的module在開(kāi)發(fā)階段都有自己的Application,完全可以把模擬登陸過(guò)程放在里面炫狱。這是一個(gè)思路藻懒,寫(xiě)一次,受益幾個(gè)月视译。
6嬉荆、依賴注入
依賴注入大家應(yīng)該要很熟悉,這是一種很好的代碼解耦方式酷含,不了解的請(qǐng)自行學(xué)習(xí)鄙早。
Android有dagger這個(gè)出名的依賴注入框架,不過(guò)我沒(méi)有用椅亚,Arouter也帶了依賴注入功能限番,夠用了。
項(xiàng)目采用MVP模式呀舔,view和presenter是一一對(duì)應(yīng)弥虐,所以直接在Activity里new出Presenter對(duì)象,沒(méi)弄什么花樣。
Model層根據(jù)業(yè)務(wù)分為各種service霜瘪,比如TaskService珠插、UserService、SettingService粥庄,對(duì)外只暴露接口丧失。這個(gè)時(shí)候使用依賴注入就很合適,Presenter只需要持有service的引用惜互,實(shí)例由Arouter負(fù)責(zé)注入布讹。
類似Activity定義路徑,為service實(shí)現(xiàn)類定義注解:
@Route(path = "/test/service/task")
public class TaskServiceImpl implements TaskService {}
然后在Presenter训堆,用Autowired注解需要被注入的Service描验。例子里有兩種方式,一種是全局注入坑鱼,一種是單個(gè)注入膘流,根據(jù)實(shí)際情況使用。
@Autowired
TaskService taskService;
@Autowired
UserService userService;
public MyPresenter() {
ARouter.getInstance().inject(this);
//taskService = ARouter.getInstance().navigation(PollingTaskService.class);
//userService = ARouter.getInstance().navigation(UserService.class);
}
上面無(wú)非是省略了new的過(guò)程鲁沥,下面再舉個(gè)復(fù)雜一點(diǎn)的例子呼股。
select user展示了user列表,提供選擇user的功能画恰,但是user列表的生成方法彭谁,只有對(duì)應(yīng)的caller知道。在caller和select user已經(jīng)組件化的情況下允扇,可以使用依賴注入簡(jiǎn)化代碼缠局。
首先,在它們倆共同的業(yè)務(wù)庫(kù)層定義一個(gè)接口BaseSelectUserService考润,里面有一個(gè)方法listUser()狭园。caller1和caller2分別實(shí)現(xiàn)BaseSelectUserService接口,完成各自listUser()的邏輯糊治。
BaseSelectUserService selectUserService =
(BaseSelectUserService) ARouter.getInstance().build(servicePath).navigation();
最關(guān)鍵是為select user注入合適的SelectUserServiceImpl對(duì)象唱矛,其中servicePath是頁(yè)面跳轉(zhuǎn)傳遞過(guò)來(lái)的參數(shù),這樣就可以正確地調(diào)用到對(duì)應(yīng)caller的listUser()井辜。如果有第三個(gè)caller绎谦,完全不用管select user,只需要依葫蘆畫(huà)瓢實(shí)現(xiàn)BaseSelectUserService接口并傳遞service路徑抑胎。
7燥滑、數(shù)據(jù)庫(kù)
項(xiàng)目的數(shù)據(jù)庫(kù)使用sqlite,orm框架是greenDAO阿逃。組件化之前铭拧,各業(yè)務(wù)app維護(hù)自己的數(shù)據(jù)庫(kù)赃蛛,集成后就要考慮各數(shù)據(jù)庫(kù)之間的關(guān)系。
數(shù)據(jù)庫(kù)表分為兩類搀菩,一是公共的表呕臂,比如用戶表、資源表肪跋;二是各業(yè)務(wù)app自身的業(yè)務(wù)表歧蒋。組件化之后,有三種方向:
- 業(yè)務(wù)app依舊各自維護(hù)數(shù)據(jù)庫(kù)州既;
- 提取公共表到上層的業(yè)務(wù)基礎(chǔ)層谜洽,統(tǒng)一管理;
- 所有表放在業(yè)務(wù)基礎(chǔ)層吴叶。
第二種是在模塊劃分上是理想的阐虚,公共表在業(yè)務(wù)基礎(chǔ)層,業(yè)務(wù)表維護(hù)在各自業(yè)務(wù)app中蚌卤,合情合理实束,不過(guò)有攔路虎。greenDAO通過(guò)@Entity將對(duì)象定義為表逊彭,在編譯時(shí)咸灿,不同module中的表會(huì)分別生成DaoMaster和DaoSession,換言之侮叮,每個(gè)module都有一個(gè)數(shù)據(jù)庫(kù)避矢。跨數(shù)據(jù)庫(kù)的多表查詢難搞签赃,不用第二種了谷异。
第一種改動(dòng)最少分尸,但是由于公共表不是定義在業(yè)務(wù)基礎(chǔ)庫(kù)锦聊,所有公共表的邏輯都需要在業(yè)務(wù)app中實(shí)現(xiàn)一遍,我不能接受咯箩绍。
剩下第三種孔庭,雖然所有業(yè)務(wù)表需要定義在業(yè)務(wù)基礎(chǔ)庫(kù),感覺(jué)不太好材蛛,但也僅僅是表定義圆到,增刪改查的邏輯還是在業(yè)務(wù)app中,在不修改數(shù)據(jù)庫(kù)品種的情況下卑吭,不好也先用著芽淡。
8、資源沖突
多個(gè)module由多名開(kāi)發(fā)人員并行開(kāi)發(fā)豆赏,無(wú)可避免會(huì)出現(xiàn)資源名稱的重復(fù)挣菲。在最終合并到mainapp時(shí)富稻,肯定會(huì)出現(xiàn)沖突,最好的方法是為資源定義一個(gè)前綴白胀。例如photo module中所有資源都加個(gè)前綴photo_椭赋。
build.gradle可以增加一個(gè)配置,強(qiáng)制資源指定前綴或杠。
android{
resourcePrefix "photo_"
}
這個(gè)配置只能限制xml文件里的資源哪怔,對(duì)于圖片資源,養(yǎng)成習(xí)慣添加吧向抢。
結(jié)束語(yǔ)
上面做了很多工作认境,但還是處于組件化的“初級(jí)階段”。組件服務(wù)暴露挟鸠、代碼徹底隔離元暴、組件生命周期、組件通信兄猩、組件測(cè)試等還有一大堆可以改進(jìn)的方向茉盏,后續(xù)會(huì)一一實(shí)踐。
多謝很多網(wǎng)上大神的努力和無(wú)私分享枢冤,獲益良多凯旋。遇到疑問(wèn)或者有更好的方法,歡迎交流油湖。