阅读 3203

医动力Android基于CC组件化框架的探索与实践

为什么要组件化?

医动力App作为公司的核心产品已经有多年历史了,随着版本的不断迭代,功能越来越多,代码量越来越大,不可避免的会产生一下问题:

  • 业务越来越复杂,维护成本高;
  • 业务耦合度高,代码越来越臃肿,团队内部多人协作开发困难;
  • 编译时间长,每修改一处代码后都要重新编译打包测试,导致非常耗时;
  • 开发测试困难,每次修改都必须打包整个项目运行.

因此,为了提高项目的可维护性和开发效率,组件化成为了必然.

组件化的目标

首先看看老版本的医动力的项目工程结构

项目分为Doctor和Patient,它们之间公共的代码放在common中,common中会依赖一些第三方的库.随着项目的迭代,Doctor和Patient里面的代码会越来越耦合,因为单个module中仅仅是以包名作为功能的划分,而包之间是可以随意调用的.

AndroidStudio/IDEA是多模块管理的,组件化的思路就是把不同的业务模块拆分到各个子Module中,同时这些业务Module之间不会存在直接的调用,这样当我们移除项目中的某个业务Module的时候不会影响整体的项目运行,如下图所示:

可以看到有「ModuleIM」,「ModuleFollow」,「ModuleDiary」三个业务模块,它们都会引用Common模块而获取一些基础库的支持,同时这些业务模块是可以单独运行的.后续会将更多的模块独立出来,完成彻底的组件化.

如何组件化?

Android的组件化的技术点主要在两个方面:

  • 配置组件独立运行的能力;
  • 组件之间的通信

第一点是通过在module的gradle.build中切换

apply plugin: 'com.android.application'
apply plugin: 'com.android.library',
复制代码

并设置对应模式下加载的AndroidManifest.xml和src的路径.

第二点由于组件之间是没有直接依赖的关系的,要想让它们通信就必须把它们注册在一个公共的地方,这里可以通过路由,也可以通过注册接口.

目前组件化的方案在网上有很多,由于组件化并不涉及Android系统级别的操作,因此是比较成熟稳定的.我们知道,组件化是需要花费大量时间和精力的,很难做到把项目彻底的拆分成组件,那有没有一种方式可以让我们不改动原有项目(改动很小)的情况下,将新功能进行组件化开发?

因此我们选择了使用CC来进行组件化的改造

Component Caller(CC)介绍

业界首个支持渐进式组件化改造的Android组件化开源框架

引用自CC官方的两张图能让我们很快的明白什么叫做渐进式组件化?

在渐进式组件化的方案中,可以先不用解耦,只需要让单独运行的组件能够调用到主App中的功能即可。思路是这样的:

  • 新业务以组件形式开发
  • 新组件需要调用的主App中的业务,在对应的模块中创建一个组件类,对外暴露对应的服务,供其它组件调用,并不需要现在就将这个模块解耦
  • 新组件通过跨App的方式调用主App中的组件
  • 主App也可以通过跨App的方式调用到单独运行的组件App中的组件
  • 在同一个module中可以创建多个组件类,将来解耦时将对应的组件类移动到解耦后的module中即可

关于CC的技术实现细节可以查看其github主页的wiki系列文章 github.com/luckybilly/…

组件化实践

CC的集成

1.在项目的根gradle.build中加入:

buildscript {

    dependencies {
        ... ...
    
        classpath 'com.billy.android:autoregister:1.4.1'
    }
}

复制代码

这个autoregister插件是用来在编译期间动态扫描并修改class文件,实现组件的自动注册,具体配置后面会提到

2.在Doctor和Patient这两个主App的gradle.build中加入

ext.mainApp = true  //设置为true,表示此module为主app module,一直以application方式编译
apply from: '../cc-setting.gradle'
复制代码

cc-setting.gradle中主要进行了以下几点操作:

  • 读取local.properties文件中的配置来区分组件是否以独立app的方式编译;
  • 添加com.billy.android:cc:1.1.0依赖;
  • 自动注册组件的配置

3.实现IComponent接口创建组件类

我们统一在Doctor和Patient中的exports包内创建App对外提供的组件,不同业务的组件放在创建在对于的类中管理

IComponent的实现也很简单,提供一个模块名称name和对不同action的处理

主App的配置就这么多,接下来要新建一个组件Module

4.业务module的配置

这里需要设置module独立运行时的applicationId,同时指定resourcePrefix资源前缀(防止不同模块之间资源文件的冲突)

因为设置了module的独立运行,就需要准备一份module在独立运行模式下的AndroidManifest文件,路径在src/main/debug下

5.业务module提供IComponent接口

和主App一样,模块要提供服务给其他模块调用,需要提供实现IComponent的子类,因为业务module提供的服务会比较少且单一,我们将它放在包名下的ExportComponent下

7.主App设置需要依赖的module

dependencies {
    api fileTree(include: ['*.jar'], dir: 'libs')
    addComponent 'module_follow'
    addComponent 'module_diary'
    addComponent 'module_im'
}
复制代码

通过addComponent方式添加依赖,其内部会根据业务module的运行模式决定是否依赖

6.设置业务module的运行模式

业务module的运行模式包括开发模式和集成模式

  • 开发模式:会以App的方式运行
  • 集成模式:打包在主App中

打开local.properties文件

module_follow=true
module_diary=true
module_im=false
复制代码

设置模块名称等于true或者false(没有设置则为false)

  • 当等于true则该模块会以App运行,这时候打包主App的时候是不会把该模块打包进去
  • 当等于false时则不能独立运行,打包主App的时候会一起打包进去

运行调试

现在你可以将组件单独运行起来了,但是由于业务组件是不包含登录功能的,因此它是没有用户登录状态的,所以我们需要通过CC的组件调用去主App中获取

补充一点,如果想要跨App调用首先需要打开CC的设置

CC.enableRemoteCC(true);
复制代码

组件调用

CCResult result = CC.obtainBuilder("common")
                .setActionName("httpInfo")
                .addParam("type", type)
                .build()
                .call();

        String baseUrl = result.getDataItem("baseUrl");
        String token = result.getDataItem("token");
复制代码

调用的方式很简单,只需要指定模块名称和对应的Action,同时可以传递参数,以上代码调用的是common模块的action为httpInfo的组件.

同时支持同步和异步的调用,前一个例子是同步调用,异步调用需要传入一个回调方法.

String callId = CC.obtainBuilder("Common")
            .setActionName("httpInfo")
            .addParam("type",type)
            .build()
            .callAsync(new IComponentCallback(){...});
复制代码

被调用组件在处理完请求后,需要作出响应,看以下代码:

public class HttpComponent implements IComponent {

        @Override
        public String getName() {
            return "http";
        }
    
        @Override
        public boolean onCall(CC cc) {
            String action = cc.getActionName();
            if(action.equals("action1")){
    
                CC.sendCCResult(cc.getCallId(),CCResult.success());
                return false;
            }else if(action.equals("action2")){
                String ccId = cc.getCallId();
    
                return true;
            }
    }
}

复制代码

注意onCall()方法的返回值,action1中返回的是false,而action2返回的是true,作用是:

  • false表示立即回调结果,这里需要调用 CC.sendCCResult(cc.getCallId(),CCResult.success() 告诉调用发返回成功
  • true则表示延迟回调结果,这时你可以拿着ccCallId,等到事情处理完后才调用CC.sendCCResult(cc.getCallId(),CCResult.success()

这有什么作用呢? 比如你在调用一个登录组件去到登录界面,只有登录成功了才返回结果,这就需要延迟回调.

网络请求组件化

项目中用到的网络请求框架是okhttp+retrofit,我们希望不同module中使用不同的retrofit的Service实例,比如在Module_follow中我们会创建FollowService.java来处理当前模块的网络请求

在集成开发模式下我们可以通过组件调用去获取主App中的Retrofit对象,我们只需要在主App中定一个name=http,action=getRetrofit的IComponent;

但在开发模式下,跨进程调用组件是传输不了Retrofit对象的,因为Android的跨进程只能传输Parcelable对象,这里我们可以在本module中提供一个相同名称的IComponent,在里面去获取主App的baseUrl和token,并创建新的Retrofit对象,这样就可以透明的处理获取Retrofit对象了.

为什么以上方式可行呢?因为CC在进行组件化调用的时候,会检查当前模块是否存在要调用的模块,如果存在则会调用本地的,不存在才会去跨进程调用.最后我们可以把这些模拟操作抽到一个lib_mock的module里面复用.

组件化小结

组件化后带来的一些变化:

  • 编译时间明显缩短
  • 开发人员之间可以通过模块分工
  • 可以在模块中尝试新的技术而不担心影响全局(Kotlin)

CC已知局限:

开发模式不能在Android8.0及以上环境运行,开发的时候可以使用虚拟机或者低版本的手机,集成模式不影响.

引用

CC:基于总线的android组件化开发框架
Android彻底组件化方案实践

关注下面的标签,发现更多相似文章
评论

查看更多 >