Android模組化
模組化/元件化
隨著客戶端專案越來越大,一個專案往往會分為不同的業務線,不同的業務線由不同的開發人員維護開發,模組化/元件化勢在必行,一個模組程式碼一條業務線,模組內職責單一,模組間界限清晰,模組自身的複用更加方便快捷,模組化的好處很多,同時也存在一些需要改進的地方:例如編譯速度的瓶頸越來越大、模組間怎麼進行高效通訊、模組怎麼獨立執行除錯、模組的可插撥以及隨意組合等等。
理想的模組化架構
- 可以參考案例專案程式碼sample2
- 如上圖所示,模組後的程式碼從下往上看,可以分為三層:
- 最下層是common層,為上層業務提供基礎支援,該層不含有業務程式碼,可以再按功能細分為多個module,提供基礎統一的服務。
- 中間層是不同的業務線模組,module-a/module-b/module-c/...等,每個模組代表不同的業務模組,自身職責儘量單一,模組間界限清晰。向下以implementation形式依賴common基礎庫
- 最上層是殼工程application,該工程沒有業務程式碼,為所有模組提供一個組裝的殼,向下以runtimeOnly的形式依賴所有的業務線,採用runtimeOnly的目的是使得殼工程儘量的職責單一,各模組間在編譯期沒有程式碼上的直接互動,需要在執行期才能產生互動,模組間更加獨立和複用性更好。
模組化後的一些優化
採用像上圖這樣模組化改造之後,還可以進行更進一步的優化工作,例如支援模組的單獨編譯執行除錯,優化程式碼編譯速度等
模組的單獨編譯執行
有兩種思路可以實現模組的獨立執行:
-
思路1: 變數控制,參考案例1sample
- 1、在各模組的build.gradle中,根據控制變數來決定依賴的library外掛還是application外掛
if(getBooleanPropertyIfExist("BIsApplicationMode")){ apply plugin: 'com.android.application' }else{ apply plugin: 'com.android.library' } 複製程式碼
- 2、application模式下需要指定單獨的manifest和一些初始化程式碼,例如launcher的Activity等
sourceSets { main { if(getBooleanPropertyIfExist("BIsApplicationMode")){ manifest.srcFile 'src/main/release/AndroidManifest.xml' }else{ manifest.srcFile 'src/main/AndroidManifest.xml' } } } 複製程式碼
- 3、getBooleanPropertyIfExist工具方法是獲取gradle環境中的變數,如果變數不存在有個容錯方案是預設都是false,不存在gradle.properties檔案專案也能正常編譯
def getBooleanPropertyIfExist(propertyString) { if(hasProperty(propertyString)){ if(project[propertyString].toBoolean()){ return true } } return false } 複製程式碼
4、這樣一來就可以在gradle.properties檔案中定義模組的控制變數了,之後可以將gradle.properties檔案放在.gitignore中防止本地修改汙染別人的程式碼
AIsApplicationMode=false BIsApplicationMode=false 複製程式碼
-
思路2: 為每個模組增加一個Application的module,參考案例2sample2
- 每個module都新增一個Application的工程然後再依賴對應的模組,可以將這樣的模組聚合到一個目錄下,例如modules-wrapper,在settings.gradle中新增:
include ':sample2:modules-wrapper:module-a-app',':sample2:modules-wrapper:module-b-app' 複製程式碼
- 然後新建modules-wrapper目錄,在該目錄下建各模組的工程module-a-app,module-b-app...
- module-a-app這樣的工程都是application工程,提供模組啟動的殼,具體參考案例2sample2
-
總結:上面兩種思路都實現了模組的獨立編譯執行,思路1的缺點是需要維護兩套manifest等程式碼資源,每次修改gradle.properties的變數後需要重新sync一下程式碼可能比較浪費時間,相對而言思路2會更好一些,雖然增加了不少工程專案,但是收縮到一個目錄下後也還是較好管理。
專案全量包打包速度優化
經過上面的模組單獨編譯改造,模組本身的打包速度得到很大提高,因為模組本身可以以Application形式編譯,不需要依賴其他無關模組。但是如果要進行殼工程的編譯,即全量模組的打包,對於大專案時間還是會很慢。
一種優化的思路是這樣的:把模組的專案project形式依賴該為aar形式依賴,因為aar裡已經是編譯好的class程式碼了,減少了java編譯為class和kotlin編譯為class的過程。把不經常改變的模組打成aar,或者如果你在開發A模組,你就可以選擇將所有除A模組以外的模組全部以aar形式進行依賴,或者你可以選擇依賴你需要關心的模組,你不關心的模組可以不依賴。aar可以釋出到公司內部私服裡,還有一種辦法是直接釋出到本地maven庫,即在本地建一個目錄例如local_maven,將所有aar釋出到該目錄下,專案中再引入該本地maven即可。下面詳細介紹通過指令碼改造快捷的實現方案:
- 首先在utils.gradle的指令碼中新增發布aar的task,可以快捷的在所有的project中注入釋出的task避免重複的釋出指令碼
//add task about publishing aar to local maven
task publishLocalMaven {
group = 'msm'
description = 'publish aar to local maven'
dependsOn project.path + ':clean'
finalizedBy 'uploadArchives'
doLast {
apply plugin: 'maven'
project.group = 'com.rong360.example.modules'
if (project.name == "module-a") {//may changer version
project.version = '1.0.0'
} else {
project.version = '1.0.0'
}
uploadArchives {
repositories {
mavenDeployer {
repository(url: uri(project.rootProject.rootDir.path + '/local_maven'))
}
}
}
uploadArchives.doFirst {
println "START publish aar:" + project.name + " " + project.version
}
uploadArchives.doLast {
println "End publish aar:" + project.name + " " + project.version
}
}
}
ext {
//...
compileByPropertyType = this.&compileByPropertyType
}
複製程式碼
- 然後就可以在各模組中執行釋出aar的指令碼,就可以在local_maven目錄下檢視到已釋出的aar
./gradlew :sample2:module-a:publishLocalMaven
複製程式碼
- 在專案的build.gradle中加入本地的maven地址
repositories {
//...
maven {
url "$rootDir/local_maven"
}
}
複製程式碼
- 在utils.gradle的指令碼中新增根據變數控制編譯方式的指令碼
//返回0,1,2三種數值,預設返回0
def getCompileType(propertyString) {
if (hasProperty(propertyString)) {
try {
def t = Integer.parseInt(project[propertyString])
if (t == 1 || t == 2) {
return t
}
} catch (Exception ignored) {
return 0
}
}
return 0
}
//根據property選擇依賴方式,0採用project形式編譯,1採用aar形式編譯,2不編譯
def runtimeOnlyByPropertyType(pro, modulePath, version = '1.0.0') {
def moduleName
if (modulePath.lastIndexOf(':') >= 0) {
moduleName = modulePath.substring(modulePath.indexOf(':') + 1, modulePath.length())
} else {
moduleName = modulePath
}
def type = getCompileType(moduleName+'CompileType')
if (type == 0) {
dependencies.runtimeOnly pro.project(":$modulePath")
} else if (type == 1) {
dependencies.runtimeOnly "com.rong360.example.modules:$moduleName:$version@aar"
}
}
ext {
//...
runtimeOnlyByPropertyType = this.&runtimeOnlyByPropertyType
}
複製程式碼
- 在gradle.properties中就可以新增控制變數來控制專案是以aar形式/project形式/不依賴三種情況來編譯了
module-aCompileType=1
module-bCompileType=0
複製程式碼
上面這樣的設定代表模組a採用aar形式依賴,模組b採用project形式依賴
- 最後在殼工程中就可以呼叫compileByPropertyType來進行依賴了,根據gradle.property中的變數來選擇依賴方式:0採用project形式編譯,1採用aar形式編譯,2不編譯
dependencies {
runtimeOnlyByPropertyType(this, 'sample2:module-a')
runtimeOnlyByPropertyType(this, 'sample2:module-b')
//...
}
複製程式碼
專案中模組的可插拔以及自由組合
經過上面模組化指令碼改造,得益於runtimeOnly的模組依賴,可以通過變數來控制模組是以aar形式/project形式/不依賴。通過設定是否依賴就可以實現模組的可插拔以及自由組合,將不需要的模組設定為2就不會參與編譯了
module-aCompileType=2
module-bCompileType=1
複製程式碼
例如上面這樣的設定就代表不依賴模組a,模組b採用aar形式進行依賴,可以通過設定gradle.properties中的變數來一鍵管理各模組的依賴關係,快速實現了模組的可插拔以及自由組合。
除了在gradle.properties直接修改變數的值,也可以不修改任何程式碼,在執行gradle的編譯task的時候可以新增引數,通過-P來設定引數:
./gradlew :sample2:app:asembleDebug -P module-aCompileType=2
複製程式碼
模組化通訊
通過runtimeOnly形式依賴各模組後,最上層的app層是無法與模組直接通訊呼叫了,另外模組間也是無法直接通訊,但隨著業務的發展難免會有交集需要通訊呼叫。要實現通訊下面介紹兩種思路:
-
傳統的方式是:(此種方式App層不能以runtimeOnly依賴各模組內,必須以api/implementation):
- 第一步,在common層裡定義好需要的功能介面,例如實現貸款利率計算的功能
public interface IRate { float getLoanRate(float input); } 複製程式碼
- 第二步,在A模組提供貸款利率計算的功能,在該模組內實現這個介面:
public class AModuleLoanRate implements IRate { @Override public float getLoanRate(float input) { return input * 0.049f; } } 複製程式碼
- 第三,如果App層需要使用這個貸款利率功能,runtimeOnly依賴就實現不了了,只有App層如果通過api/implementation依賴A模組就可以在編譯期拿到A模組的類
IRate iRate = new AModuleLoanRate(); float result = iRate.getLoanRate(100f); 複製程式碼
- 第四,上面App層使用還能實現,就是需要妥協降級到強依賴,但是如果是別的模組需要呼叫利率計算功能就沒有較好的辦法了,只有一種非常彆扭的方式是這樣:在B模組中定義一個IRate變數,但其例項化需要在App層進行,而功能的呼叫又回到了模組內:
//1、在B模組內定義一個變數 IRate iRate = null; //2、在App層拿到IRate的實現類進行B模組變數的例項化 iRate = new AModuleLoanRate(); //3、再回到B模組中進行功能的呼叫 if(iRate != null) { float result = iRate.getLoanRate(100f); } 複製程式碼
- 總結:該方式能實現功能但需要妥協runtimeOnly降級到強依賴,並行非常不易維護
-
更優化的思路:本質的原理其實跟上面的傳統的思路非常類似,只是通過框架封裝來避免上面方案的問題,提供一個模組的服務管理中心,類似Android中的ServiceManager的"概念",需要暴露服務的提供方稱為"Server端",Server端需要向模組管理中心註冊,通過key進行服務註冊之後,其他模組稱為"Client端",如果想使用這個功能服務,Client端就可以通過註冊的key來向服務管理中心ServiceManager查詢該服務,查詢到了就可以使用該服務了。具體實現有兩種思路:基於註解處理期和transform+asm兩種思路
-
思路1:在分支apt-dev
- 模組中通過註解來標示需要暴露什麼服務
@ModuleService(register = "AModuleCalculateService") class AModuleCalculateService2 : IModuleService {...} @ModuleView(register = "AModuleView", desc = "Module A View") class AModuleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) {...} 複製程式碼
- 然後在模組中配置註解處理器,java用annotationProcessor,kotlin用kapt,並配置服務的模組索引
defaultConfig { //... javaCompileOptions { annotationProcessorOptions { arguments = [MSM_INDEX: 'com.rong360.example.a.AModuleIndex'] } } } dependencies { //... kapt project(':msm-compiler') } 複製程式碼
- 將模組中通過註解處理器生成的模組服務索引加入到服務管理中心
ModuleServiceManager.instance.registerModules(AModuleIndex(), BModuleIndex()) 複製程式碼
- 之後各模組就可以通過服務管理中心獲取服務/View了
val service = ModuleServiceManager.instance.loadService("AModuleCalculateService") as IModuleService? val aView = ModuleServiceManager.instance.loadView(this, "AModuleView") 複製程式碼
- 總結:上面的註解處理器方式的方案是一種不錯的思路,但是存在的麻煩點是配置比較繁瑣,需要在每個模組裡都加上註解處理器並配置索引,當然也可以學習阿里的ARouter的思路不易配置索引,在執行時來遍歷所有dex 找出我們註解處理器生成的類,但是編譯階段能解決的問題為什麼要轉移到執行階段呢,多多少少會影響app的啟動階段的,故本方案暫時不維護了,推薦用下面的思路2。
-
思路2:在預設分支master,原理介紹
- 還是使用註解來向服務管理中心註冊
- 然後在App工程中引入gradle外掛,該外掛會在Apk打包過程中利用Android gradle的transform來hook住所有class檔案,然後掃描出class中含有模組註冊資訊的註解,將所有的註解資訊記錄下來,然後利用asm生成專案的服務索引表class:DefaultModuleIndex
- 模組的呼叫方就可以通過這個服務管理中心查詢註冊好的服務了,反射例項化服務,之後就就可以順利進行通訊了。
-
總結 思路2採用gradle外掛的形式,使使用方只需在應用一次外掛就完成了所有配置,更加方便靈活。
-
模組化通訊方案介紹:module-service-manager
Android模組化/元件化後元件間通訊框架,支援模組間功能服務/View/Fragment的通訊呼叫等,通過註解標示模組內需要暴露出來的服務和View,應用gradle外掛會通過transform來hook編譯過程,掃描出註解資訊後再利用asm生成程式碼來向框架中註冊對應的服務和View,之後模組間就可以利用框架這個橋樑來呼叫和通訊了。
模組間通訊方案引入步驟:
詳細用法見案例sample2
- 第一步、在跟目錄的build.gradle中新增gradle外掛:
dependencies {
//...
classpath 'com.rong360.msm:msm-gradle-plugin:1.0.0'
}
複製程式碼
- 第二步、在Application專案的build.gradle中應用gradle外掛
apply plugin: 'com.rong360.msm.plugin'
複製程式碼
新增api的依賴
api 'com.rong360.msm:msm-api:1.0.0'
複製程式碼
-
開始使用吧:案例參考sample2
- 1、提供方:通過註解註冊服務/View/Fragment
@ModuleService(register = "AModuleCalculateService")
class AModuleCalculateService2 : IModuleService {...}
@ModuleView(register = "AModuleView", desc = "Module A View")
class AModuleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) {...}
@ModuleFragment(register = "AModuleFragment")
class AModuleFragment : Fragment() {...}
複製程式碼
- 2、使用方:通過api呼叫
複製程式碼
val service = ModuleServiceManager.instance.loadService("AModuleCalculateService") as IModuleService?
val aView = ModuleServiceManager.instance.loadView(this, "AModuleView")
val fragment = ModuleServiceManager.instance.loadFragment("AModuleFragment")
複製程式碼