目前有赞移动端的主要工作内容是在“有赞微商城”和“有赞零售”两条公司主要的业务线,随着有赞 Saas 业务的增长,客户端也不断迭代,支持越来越多的功能。
在这个业务快速增长的情况下,移动端技术的整体架构也是一直在不断调整,来保证开发效率和业务的快速迭代。
这篇文章,主要是介绍有赞微商城 Android 组件化的一些思路和实现。
客户端的架构,从一开始的“All IN ONE” 模式(即所有代码都在 App 中),逐渐演变到目前的一个单 Project 多 Module 结构:
新的项目架构,也带了了新的问题:
我们之前虽然有在做整个工程模块化的开发,但是目前的模块化框架可以说是不够彻底的:
为了解决以上的问题,我们需要对现有的架构进行调整。
将模块的功能抽象出一些基础类,组成了模块化支持组件,它提供的功能有:
跟很多客户端的同学聊过,很多 APP 发展到一定阶段之后,必然会诞生一个所谓的 Common 模块。它就像一个大储物柜,每个人都把一些其他人可能用到的东西一股脑儿塞进去。
这么个塞法,会有两个问题:
“服务化”这个词,在服务端的开发中经常被提到,简单来说,就是根据业务划分为多个模块,模块之间的交互以互相提供服务的方式来完成。
而客户端随着业务模块的增多,也必然存在业务模块之间存在业务依赖的情况,而 Android 端常规的模块依赖的方式有:
对外暴露服务的方式有很多种:
协议的方式的问题:如果服务提供的地方更改了之后,需要手动去查询所有调用到的地方,进行更改,而且没有版本管理,而且数据解析都需要手动进行转换,改动的成本比较高,也有一定稳定性风险。
接口的方式的问题:需要额外提供一个依赖(单独把 API 层打包成一个 aar 包),使用方需要添加 Mave 依赖,所以引入依赖和发布的成本比较高。
我们最终选择了接口的方式,这种方式的稳定性和版本控制做的更好,对于改动来说,编译过程自动会帮你校验改动的影响面,而引入依赖和发布成本高的问题,完全可以交给构建工具(Gradle Plugin)来解决。
业务实现层需要做的,就是实现自己模块本身的业务逻辑,并实现自己提供的 API 接口,暴露对外的服务。
项目中现在有很多的基础组件都是统一在 Common 里面进行封装的,例如:账号库、网络库、图片加载、Web 容器库等等,这也带来了一些问题:
随着业务量和业务复杂度的增长,还有多个三方组件的引入,客户端工程代码量也变得越来越庞大,直接造成的一个问题是:打包慢!一个简单的场景:当你开发了一个商品模块内部的功能之后,你需要打整个 App 的包才能进行测试,而打一个包的时间可能是 5~10 分钟,如果一天打包 10 次,也是比较酸爽。我们的组件也需要支持单模块或者选定的某些进行打包,其中的思路也是通过自定义 Gradle Plugin 在编译阶段,动态去更改 Module 实际依赖的 Android Gradle 插件来实现的。
经测试,同一台电脑,完整打包(clean之后再安装)耗时 4 分钟,而单模块打包(同样也是 clean 之后安装)耗时 1 分钟,整体打包时间降低了 70% 以上。
上面的一些改进点,总结成一张图,就是这样的:
目前我们的方案提供 3 个基础组件依赖和 1 个 Gradle 插件:
业务模块类需要继承 BaseModule:
public class ModuleA extends BaseModule {
@Override
public void onInstalled() {
registerBusinessService(ModuleAService.class, new CachedServiceFetcher() {
@Override
public ModuleAService createService(@NotNull ModularManage manager) {
if (service == null) {
service = new ModuleAServiceImpl();
}
return service;
}
});
}
}
模块有以下几个生命周期:
其实组件内关于生命周期捕获和监听,都是借助于 Google 的 Android Architecture Components 中的 Lifecycle 库来实现的。
这里需要依赖对于 Android 的构建工具 Gralde 的扩展,它支持的高度可扩展特性,帮助我们在组件化开发中更加高效,不需要关系一些额外的工作,只需要关注开发的内容即可,对现有的代码逻辑基本没有侵入。
这里必须要提一些的就是 Gradle 的生命周期,因为我们的很多扩展功能,都是在对 Gradle 执行的生命周期的各个阶段做一些改动来实现的,大概的生命周期如图:
Android 打包成 Apk 并运行的条件有:
将以下配置添加到模块目录下的 build.gradle 文件中
modular {
// 模块包名
packageName = "com.youzan.ebizcore.plugin.demoa"
app {
// 单模块打包开关
asApp = true
// 运行的 App 的名称
appName = "Module A"
// 入口 Activity
launchActivity = "com.youzan.ebizcore.plugin.demoa.ModuleAActivity"
// 配置只在单模块打包时需要引入的依赖
requires {
require "com.squareup.picasso:picasso:2.3.2"
}
}
}
运行 modular 的 createApp Task,就会自动生成需要的类(以 module_a 为例)
自动生成的文件目录结构:
./module_a
--src
----main
------app # 自动生成 app 目录
--------java # 自动生成 Application 类
--------res # 自动生成资源
--------AndroidManifest.xml # 自动生成 Manifest 文件
运行 modular 的 runAsApp Task,模块就会被单独达成一个 apk 包,并安装到你的手机上,如果模块有上下文依赖(比如登录)的话可以额外提供依赖,加到模块的 app 的 requires 中。
这里的打包执行是在 build 目录下生成了一个打包脚本,并调用 Gradle 的 API 执行脚本来实现打包安装的。
模块 API 层提供的接口和数据结构代码是可以直接在模块内部被引用到的,方便开发,但是在暴露给外部的模块时候的时候是需要打包成 aar 上传到 Maven 来提供的,Modular-Plugin 分别针对这两个步骤提供了两个 Task,方便开发者快速进行开发和发布。
modular {
packageName = "com.youzan.ebizcore.plugin.demoa"
// 模块 API 支持相关参数
api {
// 是否需要提供 API 支持的开关(会影响到是否可以运行自动生成代码的 Task)
hasApi = true
// 对外提供的 API Service 类名
apiService = "ModuleAService"
// API 层的依赖
requires {
require "com.google.code.gson:gson:2.8.2"
}
}
}
运行 modular 的 createApi Task,就会自动生成需要的类(以 module_b 为例)
./module_b
--src
----main
------service # 自动生成 service 目录,用来存放对外接口和数据对象
--------java # 自动生成 Application 类
--------AndroidManifest.xml # 自动生成 Manifest 文件,为了单独打成 aar 包
发布功能内部使用了 'maven-publish' 插件来进行依赖的上传,开发者只关心上报的配置就好
modular{
// 模块发布需要的参数
publish {
// 是否打开模块发布
active = true
// 上报地址,支持本地路径和远程 Mave 仓库地址
repo = "../release"
groupId = "com.youzan.ebizmobile.demo"
artifactId = "modular-a"
// 上报的业务模块 aar 包的版本号
moduleVersion = "0.1.4"
// 上报的 API 层 aar 包的版本号
apiVersion = "0.1.5"
// Maven 登录名和密码,可以从 local.properties 中取
userName = ""
password = ""
}
}
运行 modular 的 uploadModule Task,Module-Plugin 会执行打包上传的任务,执行顺序是这样的:
1. 首先打包并上传 Module 的 API 模块(SourceSet 只包含 API 的类) 2. 将 Module API 的代码从模块的 SourceSet 中去除,并添加刚才上报的 API 模块的 Maven 依赖到 Module 的 dependencies 中
以图片组件为例,一般业务模块中使用到的图片相关的功能有:图片加载、图片选择等,可以把这些功能抽象成接口
interface IImageLoadSupport {
fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, imgUrl: String)
fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, @DrawableRes drawableId: Int)
fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, imgUrl: String, callback: ImageLoadCallback<IMAGE>)
fun imagePicker(activity: Activity?, selectedImgUris: List<Uri>)
fun onImagePickerResult(requestCode: Int, resultCode: Int, intent: Intent?): List<String>?
}
基础组件的实现可以在 App 中进行注册,如果需要单模块组件中使用 Support 相关功能,可以提供一套默认实现,在但模块运行时引入,在全局有一个 Support 注册中心,以 Map 的形式维护运行中的 Support 对象:
fun <SUPPORT : Any, SUPPORTIMPL : SUPPORT> registerProvider(supportCls: Class<SUPPORT>, provider: SupportProvider<SUPPORTIMPL>) {
synchronized(Lock) {
supportsProviderMap[supportCls] = provider
if (supportsMap.containsKey(supportCls)) {
supportsMap.remove(supportCls)
}
}
}
开发到现在,这边的三个组件已经能够基本完成我们对于组件化核心需求,但是,也是有一些方向可以进一步优化整套方案的使用:
组件化的道路千万条,项目的架构也是在不断得调整优化,来达到提升团队开发效率,保证项目稳定性的目的。以上的这些想法和在实际项目中的一些方案,希望能给正在进行模块化探索的同学提供一些灵感。