Android组件化彻底解耦

作者: SimpleFunc | 来源:发表于2019-07-05 16:03 被阅读170次

    现在规模比较大的app都实现了组件化方案,来解耦和方便协作。带来的问题时模块之间的相互通信比较麻烦。

    一般App组件化之后的项目结构:

    app_module.png

    App模块依赖其他所有业务模块,业务模块之间相互依赖或单向依赖。

    Android App模块之间的通信方案有以下几种:

    1. 直接依赖

    这种方式最直接,但是解耦不彻底,可以直接使用依赖模块的所有类。需要开发者自己规范自己的依赖。

    2. 事件通知

    通过广播或Bus广播消息,性能较低,而且代码维护成本很高

    3. 路由

    这应该是目前比较主流的方案,方便,维护成本比较低。主要用来解决启动其他模块的Activity或Fragment的问题。但是无法直接调用其他模块的方法,而且声明方一旦修改就需要通知调用方

    4.公用模块

    创建一个公共模块,在该模块中放入各个模块需要暴露的接口。各个模块依赖中间模块,提供服务的模块内部提供实现接口供其他模块调用,公共模块的代码都可以编辑。其他模块的人修改了接口,有可能本模块也得修改。若不想引用其他模块的实现类,还需要反射,限制实现类名称等,维护成本比较低,但是限制比较多

    彻底解耦

    彻底解耦就是在开发时不能直接依赖其他模块,不能直接使用其他模块中的类。做到模块与模块之间的完全隔离

    为了彻底解耦,就不能使用implementation,因为implementation在开发时可以引用到其他模的类。但业务模块的代码又必须要打到apk中运行。所以只能使用runtimeOnly
    官方对runtimeOnly的解释:

    runtimeOnly: Gradle 只会将依赖项添加到构建输出,供运行时使用。也就是说,不会将其添加到编译类路径

    编译的时候不能添加,打包的时候会添加,符合我们的需求。

    使用runtimeOnly依赖模块之后,开发时无法使用该模块的类,运行时可以,如果需要通信就要考虑实现方式

    • 若只是调用页面可以通过router实现,因为开发时启动Activity不需要实例化具体的Actvity对象,拿到class名称即可。Fragment可以实例化转为Fragment对象,也不需要具体的类对象

    • 若要使用runtimeOnly依赖模块的类实例,或调用方法,则必须获取具体的类对象和方法名称,要么放在公共模块中,要么用通知的形式间接实现。

    所以只调用页面router就够了,如果要使用实例或调用方法就必须产生可直接访问的依赖(当然也可以通过使用ServiceLoader的形式实现共享服务,但是不够灵活,而且维护成本也偏高)。所以肯定需要一个中间模块

    既然需要中间模块,那就创建一个。但是又不希望这个中间模块过于复杂,最好能完全独立,在参考了各种解决方案之后的采用的方案:

    app_model2.png
    • App依赖其他业务模块的逻辑不变,通过runtimeOnly依赖
    • 若App需要调用其他模块的逻辑,由其他模块提供一个接口,接口提供给外界调用的方法,然后实现该接口,非App模块当然也可以调用
    • 然后把声明的接口copy到中间模块中,需要调用的模块依赖中间模块就可以使用该接口了

    相当于不同模块中存在完全一样的包名和类名的接口,调用模块只要能拿到该捷库的实现类的类名,然后通过反射生成实例就可以通过调用接口的形式调用其方法了。

    app_module2.png

    中间模块只包含其他模块提供的接口,没有实现,所以中间模块相对来说是完全独立的,不会和其他模块耦合。但是由于中间模块和提供服务的模块中存在同样的类,因此这些类是多余的。虽然不影响编译和运行,但是最好还是能去掉。

    怎么去掉呢?那就要使用compileOnly依赖

    compileOnly:Gradle 只会将依赖项添加到编译类路径(即不会将其添加到构建输出)。如果是创建 Android 模块且在编译期间需要使用该依赖项,在运行时可选择呈现该依赖项,则此配置会很有用。
    如果使用此配置,则您的库模块必须包含运行时条件,以便检查是否提供该依赖项,然后妥善更改其行为,以便模块在未提供依赖项的情况下仍可正常工作。这样做不会添加不重要的瞬时依赖项,有助于缩减最终 APK 的大小

    不过Android library module不支持使用compileOnly依赖。若模块对外的暴露的接口中没有Android相关的类,可以直接将中间模块创建为java library.
    若必须要使用Android相关的类,可以把sdk目录下的android.jar放到中间模块的lib文件夹下依赖进去,其他依赖类似。同时也应该要求对外暴露的接口中的方法的参数尽量简洁。

    在需要和其他模块交互的模块中使用compileOnly依赖中间模块即可,而且只存在于开发编译期间。并不会将重复的类打到APK中,完美解决了此问题。

    具体实现

    1. 对外暴露的接口怎么复制?

    手动复制: 虽然可以实现,但是比较麻烦,维护成本太高,肯定不可能采用
    自动复制:通过gradle插件实现,在项目开始编译之前遍历项目所有模块的代码,找到各个模块中对外暴露的接口类,然后copy到中间模块的源码下。copy之前需要先清理之前copy的源码,因为模块中对外暴露的接口可能已经发生了改动。也可以对比下是否需要copy, 需要copy再copy

    模块中对外暴露的类怎么识别?
    可以通过注解,也可以规定该类的类名符合一定的规则,比如以固定的词开头或解释,或不一样的后缀。改为不一样的后缀之后就是普通的文本文件,不支持代码高亮。但可以在android studio中编辑器中的File type中把自定义的后缀加进去。但是如果是kotlIn和java混合项目的话就比较麻烦,毕竟一种文件后缀只能被设置为一种语言的源码

    2. 怎么找到对应接口的实现?

    通过注解的方式,把实现类和对应的接口类关联起来,然后收集到一个map容器中就可以实现了。
    注意: 一个接口可能有多个实现类,需要添加一个识别不同实现类的Key

    为了各个模块都能使用,需要把装载接口和实现类Class的map容器放入到公共模块中,类似Router的实现
    map应该存在单例对象中或设置为静态的,因为在App运行期间各个模块都可能从里面查找对应的实现类

    3. 怎么调用?

    使用的模块通过compileOnly 依赖中间模块之后,就可以直接使用中间模块中所有的类了,但是只是接口类。还需要拿到实现类才能使用。

    通过接口的类名在存放对应的容器找到对应的实现类的类名,然后通过反射的形式生成实例,调用就行了。

    4. 怎么防止copy过去的接口文件被修改?

    通过插件copy过的文件在中间模块中,开发人员可以修改该文件。一旦改动了开发时就会找不到对应的实现或方法。为了避免这类事情的发生,最好是文件不能编辑。怎么限制编辑呢?
    文件设置为只读,不好实现。
    后来想了想直接编译中间模块,生成对应的jar. 然后将copy过去的文件删掉不就行了。
    这样copy过去的源文件已经编译成class,当然无法修改
    需要监控中间模块的编译过程,当然只能在gradle插件中实现。
    由于在插件中App是第一个处理的模块,处理这些事情越早越好。一般情况下App模块都会依赖中间模块,若你的项目App模块不依赖中间模块,那以下的方法不一定有效,那就还是直接依赖源码文件吧。
    实现过程:

    • 在处理App时遍历所有其他模块的文件,除了中间模块,把符合特征的类copy到中间模块源码文件夹下
    • 监控中间模块的编译过程,由于App compileOnly依赖中间模块,所以中间模块会先编译为jar,
    • 把编译好的jar复制到中间模块的libs文件夹下
    • 删除之前copy过来的源码文件
    • 完成,还是比较简单的

    这种方案的不足之处

    1. 项目中会多出一个无代码的中间模块,感觉上可能觉得是多余的
    2. 模块中对外暴露的接口中不能引用本模块特有的类,否则复制过去之后会找不到该类。当然也可以把该接口依赖的类全部的类都copy过去。这样做不合理,而且会增加维护成本
    3. 由于使用了注解,多了一个中间模块,而且在编译之前需要复制文件,会增加项目的编译时长,但是增加的时长应该比较少,还可以接受
    4. 由于对外暴露的接口的实现类是通过反射获取实例的,所以实现类必须有一个空参数的构造方法

    相关文章

      网友评论

        本文标题:Android组件化彻底解耦

        本文链接:https://www.haomeiwen.com/subject/qarscctx.html