一种Android蝇量级组件化方案

简单,实用,速成

Posted by lx8421bcd on June 25, 2018

前言

两个月前公司说要做一个新项目,先放在主App里面,如果后面运营的好,考虑独立出来单独做一个App,听到这种需求我脑子里第一时间就想到了组件化,后续需求详细之后也在组内提出组件化来做的方案,大家也表示可以一是,于是就开始了组件化技术选型之路。
组件化的概念已经有很多文章介绍,主要就是将一个大的app根据业务拆分为很多module,业务无关的内容沉淀到基础库,根据不同的需求,搭建好壳工程,组合需求的module打包成一个app。
组件化有很多好处,也会增加不少麻烦,对于我们团队来说,组件化所要解决的事情主要是2点:

  1. 在一个业务需要独立运营的时候只用做很少的工作就能出包
  2. 模块划分,代码隔离

组件化实现最重要的点,就在于如何解决模块间通信的问题,市面上几种组件化方案我都看过一遍,很多基于URLScheme,自定义一套类似于RPC协议一样的调用模式,功能很强大,然而对于我们团队来说过于重型。 我们Android团队只有6个人,要维护3款产品,而且需要在兼顾业务需求的情况下完成组件化实现,因此对于我们来说我们首要要做的事情,是解决我们提出组件化时要处理的两个点,引入目前市面上很多组件化框架并且做全App规模级别的修改,成本是难以接受的。

正当选型头疼时我看到了微信的一篇文章微信Android模块化架构重构实践
其中关于“接口API化”的内容很吸引我,直接使用接口来作为module对外暴露功能的媒介要比定义协议轻量许多。然而这篇文章并没有给出什么实现demo或者示例代码。也正如我之前所言我们的团队规模不允许我们在工程结构上大动干戈。 与此同时,同学推荐的CC提出的“组件总线”的概念也吸引到了我,CC这个框架也很轻量,功能强大、支持渐进式改造,然而我们的需求比所提供的功能还要少,只有那2点。

所以我最后决定的实现方案思路参考了“微信的模块化”和CC的“组件总线”,1天时间实现了一个很简陋但完全满足需求组件路由。也就是本文所说的“蝇量级”组件化。

工程结构

组件化想要做的第一步还是代码解耦,模块化,业务module如何去划分是组件化最为重要的问题,如果这点想不清楚,那么可以认为是没有组件化的需求,干脆不要折腾。业务module分不清楚将会是今后module边界不清晰,代码混乱的元凶。

剥离出业务无关的代码作为基础库,在我们的应用中,我们将以下内容判定为业务无关:

  • 业务无关的各种基类、工具类、自定义控件
  • 任何模块都需要依赖的第三方SDK
  • 用户账户系统以及各种业务无关的接口实现(如果这些都不使用,那这个module也没必要放在我们的工程里了)

以上内容作为应用框架层(framework)提供给各个module使用。
我们公司的应用是一个综合直播类应用,因此我将业务大概分为以下几个大模块:

  • Framework module - 基础资源库,业务无关模块组成
  • ComponentSDK - 组件化功能module,组件化功能实现存放在这里,它也是路由模块。
  • IM module - IM模块,由我们的IM组件+第三方IMSDK组成
  • VideoSDK module - 视频模块,由各种直播相关SDK + 直播间/视频播放器等组件组成
  • App modules - 各个业务模块
  • shell - 壳工程,也是应用打包的module

可以简要将这些module分为3类:业务无关的基础库、业务相关的组件module、业务module。

路由

路由模块

之所以强调是“蝇量级组件化”,就是在于路由的实现及其简单,之前我们说直接使用接口来作为module对外提供功能的形式,那么module接口定义必须得让所有业务module能够访问到才行,所以任何想要实现组件化调用的module都必须依赖ComponentSDK。 ComponentSDK module大概结构如下:

基本的工程结构是
-com.companyname.componentsdk
    -components         // package, module 定义存放
        -IMComponent.java
        -ABizComponent.java
        -BBizAComponent.java
        -.....
    -entities           // package, module 间均会使用的JavaBean定义存放。
        -Order.java
        -Video.java
        -...
    -ComponentBus.java  // java class 组件总线类定义及实现
    -IComponent.java    // java class 基础组件接口定义

路由接口定义

IComponent接口定是组件Component类型的定义基类,定义了所有module所共有的方法,在ComponentSDK中module对外功能提供以Java Interface的形式定义,继承IComponent接口。

public interface IComponent {
    
    void onRegister();
    
    void onUnregister();
}

一个module A需要提供什么功能,直接在AComponent接口中定义即可

public interface IMComponent extends IComponent {

    void openConversation(int conversationId);

    void deleteConversation(int conversationId);

    void blockUser(int userId);

    Fragment getIMConversationFragment();

    ......
}

由于这种方式没有太多的封装,可以充分利用语言和AndroidSDK本身的特性,组件之间的交互也可以不止于数据,可以传递诸如View,Fragment等Android SDK内的基础控件定义,当然了,建议此类传递止步于此,壳工程从组件module里拿来展示就行了,别再进行过多的编辑,避免产生高耦合。
如果看上去属于组件module的页面在壳工程里与其它业务有很多交互,那么个人认为这个页面应该在壳工程内实现,而不要想着把它弄到组件module里然后开特例,这样就违背了组件化的初衷。

路由接口实现

在业务module里,编写一个类,实现组件SDK中定义的组件接口即可

public class IMComponentImpl implements IMComponent {

    void onRegister() {
        // implementation code ......
    }
    
    void onUnregister() {
        // implementation code ......
    }

    void openConversation(int conversationId) {
        // implementation code ......
    }

    void deleteConversation(int conversationId) {
        // implementation code ......
    }

    void blockUser(int userId) {
        // implementation code ......
    }

    Fragment getIMConversationFragment() {
        // implementation code ......
        return imCvrFragment;
    }

    ......
}

在编写组件实现时,需要注意的一点是组件实现的生命周期和Application是一致的,在实现组件接口定义的对外方法时尽量不要在这里缓存Activity Context、View等Android组件控件,避免内存泄漏

依赖

依赖这一块,主要是要避免其他人在合作开发时,因为不注意而直接引用组件module内的代码和资源,导致产生强依赖;所以在开发时要将壳工程对组件module的依赖关系屏蔽,让开发者只能通过组件接口调用到组件module内对外开放的内容。
虽然我知道可以自定义gradle的implementation指令,但是出于开发速度的考虑,我没有整这套内容,而是直接采取特定impelmentation的形式:

// build.gradle file in shell module
dependencies {
    //baseic dependencies
    implementation project(':framework')
    implementation project(':componmentsdk')

    // IM SDK 
    debugImplementation project(':imsdk')
    releaseImplementation project(':imsdk')
    // Video SDK
    debugImplementation project(':videosdk')
    releaseImplementation project(':videosdk')
    //......
    debugImplementation project(':bizmoduleA')
    releaseImplementation project(':bizmoduleA')
}

等到之后有时间可以向CC学习自定义依赖命令,优化依赖方法

初始化

简而言之组件注册就是在应用初始化的时候将各个ComponmentImpl注册到组件总线上,就可以供组件间调用了

    /* 组件注册到组件总线 */
    ComponentBus.register(new IMComponentImpl());
    ComponentBus.register(new VideoSDKComponmentImpl());
    ......

但现在有个问题梗在最后阶段,开发阶段壳工程是无法引用到组件module里实现的ComponentImpl的,如何拿到组件接口实现类并生成实例注册呢?
在上段的依赖管理中,我们最开始是直接使用implementation将业务module直接引入的,但这样的问题是开发过程中只能靠code review和程序员自觉来避免出现module间的直接调用,众所周知没有机制挡死,只靠自觉是靠不住的,很容易出现破窗。
在看CC代码的时候,有个开发思路启发了我,或者说是点通了我:
组件化与插件化相当不同的一点就是组件化只是针对工程来说的,最后打包出来的App仍然是一个完整的App,组件间内容的直接依赖只是在开发环境中被屏蔽,而在打包后的应用环境内是可见的。换句话说,如果你在壳工程里写了直接调用组件module的代码,或者在组件module里写调用壳工程内容的代码,通过某种方式绕开编译检查成功打包,那么在打包生成的apk中,这个代码是不会报错的,因为依赖找得到。
所以只需要在生成apk的过程中生成注册代码插入到Application初始化的过程中,就可以实现模块解耦并完成自动注册了。

还好,作者在实现CC的时候也注意到了这点,并将其封装为 AutoRegister
因此目前直接使用AutoRegister就可以实现我的需求,只不过需要做一点小小的适配。

// code in component_config.gradle

project.apply plugin: 'auto-register'
project.ext.registerInfoList = [
    [
        // 需要注册的接口类,AutoRegister会扫描所以此接口的实现并注册
        'scanInterface'             : 'com.company.componentsdk.IComponent'    
        // 需要注册到哪里,这里是组件总线类
        , 'codeInsertToClassName'   : 'com.company.componentsdk.ComponentBus'  
        //未指定codeInsertToMethodName,默认插入到static块中,故此处register必须为static方法
        , 'registerMethodName'      : 'register' // 使用resister方法注册
        , 'exclude'                 : [
            //排除的类,支持正则表达式(包分隔符需要用/表示,不能用.)
            'com.gameabc.framework.componentize.'.replaceAll("\\.", "/") + ".*"
        ]       
    ]
]
autoregister {
    registerInfo = registerInfoList
}

需要注意的一点是,AutoRegister是没有办法检查累的继承关系的,换句话说,IMComponentImpl实现了IMComponent接口,IMComponent则继承自IComponent,那么AutoRegister在扫描IComponent接口的实现时是扫不到IMComponentImpl的,也就会导致注册失败。不过还好Java语法特性支持接口多重实现,在实现子接口的同时实现其父接口,并不会影响实际的实现和调用,因此只需要在组件接口实现类上动一点手脚,就可以保证其被AutoRegister扫描到并自动注册

public class IMComponentImpl implements IMComponent, IComponent {

    ......

}

组件间调用方式

通过组件总线获取组件接口实现,直接调用组件接口中定义的方法

    // 壳工程中需要打开一个IM会话
    int conversationId = getConversationIdFromSomeWhere();
    ComponentBus.get(IMComponent.class).openConversation(conversationId);

    // 获取IM组件中定义的IM会话列表页
    Fragment cvrFragment = ComponentBus.get(IMComponent.class).getIMConversationFragment();

如上所示,没有任何弯弯绕,这也是我认为本方案相比于现在几种开源组件化方案的优势所在

后记&总结

总的来说,这个方案其实是很简陋的,只达到做组件化最初的需求:模块间解耦,能相互调用,支持渐进式改造。
市面上很多组件化方案提供的诸如跨应用,跨进程调用等,都没有在本方案中体现,但并不是不可做到,只不过我这边暂时没有这种需求,因此没有在实现中体现。 由于完整的实现方案涉及到公司的代码,因此也没有什么demo可以分享了,也许以后有时间,可以将这个方案整理完善,做成一个开源的极简组件化实现:)

如果读者能够看明白我文中所说的内容,那么我相信你应该能够根据我的思路实现这一个组件化方案,核心代码也就200~300行而已,如果说你看不明白,建议你还是不要轻易尝试组件化,避免因为hold不住而产生一系列问题。如果说时间充裕的化,建议还是使用CC来进行组件化,ARouter的方案有点过于重型,需要对应用进行大幅改造,如果以后业务收缩需要撤掉组件化实现会很蛋疼