一個軟件系統(tǒng)的開發(fā)可能只需要2到3個月就能完成,而這個系統(tǒng)的迭代和維護時間可能達2到3年之久——《不記得哪本書上說的》
Android移動端項目經(jīng)過長時間的迭代和維護,代碼經(jīng)手不同的人,產(chǎn)生冗余和規(guī)范問題也是一件不可避免的事情,而單工程的架構(gòu)在面對這樣的情況更是心有余而力不足,組件化也正是針對此類場景衍生出來的技術(shù)手段
特點
組件化最明顯的兩個優(yōu)勢:代碼解耦和并行開發(fā)。通過不同維度和應(yīng)用環(huán)境下進行不同程度的拆分,達到組件靈活配置,增加開發(fā)效率的目的。 所以細化來說,組件化就是根據(jù)功能和業(yè)務(wù)來拆分module,最后module組成模塊,而后模塊組裝成應(yīng)用。

應(yīng)用入口:該層其實只是一個空殼,用來存放啟動頁面,同時依賴所有業(yè)務(wù)組件,從這里開始,便是全量應(yīng)用的起始位置。
應(yīng)用業(yè)務(wù)組件:根據(jù)不同業(yè)務(wù)橫向拆分出來的業(yè)務(wù)組件,任何一個業(yè)務(wù)組件都可以獨立出來成為一個應(yīng)用存在,有完整的應(yīng)用周期,這些組件是橫向拆分出來的,所以他們之間并不存在依賴關(guān)系。
通用業(yè)務(wù)功能組件:通用業(yè)務(wù)其實相當(dāng)用這類業(yè)務(wù)是從應(yīng)用業(yè)務(wù)中抽取出來的并集,從應(yīng)用上說,他屬于業(yè)務(wù),而針對應(yīng)用業(yè)務(wù)而言則更像是一種功能,好比登錄這種業(yè)務(wù)功能,不需要關(guān)心有沒有界面,當(dāng)中是怎樣的邏輯,只需要提供結(jié)果即可,如有必要,中間的過程也可以通過回調(diào)的方式向外部暴露出來。
支撐服務(wù)組件接口:通用業(yè)務(wù)組件對外聲明的功能接口抽取放到該層,可以說通用業(yè)務(wù)功能組件就是這層接口的實現(xiàn)者,同時考慮到應(yīng)用業(yè)務(wù)通用代碼的抽取,帶業(yè)務(wù)屬性的Base也可以放到該層。該層的接口抽取也可以理解這里是Module依賴的一種妥協(xié),當(dāng)同樣層級的兩個通用功能組件需要互相通信的時候,如果沒有抽取一層公共的接口層出來,就會造成你中有我我中有你的尷尬局面,破壞了組件化解耦項目的初衷。
功能支撐:項目中用到的諸如網(wǎng)絡(luò)請求,圖片加載,依賴的第三方和XXUtils等通用純Feature代碼就可以放到這里,是以上每層代碼的功能支撐,尤為重要,同時對部分通用View的封裝也可以放到這里供上層使用。
每層的橫向拆分模塊之間是沒有任何的依賴關(guān)系的,如果要做到相互之間的通信或是頁面的跳轉(zhuǎn),那么就需要提供一個路由來作為組件間通信的橋梁。
綜上所述,一個簡單的組件化工程結(jié)構(gòu)就出來了,說白了就是多Module開發(fā)而已

代碼解耦
代碼解耦主要是從兩個方面,其一是公共代碼的抽取和歸納,其二是面向接口編程,接口下沉。
譬如說日期序列化方法,獲取字符串中的年月日,對外獲取日期字符串和format格式即可,??吹降腦XUtils便是常用的代碼抽取方式,上層只關(guān)心結(jié)果,不關(guān)心具體的實現(xiàn)邏輯這是一種簡單的減少代碼冗余方式。
public static Date serializeDate(String dateString, String dateFormat);
而代碼解耦其實與這種方式類似,只不過所調(diào)用的靜態(tài)方法變成了一個接口
public interface DateSerializable{
void serializeDate(String dateString, String dateFormat);
}
上層去持有這個接口,而不是一個具體的類,通過實例化對應(yīng)的實現(xiàn)來獲得具有能力的實例來進行業(yè)務(wù),那么當(dāng)?shù)讓影l(fā)生了改變或是實現(xiàn)的時候,上層只需要實例化對應(yīng)的新實現(xiàn)類即可,如果把這層實例化也作為接口去作,那么上層完全不用改變就能擁抱變化,遵循設(shè)計原則去設(shè)計具體的應(yīng)用和功能代碼
依賴注入
繼續(xù)深入之后就是面向接口編程,上層與下層之間的通信以及下層提供能力的媒介以接口的形式存在,這樣后期實現(xiàn)的改變可以更換不同的實現(xiàn)類即可,上層幾乎不需要做任何的改動。對于能夠提供通用能力與業(yè)務(wù)沒有直接關(guān)系的Feature接口應(yīng)盡量下沉到底層。
而橫向的業(yè)務(wù)代碼或者功能實現(xiàn)可以進行依賴注入的方式來達到解耦的目。
依賴注入簡單示例
類比程序員ICoder,提供一個coding的能力
private interface ICoder {
void coding();
}
而后提供一個工作者的類,由工作者來持有coding這項技能,如下:
private static class Worker{
private ICoder mCoder;
public Worker(ICoder coder){
mCoder = coder;
}
public void working(){
mCoder.coding();
}
}
顯而易見,工作者內(nèi)部持有一個ICoder這樣的一個對象,而這個對象則是通過構(gòu)造傳遞進來,這樣的方式便能稱之為依賴注入Di(Dependency injection)
完善接下來的代碼
private static class Javaer implements ICoder{
@Override
public void coding(){
System.out.println("i am java coder, code for java");
}
}
private static class Pythoner implements ICoder {
@Override
public void coding(){
System.out.println("i am python coder, code for python");
}
}
創(chuàng)建兩個實現(xiàn)coding能力的類,分為Java開發(fā)和Python開發(fā),現(xiàn)在就相當(dāng)于公司有兩個程序員,他們分別是Java和Python,當(dāng)來活的時候,可以甩鍋到這兩位同僚身上。
public static void main(){
Worker worker = new Worker(new Javaer());
worker.working();
}
可以看到工作者持有的能力由外部賦值進去,工作者內(nèi)部需要持有對這層工作能力的接口。
上面依賴注入的方式就是一個控制反轉(zhuǎn)Ioc(Inversion of control)的例子:由程序內(nèi)部定制規(guī)則,外部傳遞遵循了內(nèi)部規(guī)則的實現(xiàn)。
結(jié)構(gòu)解耦
結(jié)構(gòu)的解耦其實一般針對應(yīng)用的整體業(yè)務(wù)而言進行的一個"分Module"操作,根據(jù)業(yè)務(wù)類型的橫向拆分,業(yè)務(wù)屬性的縱向拆分以及功能SDK下沉。
古老的開發(fā)套路一般都會將Feature相關(guān)的代碼單獨作為一個Module進行依賴,然后上層的應(yīng)用和業(yè)務(wù)寫到APP中去。

這是最常見的操作,通過抽取常用的功能代碼,做上簡單的封裝,抽取到一個公共庫中,在編寫具體界面或者業(yè)務(wù)是可以直接拿來用。
而我們組件化的操作,則在此基礎(chǔ)上做一個更加細粒度的抽取,根據(jù)功能或是業(yè)務(wù)來進行橫向拆分和縱向排序。
橫向拆分
橫向就是對類別不同的模塊做一個拆分,對應(yīng)的就是業(yè)務(wù)類模塊,功能類模塊,而具體細化下來就負責(zé)業(yè)務(wù)的
Business類模塊:businessA、businessB...
基礎(chǔ)業(yè)務(wù)功能模塊:login、share、welcome...
支撐功能類模塊:baseUI、commonLib...
縱向排序
而縱向排序就是對橫向拆分出來的模塊做一個依賴關(guān)系的排序

技術(shù)細節(jié)
多Module工程
根據(jù)上述理論對工程進行拆分
根據(jù)上面的框架圖,大致把一個基礎(chǔ)應(yīng)用項目拆分出下列幾個Module

在此有一個小細節(jié),可以對基礎(chǔ)功能庫所依賴的三方做一個簡單的gradle管理
dependencies = [
"appcompatv7" : "com.android.support:appcompat-v7:${SUPPORT_LIB_VERSION}",
"constraintlayout" : "com.android.support.constraint:constraint-layout:${othersVersion.constraintlayout}",
"recyclerview" : "com.android.support:recyclerview-v7:${SUPPORT_LIB_VERSION}",
"cardview" : "com.android.support:cardview-v7:${SUPPORT_LIB_VERSION}",
"design" : "com.android.support:design:${SUPPORT_LIB_VERSION}",
"glide" : "com.github.bumptech.glide:glide:${othersVersion.glideVersion}",
"glideCompiler" : "com.github.bumptech.glide:compiler:${othersVersion.glideVersion}",
"rxjava" : "io.reactivex.rxjava2:rxjava:${othersVersion.rxjava2}",
"rxandroid" : "io.reactivex.rxjava2:rxandroid:${othersVersion.rxandroid}",
"litepal" : "org.litepal.android:core:${othersVersion.litepal}",
"retrofit" : "com.squareup.retrofit2:retrofit:${othersVersion.retrofit}",
"convertergson" : "com.squareup.retrofit2:converter-gson:${othersVersion.convertergson}",
"adapterrxjava2" : "com.squareup.retrofit2:adapter-rxjava2:${othersVersion.adapterrxjava2}",
"converterScalars" : "com.squareup.retrofit2:converter-scalars:${othersVersion.converterScalars}",
"okHttpLoggingInterceptor" : "com.squareup.okhttp3:logging-interceptor:${othersVersion.okHttpLoggingInterceptor}",
"eventbus" : "org.greenrobot:eventbus:${othersVersion.eventbus}",
"leakcanaryAndroid" : "com.squareup.leakcanary:leakcanary-android:${othersVersion.leakcanaryAndroid}",
"leakcanaryAndroidNoOp" : "com.squareup.leakcanary:leakcanary-android-no-op:${othersVersion.leakcanaryAndroidNoOp}",
"leakcanarySupportFragment": "com.squareup.leakcanary:leakcanary-support-fragment:${othersVersion.leakcanarySupportFragment}",
"arouterApi" : "com.alibaba:arouter-api:${othersVersion.arouterApi}",
"arouterCompiler" : "com.alibaba:arouter-compiler:${othersVersion.arouterCompiler}",
]
圖中對依賴的三方進行了一個簡單的依賴,后期也可寫一個函數(shù)來將依賴進行升級,重點不在寫法,在于抽取。
- 在根目錄下建立一個xx.gradle文件
- 編寫對應(yīng)的依賴常量代碼
- 在build.gradle中引用
- 在對應(yīng)的文件中使用
注意,如果想要在別的.gradle中使用聲明的這些常量,一定要在抽取的xx.gradle文件中將對應(yīng)的代碼塊用"ext"進行包裹
/**
* 三方依賴庫
*/
othersVersion = [
glideVersion : "4.8.0",
rxjavaVersion : "2.1.1",
rxandroidVersion : "2.0.1",
constraintlayout : "1.1.2",
rxjava2 : "2.1.1",
rxandroid : "2.0.1",
recyclerview : "27.1.1",
litepal : "2.0.0",
retrofit : "2.4.0",
converterScalars : "2.4.0",
okHttpLoggingInterceptor : "3.8.0",
convertergson : "2.4.0",
adapterrxjava2 : "2.4.0",
eventbus : "3.1.1",
leakcanaryAndroid : "1.6.1",
leakcanaryAndroidNoOp : "1.6.1",
leakcanarySupportFragment: "1.6.1",
multiDex : "1.0.3",
arouterApi : "1.4.1",
arouterCompiler : "1.2.2",
arouterRegister : "1.0.2",
buglyCrashReport : "latest.release",
buglyNativeCrashReport : "latest.release"
]
在工程根目錄的build.gradle中加上如下代碼:
apply from: "config.gradle"
這樣在Module對應(yīng)的編譯腳本文件中就能正常引用了,如:
def ANDROID_VERSIONS = rootProject.ext.android
def OTHER_LIBRARY = rootProject.ext.dependencies
...
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
api OTHER_LIBRARY.appcompatv7
api OTHER_LIBRARY.constraintlayout
api OTHER_LIBRARY.recyclerview
api OTHER_LIBRARY.cardview
api OTHER_LIBRARY.retrofit
api OTHER_LIBRARY.converterScalars
api OTHER_LIBRARY.okHttpLoggingInterceptor
api OTHER_LIBRARY.convertergson
api OTHER_LIBRARY.adapterrxjava2
}
如此以后在查看或者更換依賴的時候也方便查看和維護,注意在進行依賴的過程中,因為依賴不同的三方,可能會出現(xiàn)重復(fù)依賴相同庫而版本不一致的情況,這里有兩種解決辦法,一種是在對應(yīng)依賴的三方中剔除對應(yīng)的pom依賴,如:
api('com.facebook.fresco:fresco:0.10.0') {
exclude module: 'support-v4'
}
另外一種是強制依賴的相同庫的版本,如:
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
if (requested.group == 'com.android.support') {
if (requested.name.startsWith("support-") ||
requested.name.startsWith("animated") ||
requested.name.startsWith("cardview") ||
requested.name.startsWith("design") ||
requested.name.startsWith("gridlayout") ||
requested.name.startsWith("recyclerview") ||
requested.name.startsWith("transition") ||
requested.name.startsWith("appcompat")) {
details.useVersion SUPPORT_LIB_VERSION
} else if (requested.name.startsWith("multidex")) {
details.useVersion OTHER_VERSION.multiDex
}
}
}
}
組件路由
路由其實是組件化的核心組件,網(wǎng)上也有很多優(yōu)秀的開源庫,這里就直接使用阿里的開源庫ARouter,ARouter也是其中比較知名和穩(wěn)定的路由框架之一。
-
添加依賴和配置
android { defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] } } } } dependencies { // 替換成最新版本, 需要注意的是api // 要與compiler匹配使用,均使用最新版可以保證兼容 compile 'com.alibaba:arouter-api:x.x.x' annotationProcessor 'com.alibaba:arouter-compiler:x.x.x' ... } // 舊版本gradle插件(< 2.2),可以使用apt插件,配置方法見文末'其他#4' // Kotlin配置參考文末'其他#5' -
添加注解
// 在支持路由的頁面上添加注解(必選) // 這里的路徑需要注意的是至少需要有兩級,/xx/xx @Route(path = "/test/activity") public class YourActivity extend Activity { ... } -
初始化SDK
if (isDebug()) { // 這兩行必須寫在init之前,否則這些配置在init過程中將無效 ARouter.openLog(); // 打印日志 ARouter.openDebug(); // 開啟調(diào)試模式(如果在InstantRun模式下運行,必須開啟調(diào)試模式!線上版本需要關(guān)閉,否則有安全風(fēng)險) } ARouter.init(mApplication); // 盡可能早,推薦在Application中初始化 -
發(fā)起路由操作
// 1. 應(yīng)用內(nèi)簡單的跳轉(zhuǎn)(通過URL跳轉(zhuǎn)在'進階用法'中) ARouter.getInstance().build("/test/activity").navigation(); // 2. 跳轉(zhuǎn)并攜帶參數(shù) ARouter.getInstance().build("/test/1") .withLong("key1", 666L) .withString("key3", "888") .withObject("key4", new Test("Jack", "Rose")) .navigation(); -
添加混淆規(guī)則(如果使用了Proguard)
-keep public class com.alibaba.android.arouter.routes.**{*;} -keep public class com.alibaba.android.arouter.facade.**{*;} -keep class * implements com.alibaba.android.arouter.facade.template.ISyringe{*;} # 如果使用了 byType 的方式獲取 Service,需添加下面規(guī)則,保護接口 -keep interface * implements com.alibaba.android.arouter.facade.template.IProvider # 如果使用了 單類注入,即不定義接口實現(xiàn) IProvider,需添加下面規(guī)則,保護實現(xiàn) # -keep class * implements com.alibaba.android.arouter.facade.template.IProvider -
使用 Gradle 插件實現(xiàn)路由表的自動加載 (可選)
apply plugin: 'com.alibaba.arouter' buildscript { repositories { jcenter() } dependencies { classpath "com.alibaba:arouter-register:?" } }可選使用,通過 ARouter 提供的注冊插件進行路由表的自動加載(power by AutoRegister), 默認通過掃描 dex 的方式
進行加載通過 gradle 插件進行自動注冊可以縮短初始化時間解決應(yīng)用加固導(dǎo)致無法直接訪問
dex 文件,初始化失敗的問題,需要注意的是,該插件必須搭配 api 1.3.0 以上版本使用! -
使用 IDE 插件導(dǎo)航到目標(biāo)類 (可選)
在 Android Studio 插件市場中搜索ARouter Helper, 或者直接下載文檔上方最新版本中列出的arouter-idea-pluginzip 安裝包手動安裝,安裝后
插件無任何設(shè)置,可以在跳轉(zhuǎn)代碼的行首找到一個圖標(biāo) ()navigation
點擊該圖標(biāo),即可跳轉(zhuǎn)到標(biāo)識了代碼中路徑的目標(biāo)類
單獨調(diào)試
當(dāng)工程被拆分為組件化的時候,那么Module的單獨調(diào)試就顯得尤為重要,無論是對問題的跟蹤還是業(yè)務(wù)線的并行開發(fā),都需要工程具備單獨運行調(diào)試的能力。這里單獨調(diào)試同樣是對gradle的操作,通過對編譯腳本的編寫來達到組件單獨運行的目的
1.對需要單獨運行的Module抽取變量進行記錄
/**
* 歡迎組件
*/
welcomeLibRunAlone = false
/**
* 分享組件
*/
shareLibRunAlone = false
/**
* 主業(yè)務(wù)組件
*/
mainBusinessLibRunAlone = false
/**
* 登錄組件
*/
loginLibRunAlone = false
- 在對應(yīng)Module的編譯腳本文件中添加判斷Module是以Lib形式依賴還是以App方式進行依賴,如下代碼
def runAlone = rootProject.ext.loginLibRunAlone.toBoolean()
if (runAlone) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
當(dāng)進行到這里,其實就可以讓Module單獨運行起來,同時添加Application中一些必要的元素,清單文件Manifest.xml文件,但是這個xml文件是文件在單獨運行的過程中所需要的,所以這里要放到一個單獨的目錄下

同時在此基礎(chǔ)上通過編譯腳本配置單獨運行時獲取的Android相關(guān)文件
sourceSets {
main {
if (runAlone) {
manifest.srcFile 'src/main/runAlone/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java{
exclude 'src/main/runAlone/*'
}
}
}
}
根據(jù)以上配置,就可以使得登錄模塊單獨運行起來,調(diào)試起來非常方便
整體調(diào)試
根據(jù)上述方案,app殼其實只需要依賴業(yè)務(wù)組件即可
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':mainbusinesslib')
}
業(yè)務(wù)組件在根據(jù)調(diào)試變量對相應(yīng)的基礎(chǔ)組件進行依賴
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
api project(':baseuilib')
annotationProcessor OTHER_LIBRARY.arouterCompiler
if(!shareAlone){
api project(':sharelib')
}
if(!welcomeAlone){
api project(':welcomelib')
}
if(!loginAlone){
api project(':loginlib')
}
}
編譯優(yōu)化
這里的編譯優(yōu)化是針對gradle編譯而言,其一是gradle在編譯時的配置,如優(yōu)化不檢查png圖片的合法性等等
其二是固化底層代碼,只編譯業(yè)務(wù)線代碼,所謂固化其實是對開發(fā)不相關(guān)的代碼進行aar依賴,這樣在編譯的時候就不需要去編譯這些庫中的代碼,大大節(jié)省編譯時間,增加開發(fā)效率
def dependencyMode = "aar"
...
commonLibVersino = "1.0.0"
loginLibVersino = "1.0.0"
shareLibVersino = "1.0.0"
welcomeLibVersino = "1.0.0"
switchs = [
"CommonLib" : dependencyMode == "aar",
"ShareModule": dependencyMode == "",
"LoginModule": dependencyMode == "",
"WelcomeLib" : dependencyMode == "",
]
...
dependencies = [
...
"commonlib" : switchs['CommonLib'] ? "com.done.commonlib:CommonLib:" + "$commonLibVersino" : findProject(':commonlib')
]
同時在setting.gradle中編寫一下代碼
...
include switchs['CommonLib'] ? '' : 'commonlib'
綜上,一個簡單的組件化架構(gòu)就搭建完畢了
后期會整理一下的相關(guān)的代碼發(fā)布到github上哈歡迎大家進群交流哈群號:929891705
Demo地址
