Android模組化改造以及模組化通訊框架

heimashi發表於2018-10-29

Android模組化

例項程式碼和通訊框架地址

模組化/元件化

隨著客戶端專案越來越大,一個專案往往會分為不同的業務線,不同的業務線由不同的開發人員維護開發,模組化/元件化勢在必行,一個模組程式碼一條業務線,模組內職責單一,模組間界限清晰,模組自身的複用更加方便快捷,模組化的好處很多,同時也存在一些需要改進的地方:例如編譯速度的瓶頸越來越大、模組間怎麼進行高效通訊、模組怎麼獨立執行除錯、模組的可插撥以及隨意組合等等。

理想的模組化架構

image

  • 可以參考案例專案程式碼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")
複製程式碼

相關文章