1 背景
随着项目的增加,弊端也随之而来:
- 不同的App之间有大量的重复功能
- 代码之间耦合太严重,改了一部分代码却影响到了其他不相关的代码
- 第三方开源框架版本不统一而且不同的app要开启不同的工程,开发起来极其麻烦
- 项目编译起来时间越来越长
2 整体框架
针对以上问题对项目进行了重构。将通用功能封装起来、根据不同业务分为不同的组件以及用gradle统一进行项目构建。如下所示:
整体的架构分为三层,越底层变动的频度越低:
基础层:主要是开源的第三方开源框架或者第三方SDK。这些基本是不会改动的,除非有特殊需求。
封装层:这边分为两部分。第一部分是将架构封装成基类,使用的时候只要继承这些基类既可。这样子的好处是将大量的通用代码下沉到基类中,业务代码只需关系具体逻辑即可。第二部分是功能模块,相当于一个功能池。有新的功能就不断往里去添加。如果新需求且功能已经实现,直接使用即可。
业务层:所谓业务组件就是跟业务紧密相关的,也是变化最频繁的一层。这边分为两块一个是业务组件一个是宿主。分成两块主要时目前是旧项目改造,宿主组件是目前的旧项目,而业务组件是提取出的通用组件。组件之前是相互独立,而且各个组件都可以生成app独立测试。
Gradle工具:第一是统一版本,比如说第三方SDK的版本,kotlin插件的版本等都可以在这里定义。规避了旧项目中各个App版本各异杂乱无章的问题。第二是Gradle可以设置在同一个工程里同时导入多个app。最后一个功能就是生成动态库提供给第三方调用。
以下是具体的工程结构:
结构里的每一栏都是一个文件夹,library-files是必须导入的文件夹其他可按需选择不互相依赖。功能模块以及登陆组件会编译生成依赖库放置于文件夹library-files中供其他项目调用。组件调用功能模块的方式是导入library-files中提供的依赖库并直接使用其中的接口。
3 具体分析
3.1 组件之间的通信
这边使用的Arouter开源框架,他实现了独立的不同组件之间的通信。如下图所示:
3.1 .1 优势
这边不同组件可以看成不同的app,因为他们可以独立运行。这样做的优势一点是可以独立测试,一个是可以并发开发。比如说以下的例子:
比如说目前某App是拆出了一个登陆模块出来。因为登陆模块在其他的应用中也是可以调用的,属于可以共用的模块。这边的登陆模块是可以生成登陆app来独立运行测试的,这里的优势是他的测试并不用全部编译整个某App。这种方式速度快而且不受其他逻辑干扰,同时方便回归测试。登陆模块可以专门交付给工程师维护,而某宿主模块交于其他工程师开发,那么就实现了并发开发。
这个是它的优势,现在说说具体的操作逻辑。
3.1 .1 具体操作
宿主模块刚进入app,cookie过期以及退出登陆都会跳转到登陆模块,而登陆模块登陆成功之后会跳转至宿主模块。下面是具体代码:
@Route(path = "/sample/LoginSuccess") //这个是登陆成功的注解
@Route(path = "/StationLogin/Main") //这个是登陆界面的注解
ARouter.getInstance().build("/sample/LoginSuccess").navigation() //跳转至登陆成功后的界面
ARouter.getInstance().build("/StationLogin/Main").navigation() //跳转至登陆界面
ARouter.getInstance().build("/StationLogin/Main")
.withString("errorInfor","Cookie过期,请重新登陆")
.navigation();//带有参数的跳转
在宿主模块的登陆成功后所要跳转的界面上加上登陆成功的注解,在登陆模块调用navigation()即可实现从登陆模块到某模块的跳转。某模块到登陆模块的跳转也是同样的道理。同时网络请求是作为一个依赖库的形式被某模块调用,所以我们可以直接在网络请求中拦截cookie并检测出过期轧出跳转请求,同样也是可以方便地跳转至登陆模块。
3.2 功能的封装
功能封装也就是将一个个通过的功能封装起来,对外提供接口调用,下面以OTG通信以及串口通信来举例子:
这个是串口以及OTG通信的代码,之前的做法是直接复制到工程中使用。这样做的缺点有两个:
-
复制粘帖这些代码会使得工程充斥了大量与业务逻辑不相关的代码,使得项目变得杂乱。
-
如果有串口通信有bug,那么就需要每个粘帖的地方都去改动。工程量大而且难以做到。
现在的改造方式是将其内部逻辑封装起来禁止调用,并对外提供接口最后生成aar依赖库。aar库是编译好的class文件,所以第三方工程引入的时候不会再次编译,从而减少了编译时间。
下面是封装好的串口调用接口:
SerialInteract.singleton
.addSerial("/dev/ttyS3","9600")
.addSerial("/dev/ttyS4","9600")
.addSerialByKey("ttyACM","115200")
.SetListener(object: SerialInteract.OnDataReceive {
override fun OpenSerialSuccess(dev: String,errMsg:String) {
//TODO
}
override fun DispRecData(ComRecData: ComBean) {
//TODO
}
})
.Start()
使用者只需导入aar依赖库,并调用该接口即可。源码中的内部代码是禁止调用的。如果后续有功能改动也尽量在这些对增加外接口并保持原有的接口不变。
相对来说这种方式代码量少了许多而且逻辑清晰。
3.3 依赖库的生成
依赖库项目要引入的第三方库,这个重构里会把功能模块以及组件都封装成的依赖库。在gradle中增加以下代码:
publishing {
publish.dependsOn(deletPath)
publish.dependsOn(assembleRelease)
publications {
maven(MavenPublication) {
groupId 'sample'
artifactId 'network'
version '0.0.1'
artifact "$buildDir/outputs/aar/network.aar"
pom.withXml {
def dependenciesNode = asNode().appendNode('dependencies')
configurations.implementation.allDependencies.each { dependency ->
if(!"unspecified".equals(dependency.name) && !"unspecified".equals(dependency.version)){
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', dependency.group)
dependencyNode.appendNode('artifactId', dependency.name)
dependencyNode.appendNode('version', dependency.version)
}
}
}
}
}
repositories{
maven {
url "$rootDir/../library-file"
}
}
}
这个便会编译源码在指定文件夹编译生成依赖文件,这样就将功能的实现与使用分开了。
目前是封装了8个功能依赖库以及一个模块依赖库。在项目中只需一行代码即可使用这些封装好的依赖库。
gradle的另一个用处是它可以统一使用的版本,如下所示:
ext {
android = [
compileSdkVersion: 28,
buildToolsVersion: "28.0.3",
minSdkVersion : 24,
targetSdkVersion : 28,
versionCode :1,
versionName : '0.0.13',
]
dependencies = [
appcompatV7 : 'androidx.appcompat:appcompat:1.0.0',
design: 'com.google.android.material:material:1.0.0',
fresco: 'com.facebook.fresco:fresco:1.3.0',
rxjava: 'io.reactivex.rxjava2:rxjava:2.1.2',
...
}
比如说某app,那么我们项目中只需以下部分:
业务逻辑(宿主组件)+登陆依赖库文件(登陆组件)+网络依赖库文件(提供接口)+工程外壳(Gradle)
4 未来展望
优化Gradle逻辑,进一步优化编译的效率
学习并引入jetpack,mvvm等心技术点