组件化概述
问:什么是组件,什么是组件化?
答:在软件开发领域,组件(Component)是对数据和方法的简单封装,功能单一,高内聚,并且是业务能划分的最小粒度。举个我们生活中常见的例子就是电脑主板上每个元件电容器件,每个元件负责的功能单一、容易组装、即插即拔,但作用有限,需要一定的依赖条件才可使用。
问:组件化、模块化容易混淆,两者区别又是什么?
答:模块化就是将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块我们相对熟悉,比如登录功能可以是一个模块,搜索功能可以是一个模块等等。而组件化就是更关注可复用性,更注重关注点分离,如果从集合角度来看的话,可以说往往一个模块包含了一个或多个组件,或者说模块是一个容器,由组件组装而成。简单来说,组件化相比模块化粒度更小,两者的本质思想都是一致的,都是把大往小的方向拆分,都是为了复用和解耦,只不过模块化更加侧重于业务功能的划分,偏向于复用,组件化更加侧重于单一功能的内聚,偏向于解耦。
问:组件化能带来什么好处?
答:简单来说就是提高工作效率,解放生产力,好处如下:
- 代码简洁,冗余量少,维护方便,易扩展新功能。
- 提高编译速度,从而提高并行开发效率。
- 避免模块之间的交叉依赖,做到低耦合、高内聚。
- 引用的第三方库代码统一管理,避免版本统一,减少引入冗余库。
- 定制项目可按需加载,组件之间可以灵活组建,快速生成不同类型的定制产品。
- 制定相应的组件开发规范,可促成代码风格规范,写法统一。
- 系统级的控制力度细化到组件级的控制力度,复杂系统构建变成组件构建。
- 每个组件有自己独立的版本,可以独立编译、测试、打包和部署。
实践难点
组件间通讯
常见组件通讯方式
本地广播,也就是LoacalBroadcastRecevier。更多是用在同一个应用内的不同系统规定的组件进行通信,好处在于:发送的广播只会在自己的APP内传播,不会泄漏给其他的APP,其他APP无法向自己的APP发送广播,不用被其他APP干扰。本地广播好比对讲通信,成本低,效率高,但有个缺点就是两者通信机制全部委托与系统负责,我们无法干预传输途中的任何步骤,不可控制,一般在组件化通信过程中采用比例不高。
进程间的AIDL。这个粒度在于进程,而我们组件化通信过程往往是在线程中,况且AIDL通信也是属于系统级通信,底层以Binder机制,虽说Android提供模板供我们实现,但往往使用者不好理解,交互比较复杂,往往也不适用应用于组件化通信过程中。
匿名的内存共享。比如用Sharedpreferences,在处于多线程场景下,往往会线程不安全,这种更多是存储一一些变化很少的信息,比如说组件里的配置信息等等。
Intent Bundle传递。包括显性和隐性传递,显性传递需要明确包名路径,组件与组件往往是需要互相依赖,这背离组件化中SOP(关注点分离原则),如果走隐性的话,不仅包名路径不能重复,需要定义一套规则,只有一个包名路径出错,排查起来也稍显麻烦,这个方式往往在组件间内部传递会比较合适,组件外与其他组件打交道则使用场景不多。
主流方式
引入BaseModule放置所有对外接口,组件层的模块都依赖于基础层,从而产生第三者联系,这种第三者联系最终会编译在APP Module中,那时将不会有这种隔阂,那么其中的Base Module就是跨越组件化层级的关键,也是模块间信息交流的基础。
缺点
这种方式的问题在于因为所有的业务组件对外接口都定义在BaseModule中,所有业务组件都依赖BaseModule,那么无论是修改还是新增变动都会涉及到整个项目层面。并且无论Module 2 组件是否使用到Module 1组件的对外功能都会引入Module 1 组件所有的对外接口,增加了业务组件代码之间的关联性,模糊了各业务组件的职责边界。
优化方式
每个组件声明自己提供的服务 Service API接口,声明完成后抽取API到独立组件中。组件业务层依赖API层,并实现接口功能并注册 Service 实现到一个统一的路由 Router 中去。如果要使用某个组件的功能,只需要依赖该组件API层后向Router 请求这个 Service 的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。
实践原理
步骤
- 读取 组件配置信息
- 创建 Service API 环境
- 独立 Service API 于业务组件目录
- 同步时打包 Service API 为单独jar包
- 开发时使业务组件依赖 Service API jar包
- 发布时业务组件aar包和Service API jar包单独发布
实践过程
1. 定义组件配置信息
完整版 modular.gradle 文件
modular {
packageName "com.ydl.other"
// 模块发布需要的参数
publish {
modules {
//发布信息
groupId = "com.ydl"
artifactId = "m-other-module-xxxx"
// 上报的业务模块 aar 包的版本号
version = "0.0.1"
}
api {
//发布信息
groupId = "com.ydl"
artifactId = "m-other-api"
// 上报的 API 层 aar 包的版本号
version = "0.0.1"
// API 层打包时需要引入的依赖
apiDependencies {
implementation "com.google.code.gson:gson:2.8.2"
}
}
}
}
2. 创建 Gradle Plugin 读取配置信息
项目结构
核心代码
//默认引入项目目录下的modular配置文件
project.allprojects.each {
if (it == project) return
Project childProject = it
def modularScript = new File(childProject.projectDir, 'modular.gradle')
if (modularScript.exists()) {
modularExtension.childProject = childProject
project.apply from: modularScript
}
}
3. 生成组件发布任务
-
接入maven-publish 插件,发布jar/aar到私库
project.plugins.apply('maven-publish')
-
生成任务信息实例
核心代码
String displayName = project.getDisplayName() //去除单引号 publication.project = displayName.substring(displayName.indexOf("'") + 1, displayName.lastIndexOf("'")) def buildMis = new File(project.projectDir, 'build/modular') publication.sourceSetName = publication.name publication.buildDir = new File(buildMis, publication.name) SourceSet misSourceSet = new SourceSet() def modularDir if (publication.sourceSetName.contains('/')) { modularDir = new File(project.projectDir, publication.sourceSetName + '/modular_api/') } else { modularDir = new File(project.projectDir, 'src/' + publication.sourceSetName + '/modular_api/') } misSourceSet.path = modularDir.absolutePath misSourceSet.lastModifiedSourceFile = new HashMap<>() project.fileTree(modularDir).each { if (it.name.endsWith('.java') || it.name.endsWith('.kt')) { SourceFile sourceFile = new SourceFile() sourceFile.path = it.path sourceFile.lastModified = it.lastModified() misSourceSet.lastModifiedSourceFile.put(sourceFile.path, sourceFile) } } publication.modularSourceSet = misSourceSet publication.invalid = misSourceSet.lastModifiedSourceFile.isEmpty()
-
生成发布Task
核心代码
def flavorName = "" if (publication.name != "module") { flavorName = ModularUtil.captureName(publication.name) } AbstractArchiveTask bundleReleaseAar = project.getTasks().findByName("bundle${flavorName}ReleaseAar") if (bundleReleaseAar != null) { mavenPublication.artifact(bundleReleaseAar) } //添加业务组件依赖关系至POM文件 mavenPublication.pom.withXml { // for dependencies and exclusions def dependenciesNode = asNode().appendNode('dependencies') project.configurations.implementation.allDependencies.withType(ModuleDependency) { ModuleDependency dp -> if (dp.name!=null&&dp.version!=null){ def dependencyNode = dependenciesNode.appendNode('dependency') dependencyNode.appendNode('groupId', dp.group) dependencyNode.appendNode('artifactId', dp.name) dependencyNode.appendNode('version', dp.version) // for exclusions if (dp.excludeRules.size() > 0) { def exclusions = dependencyNode.appendNode('exclusions') dp.excludeRules.each { ExcludeRule ex -> def exclusion = exclusions.appendNode('exclusion') exclusion.appendNode('groupId', ex.group) exclusion.appendNode('artifactId', ex.module) } } } } }
使用
任务列表
组件目录
接口定义
/**
* Created by haorui on 2019-10-10.
* Des:
*/
public interface UserService extends IProvider {
UserInfo getUser();
}
接口实现
/**
* Created by haorui on 2019-10-10.
* Des:
*/
@Route(path = "/user/UserService")
public class UserServiceImpl implements UserService {
public UserServiceImpl() {
}
@Override
public UserInfo getUser() {
return new UserInfo("from user");
}
@Override
public void init(Context context) {
}
}
依赖
compileOnly 'com.ydl:m-user-api:0.0.3'
调用
UserService mUserService = ARouter.getInstance().navigation(UserService.class);
mUserService.getUser();
总结
本文中说明的方案在实践过程中调研了现有市面上流行的组件化架构,并在其基础上使用Gradle Plugin进行优化。降低项目业务组件之间的不必要的耦合依赖,节约接口层升级所带来的维护成本,并封装统一上传Task,避免了多次重复工作。
感谢以下极客的无私分享
斜杠Allen -- Android组件化框架设计与实践