方案设计背景(前面都是废话)
初期Android项目构建方式基本为分层结构设计,由于初期项目较小这种架构简单,清晰因此沿用至今。而当项目业务逐渐扩大时发现简单的分层结构已经无法满足现有项目架构,尤其是多人开发时各个业务之间沟通成本成指数上升。 应运而生的模块化思想诞生,初期大家只是将项目的业务进行简单拆分成各个module,以主module去引用子module。
后来发现这种方式隔绝了子module之间的通信,且主module只能主动的引用子module,无法被子module访问。随之而来进入主题了--组件间如何去通信
组件间通信方式
目前最常用的通信方式有Event、协议、RPC接口三种主要形式,但实际上都只是协议的一种变形封装而已。从标题能看出我参考了微信的模块化通信我就不墨迹,我更倾向于微信的RPC形式的通信方式。 原因有以下几点:
- 协议以接口形式展现能避免协议格式以及字段的维护
- 双方开发时省却协议解析步骤,避免解析过程中错误概率
- 将接口以aar形式对外开放,能极大节省双方接入以及后续维护成本
方式已经确定那接下来就是解决实现上的难题,从上述原因中我们可以看到几个关键点:
- 如何暴露接口
- 如何生成接口aar
- 如何注册并初始化接口实现类
首先我们来说说如何生成接口aar,大家都知道aar对应的是一个独立的module,那如何自动生成该module? 从微信的实现上看它是为setting新增了include_with_api方法,并修改了gradle指向的setting实现类,从而在初始化setting类时自动生成接口module。 由于个人研究时间有限目前采用取巧方案,在setting.gradle文件中新增include_with_api方法用来自动生成接口module,而build.gradle和AndroidManifest.xml文件皆从实现module中拷贝过来,并修改AndroidManifest.xml中的package以及去掉android:label信息,再去掉原有build.gradle的接口module依赖,从而顺利解决接口module生成方式。
def include_with_api(def projectName) {
include projectName
String rootDir = rootDir.getAbsolutePath();
String moduleName = ((String) projectName).replace(":", "")
String parentName = moduleName.replace("plugin-", "");
copy() {
from rootDir + '/' + parentName + '/build.gradle'
into rootDir + '/' + moduleName + '/'
filter { line ->
String content = line;
if (content.contains(moduleName)) {
content = "";
}
content
}
}
copy() {
from rootDir + '/' + parentName + '/src/main/AndroidManifest.xml'
into rootDir + '/' + moduleName + '/src/main/'
filter { line ->
String content = line;
content = content.replace("android:label=\"@string/app_name\"", "")
if (content.contains("package=\"")) {
content = content.replace("\">", ".plugin\">")
}
content
}
}
}
复制代码
再来看下关于接口暴露的问题,其实接口暴露方案主要还是参考微信只不过中间实现细节是否一样就不可知了。 下面着重介绍下我的细节实现,从微信方案中可以看出是将接口文件从.java后缀改成.api,然后将.api后缀的文件拷贝至接口module中。通过gradle插件来定制依赖配置项compileApi,实现module通过compileApi依赖关联接口module,在解析中将实现module中接口文件拷贝至接口module并修改后缀名从而通过编译。
Configuration configurationtest = project.configurations.create("compileApi")
configurationtest.canBeResolved = true;
configurationtest.canBeConsumed = true;
configurationtest.setVisible(false)
//当该依赖配置被解析时会执行
configurationtest.allDependencies.all {obj->
moveFile(project, obj.name)
project.dependencies.add("compile", project.project(':' + obj.name))
}
复制代码
接下来我们可以考虑如何注册以及初始化接口实现类,所谓的注册也就是如何通过接口找到实现类并隐式实例化。
@AService("com.netease.add.Add")
public abstract class IAdd extends IService{
abstract int add(int a, int b);
}
public static void register(Class clazz) {
try {
AService aService = clazz.getAnnotation(AService.class);
if (aService == null) {
return;
}
String clazzName = aService.value();
if (!TextUtils.isEmpty(clazzName)) {
T t = (T) Class.forName(clazzName).newInstance();
ServiceManager.getInstance().put(clazz.getSimpleName(), t);
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
复制代码
考虑到接口的局限性,我通过注解形式将实现类信息传递给接口。至于具体的初始化环节我就不搬过来了,大家可以参照微信的方式自由发挥。 在注册这个点上还有很多其他方案比如aop/ioc等,大家有兴趣的话可以去研究并不难。
最后
上述只是我对微信模块化通信环节的实现,从而打通了整个环路。至于项目的模块化改造还得依情况而定,从思维角度来讲要遵循大事化小,如果本身就是个小项目就没必要引入繁琐的模块化架构。模块化不单单只是说说以及架构环节,还涉及如何划分模块边界等细节问题。 最后希望我们分享的一点经验能对大家有些价值。