module-service-manager

Introduction: Android 模块化/组件化通信框架
More: Author   ReportBugs   
Tags:

目录


模块化或组件化

随着客户端项目越来越大,一个项目往往会分为不同的业务线,不同的业务线由不同的开发人员维护开发,模块化/组件化势在必行,一个模块代码一条业务线,模块内职责单一,模块间界限清晰,模块自身的复用更加方便快捷,模块化的好处很多,同时也存在一些需要改进的地方:例如编译速度的瓶颈越来越大、模块间怎么进行高效通信、模块怎么独立运行调试、模块的可插拨以及随意组合等等。

理想的模块化架构

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 避免重复的发布脚本 ```Groovy //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
```Shell
./gradlew :sample2:module-a:publishLocalMaven
  • 在项目的 build.gradle 中加入本地的 maven 地址

    repositories {
          //...
          maven {
              url "$rootDir/local_maven"
          }
      }
    
  • utils.gradle的脚本中添加根据变量控制编译方式的脚本 ```Groovy //返回 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 形式/不依赖三种情况来编译了
```Groovy
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,之后模块间就可以利用框架这个桥梁来调用和通信了。

模块间通信方案引入步骤:

  • 第一步、在跟目录的 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 ```kotlin @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 调用
```kotlin
val service = ModuleServiceManager.instance.loadService("AModuleCalculateService") as IModuleService?
val aView = ModuleServiceManager.instance.loadView(this, "AModuleView")
val fragment = ModuleServiceManager.instance.loadFragment("AModuleFragment")
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools