组件化实践

工程链接

1.背景介绍

工程配置 android gradle3.0.1,Gradle4.4。因为Android Studio的版本不同,可能upgrade gradle plugin的时候,会触发起码module的build.gradle的运行,而此时module的build.gradle又要使用自己写的这个plugin的东西去解析自己的声明,所有这就是一个鸡和蛋的问题。所以建议将module_build单独拉project,编辑上传本地maven库,然后实例工程使用上传的gradle插件记好了;

最新的修改:所有的module都变为地位平等,app的module也需要配置是否可以作为其他module的依赖module。gradle编译的时候,一般命令为:gradle build(表示运行以根目录下config.gradle中配置的main_module为主module的编译工作),单独编译指定module,可以使用命令:gradle :module_name:task_name。

项目功能已趋近稳定,具体每个功能都已经有完备的流程;

就目前我看到的产品业务发展规划,是在不断完善项目,修复bug的前提下,主要工作就是对接第三方的设备。

而在这个过程中,比较突出问题:针对越来越多不同设备,不同需求的接入,修剪适配项目代码,但是前期代码糙快猛,模块功能之间耦合比较严重,对项目代码的修剪工作造成了不小的麻烦,多半情况下的妥协处理就是把不需要的功能入口屏蔽掉,但是相关代码还存在。所以即使某个第三方设备仅仅需要一小部分的功能,其安装包也是和全功能的版本大小一样的,这种在我们推送有流量限制的前提下,是很不节俭的。

另一个也是代码耦合造成的问题:因为所有功能都耦合在一个module中,在一个版本的迭代中,就有可能因为相互依赖的问题,本该并行开发的工作,变成了线性,严重影响效率。对于测试人员,也要因为任何的改动,做全功能的回归测试。

基于以上,项目的组件化重构就提上了日程。



2.组件化原理

  • 组件化的定义:

    Component-based software engineering (CBSE), also known as component-based development (CBD), is a branch of software engineering that emphasizes the separation of concerns in respect of the wide-ranging functionality available throughout a given software system. It is a reuse-based approach to defining, implementing and composing loosely coupled independent components into systems. This practice aims to bring about an equally wide-ranging degree of benefits in both the short-term and the long-term for the software itself and for organizations that sponsor such software.

概括一下,就是基于可重用的目的,将一个大型的软件系统按照分离关注点的形式,拆分成多个独立的组件,以此减少耦合。针对我们的项目,大体的体现如下:(未体现base-lib等底层公共module)

项目的架构演进
组件化思想应用到我们的项目上,就是将收银,Erp,团购验券,预授权等业务模块进行解耦,独立成单独的组件,降低业务复杂度,每个组件都可以有自己独立的版本。根据不同的设备和商务需求,动态的去组合不同组件。这样做的好处,

  1. 代码耦合降低,一个模块的改动不涉及或者很少涉及其他模块,提升代码质量,减少出bug的概率;

  2. 代码修改范围变小,测试的工作量也相应的减少,将具体的业务分拆组件化之后,可以将没有变动的模块剔除回归测试的checklist,减少测试工作量;

  3. 每一个独立的组件,都可以独立运行,方便开展独立测试;

  4. 根据不同设备的需求,定制app,不需要的组件不会打包到app中,减少apk的大小;



3.方案选择

明确了目标,那么就要提出对应的实现方案,网络上有一热心网友对比了几个比价成熟的组件化方案:组件化方案对比

根据热度和传播程度,重点关注了一下得到 DDComponentForAndroid阿里Arouter

DDComponentForAndroid 在路由上面的设计和其他几家通过注解,动态代码生成的实现方式都是大同小异的,但是他在使用Gradle Plugin实现Module之间的完全隔离上,想法比较新颖。只需要声明使用一个plugin,完全不用再多处手动配置手动配置的实例

ARouter 严格意义上说,不能算是一个组件化框架,是一个用来解耦的路由框架,通过路由完成功能模块间之间的跳转,杜绝了模块间之间的直接依赖,也是组件化重要的思想和工具。

经过方案的对比,所以最终决定我们项目中的组件化使用:

ARouter (路由功能) + Gradle Plugin(Module隔离,Module初始化代码动态注入)




4.方案实施

1, ARouter的使用

由于ARouter推出时间比较长,功能多样,性能稳定,关于如何使用,网络上比比皆是,没必要过多介绍了,这里重点关注一下其优缺点和典型的应用场景:

  • 功能介绍

    1. 支持直接解析URL进行跳转、参数按类型解析到Bundle,支持Java基本类型(*)

    2. 支持应用内的标准页面跳转,API接近Android原生接口

    3. 支持多模块工程中使用,允许分别打包,包结构符合Android包规范即可(*)

    4. 支持跳转过程中插入自定义拦截逻辑,自定义拦截顺序(*)

    5. 支持服务托管,通过ByName,ByType两种方式获取服务实例,方便面向接口开发与跨模块调用解耦(*)

    6. 映射关系按组分类、多级管理,按需初始化,减少内存占用提高查询效率(*)

    7. 支持用户指定全局降级策略

    8. 支持获取单次跳转结果

    9. 丰富的API和可定制性

    10. 被ARouter管理的页面、拦截器、服务均无需主动注册到ARouter,被动发现

    11. 支持Android N推出的Jack编译链

  • 2 不支持的功能

    1. 自定义URL解析规则(考虑支持)

    2. 不能动态加载代码模块和添加路由规则(考虑支持)

    3. 多路径支持(不想支持,貌似是导致各种混乱的起因)

    4. 生成映射关系文档(考虑支持)

  • 3 典型应用场景

    1. 从外部URL映射到内部页面,以及参数传递与解析

    2. 跨模块页面跳转,模块间解耦

    3. 拦截跳转过程,处理登陆、埋点等逻辑

    4. 跨模块API调用,模块间解耦(注册ARouter服务的形式,通过接口互相调用)

2,关键点:组件化Gradle插件

上文提到一种手动配置组件合并打包和组件独立打包的开发模式,其配置需要过多人为的参与。在配置完后,当前的配置只能符合一种组件组合方式的运行,如图所示:

一般项目中的run configuration

当前可以运行app这个module,可能app module依赖于base,payplatform这两个module;

但是如果我现在想运行payplatform这个module,那么就需要手动的去改每一个module的gradle配置,然后clean project。操作完全部修改步骤之后,才能run payplatform,着实繁琐。

那么我们的目标是如下图这样的:

组件化项目中的run configuration

每个module都是不需要手动去修改配置的,选中任意一个module,根据预先配置好的module依赖关系,会自动将配置的组件打包进apk,然后运行在机器上。

每个module都可以直接run的秘密:

每一个module都会有一个build.gradle文件,其管理当前module的配置,在每一个build.gradle的开头,都有一句声明:

1
apply plugin:com.android.application / com.android.library

只要是使用application plugin的module,AS就认为这是一个可运行的module,声明为library plugin的module,AS认为它是一个库,不具备单独运行的能力,因此也就会在Run Configurations中对应的module上打上一个叉号❌。

那么我们是不是可以直接将所有module都声明使用application,就可以达到所有module全部可运行的目的呢?图样图森破。

不能这么操作的原因有二:

  1. 当一个module作为application开始build时,他要求他所依赖的其他所有module,都必须使用library plugin。不然build是无法完成的。

  2. 在build过程中,其中有一步就是merge AndroidManifest.xml,其会将所有module的manifest做一个合并,其他module作为app运行,一定会声明作为LAUNCHER的Activity,也多半会声明自定义的Application,那么在合并时就会报错,build system不知道要保留哪一个module的Application。即使build完成之后的安装,也会因为manifest中存在声明的每个module的LAUNCHER,而会在桌面上出现多个启动图标。

那么我们自定义的gradle plugin,就需要在点击run时,根据所点击的module,动态的取修改所有的module的build.gradle;大体流程如下:

Gradle的编译流程

步骤解析

当run某一个具体module的时候:

step1:

在每个module下,都有一个gradle.properties的配置文件其中声明了当前module作为独立app运行时,需要依赖的module信息:

1
2
3
isRunIndependence = true
release_dependency_module = modulea,moduleb
debug_dependency_modlue = modulea,moduleb

当在命令行下执行:

1
./gradlew :app:assembleDebug

表明当前是将app 这个module作为主module,那么其就会读取app module目录下的gradle.properties,获得app 所依赖的module信息。

step2-1,step2-2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//节选自gradle plugin
void useDebugSourceSets(Project project) {
project.android.sourceSets {
main {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
java.srcDirs = ['src/main/debug/java', 'src/main/java']
res.srcDirs = ['src/main/debug/res', 'src/main/res']
assets.srcDirs 'src/main/assets'
jniLibs.srcDirs 'src/main/jniLibs'
}
}
}
void useReleaseSourceSets(Project project) {
project.android.sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs 'src/main/java'
res.srcDirs 'src/main/res'
assets.srcDirs 'src/main/assets'
jniLibs.srcDirs 'src/main/jniLibs'
}
}
}

对应的,需要在module中建立相应的目录:

Module的组织形式

step3:
一般我们会将一些全局的初始化动作,变量在自定义的Application中进行声明。在Module作为独立App 进行build的时候,这样做是没有问题的,但是当Module作为组件被包含在主Module的代码中时,如上面提到的,为了避免merge Manifest失败,就不能再在Module自身的Manifest中声明自定义的Application了,且即使Module继续使用自定义Application,那么此时Module的Application也是一个普通的类,没有生命周期,不会被系统调用。那么Module又必须要进行一些初始化,怎么做呢?此时就要使用到android gradle1.5之后提供的Transform接口了:transform-api简介,配合transform-api就需要javaassist基本操作demo,一个能够将动态生成的代码插入到class文件的开源库,最终编译成dex,打包到apk中。

首先在每个module的build.gradle中声明,module作为app,作为组件时初始化使用的类名:

1
2
3
4
appindicator{
myRealApp = 'com.liukuai.modulea.ModuleAApplication' //独立运行时使用
myShadowApp = 'com.liukuai.modulea.app.ModuleApp' //module运行时类似Application功能的类
}

然后插入到transforms过程中的自定义Transform做如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 节选自AppTransform
* @mainClass: 主Module appindicator中声明的myRealApp
* @moduleClazz: 所有组件Module appindicator中声明的myShadowApp的集合
**/
private void injectModuleAppCode(CtClass mainClass, List<CtClass> moduleClazz, String path){
println "begin inject Module App code :${mainClass.getName()}"
mainClass.defrost()
try{
CtMethod mainCreateMethod = mainClass.getDeclaredMethod("onCreate")
StringBuilder builder = new StringBuilder()
println "begin inject Module App code 1"
//因为android.jar不在classPool的classpath搜索路径内,要使用this,就需要将android的内容添加到搜索路径中
def androidPath = getSDkJarPath()
println "androidPath:${androidPath}"
classPool.insertClassPath(androidPath)
for(CtClass moduleClass : moduleClazz){
builder.append("new " + moduleClass.getName() + "().onCreate(this);")
}
mainCreateMethod.insertAfter(builder.toString())
}catch(Exception e){
println '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$'+"happen Exception: ${e.getMessage()}"
}
mainClass.writeFile(path)
mainClass.detach()
println "end inject Module App code"
}

编译完成之后,通过反编译看一下是否奏效了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.liukuai.modulazition;
import com.alibaba.android.arouter.launcher.ARouter;
import com.liukuai.base_lib.BaseApplication;
import com.liukuai.modulea.app.ModuleApp;
public class MyApplication extends BaseApplication {
public void onCreate() {
super.onCreate();
ARouter.openLog();
ARouter.openDebug();
ARouter.init(this);
new ModuleApp().onCreate(this);
new com.liukuai.moduleb.app.ModuleApp().onCreate(this);
}
}

bingo,此时,在启动apk时,也会自动的启动Module的初始化工作。

5.总结

在每一个Module(不管是主工程,还是其他功能Module),都声明使用该gradle插件,然后在对应Module的build.gradle中,声明该模块单独运行和作为module运行时,初始化需要调用的类(都是继承自AppSimilar接口)。

在对应Module的proper.properties文件中声明,当前这个模块如果单独运行所需要依赖的其他module信息。

像base_lib这种,不是独立功能模块,直接使用android library插件,不能声明使用组件化的gradle插件。

然后选择module,直接run就可以了(如果碰到编译错误,什么R8之类的,clean一下再run)。




附1:关于Android Gradle dependencies中集中依赖方式的区别:

android dependencies api

可以看到,在android Gradle 3.0以后推出的runtimeOnly是最接近于我们对于组件化的需求的。但是其还是有些许不足,runtimeOnly虽然能隔离module间的java依赖,但是没有隔离资源,也就是moduleA可以直接调用到moduleB的layout,string,drawable等,无法实现我们期望的module间完全隔离。

附2:Android App Bundle 官方的动态插件方案

The new app publishing format, the Android App Bundle, is an improved way to package your app. The Android App Bundle lets you more easily deliver a great experience in a smaller app size, allowing for the huge variety of Android devices available today. You don’t need to refactor your code to start benefiting from a smaller app.

感觉国内的一杆插件化,要毙掉了 :P

参考文章