前言
随着公司业务发展,Android 客户端代码量逐渐增多,使用一个工程管理所有代码的模式存在代码臃肿、编译时间过长等问题,需要对 Android 客户端项目进行组件化。
基础方案
基于团队规模较小且产品迭代开发较快的现状,为了快速实现模块化并且上线,对于模块化制定的标准比较简单:剥离各业务和基础组件代码,且保持彼此独立。
代码剥离过程为划分业务模块、基础业务模块、基础组件三部分;业务模块划分主要根据公司的产品线进行,划分粒度较大,包括普通投资产品、基金两大业务模块;基础业务模块涵盖各业务模块共用的业务逻辑,包括分享模块、H5交互模块、用户模块等;基础组件划分主要为了提取与业务无关的基础组件,包括网络请求、自定义控件、基础工具类等。最后划分出的模块架构如下图:
其中业务模块除了产品线模块,还包括主页面和登录/注册;主页面模块主要包括启动页、主场景的几大 Tab 页面;登录/注册模块包括用户登录、注册、修改密码等系列页面,由于内容较独立且使用场景较广泛,均被提取成独立模块。
从依赖角度分析,业务模块属于基础业务模块的上层项目,基础业务模块属于基础组件的上层项目,上层项目可依赖下层项目,反过来则不允许。几大业务模块之间保持独立,不允许被其他业务模块直接引用;基础业务模块和基础组件均可看作普通的 library,被上层项目直接引用。
每一个模块/组件均被提取为独立项目,代码也提交至不同的 git repository 进行管理。
技术框架
基于以上方案将项目代码剥离成各模块之后,整体实现机制为:
除主模块仍然为 application module,其他模块/组件均为 library module,生成 aar 并 publish 至 maven 仓库;
业务模块通过生成路由配置文件,使用路由机制互相调用各业务模块的功能;
项目引用基础业务模块和基础组件时,直接在依赖中加入对应的aar配置即可。
业务模块之间采用路由机制的目的为保持业务模块之间的绝对独立和解耦,路由机制大致描述为:
业务模块生成对应的路由配置文件,并保存至各项目的 assets 目录中;
业务模块采用路由配置文件中的配置,通过反射调起其他业务模块中的功能。
路由配置文件生成
该步骤主要使用APT工具,实现路由配置文件的自动生成,文件格式采用 properties 格式。
首先,定义各业务模块路由文件;新建名为 RouterFilte r的 Annotation,每个业务模块新建一个 Router 类,使用 RouterFilter 注解并赋值;Router 类中的方法为该业务模块暴露给其他模块的方法,实现页面跳转等功能。
1 @RouterFilter("main") 2 public class MainRouter extends BaseRouter { 3 @RouterAction("some") 4 public void gotoSomeActivity(Context context, Bundle bundle) { 5 } 7 }复制代码
其次,自定义 APT 采用的 processor;建立 Processor 类,对项目中带有 RouterFilter注解的类进行处理,将 RouterFilter 注解的值和 Router 类的类名一一对应地保存在properties 文件中,生成的 properties 文件命名一般为:router_package_name.properties,properties 文件内容格式如下:
1 main=package.name.MainRouter复制代码
最后,由于 APT 自动生成的 properties 文件在 build/generated/source/apt/... 目录中,需要在生成 apk 或者 aar 之前将该目录作为 assets 路径的一部分。如果模块是application 项目,则在 gradle 配置文件中添加如下配置:
1 android.applicationVariants.all { variant -> 2 //这一步保证dofirst永远执行 3 variant.preBuild.outputs.upToDateWhen { false } 4 variant.preBuild.doFirst { 5 String productFlavor = variant.flavorName 6 android.sourceSets.debug.assets.srcDirs = ["assets", "build/generated/source/apt/${productFlavor}/debug/router"] 7 android.sourceSets.release.assets.srcDirs = ["assets", "build/generated/source/apt/${productFlavor}/release/router"] 8 } 9 }复制代码
如果模块是 library 项目,可添加如下较简单的配置:
1 sourceSets { 2 main { 3 android.sourceSets.debug.assets.srcDirs = ["assets", "build/generated/source/apt/debug/router"] 4 android.sourceSets.release.assets.srcDirs = ["assets", "build/generated/source/apt/release/router"] 5 } 6 }复制代码
调起其他业务模块功能
根据以上路由文件配置生成的步骤,在最后打包的 apk 中,各业务模块生成的路由properties 配置文件便都汇总在 apk 的 assets 目录中,所有业务模块均可读取到其他业务模块的配置文件。调起其他业务模块功能的机制较简单,只需提供 RouterFilter 注解的 value 和 RouterAction 注解的 value ;通过 RouterFilter 注解的 value 和 properties 配置文件可获取 Router 类的全类名,通过 RouterAction 注解的 value 可获取Router 类中的 Method 信息,采用反射即可调用该方法,从而调起指定页面或者实现其他功能。
辅助工具
模块化工作中除了实现上述技术框架,还需要实现很多便于开发的辅助性工具。
自定义 gradle 插件
模块化导致项目增多,每个项目都存在基本的版本配置信息,比如 minSdkVersion,targetSdkVersion 等;为了统一配置和方便修改,自定义 config 插件保存配置信息,在每个项目中使用插件中的变量而非具体的值。 config 插件中基本配置信息为:
1 project.customConfig.compileSdkVersion = 26 2 project.customConfig.minSdkVersion = 17 3 project.customConfig.targetSdkVersion = 26 4 project.customConfig.supportLibVersion = '26.1.0'复制代码
项目中使用配置信息如下:
1 apply plugin: 'xxx.xxx.xxx.config' 2 ... 3 android { 4 compileSdkVersion project.customConfig.compileSdkVersion 6 defaultConfig { 7 minSdkVersion project.customConfig.minSdkVersion 8 targetSdkVersion project.customConfig.targetSdkVersion 9 ... 10 } 11 ... 12 }复制代码
另外,每个 library 项目都需要 publish 到远程 maven 的操作,每个项目都进行 maven publish 的配置十分繁琐;同样地,自定义 publish 插件封装所有 publish 的配置参数,在各项目中只需 apply plugin: 'xxx.xxx.publish' 使用插件即可。
多项目管理的脚本
由于模块化项目属于不同git repository,需要批量操作多git项目;目前git多项目操作方式主要包括submodule,repo等。由于我们的模块化多项目之间彼此独立,而且同一个项目需要切换多分支,不同项目所处分支也不一定相同,因此目前多项目管理采用自定义脚本的方式。已实现所有git基本批量操作的脚本,并效仿repo封装成简单命令模式,便于使用。
在模块化实现之后的开发过程中,还有许多问题均需要批量脚本,比如某个库修改了版本号,为了防止存在兼容性问题,其他所有项目中依赖此库的版本号最好都提升为新版本号,该工作便依赖于脚本完成。
jenkins自动化
在团队开发过程中,每个成员都可能对同一个 library 进行修改,为了保证远程 maven库中 library aar 的版本是最新的,通过 jenkins 自动化实现了每个 library 模块项目push 到 gitlab 之后自动触发 maven publish 任务。在 jenkins 配置中,所有模块化项目共享同一个 jenkins job,通过 webhook 的参数即可动态获取当前 push 项目的 git ssh url 以及 branch 等。
实践总结
目前模块化稳定使用了一段时间,可大致总结出如下优点:
由于不同模块/组件存在上下层级,可一定程度对代码进行解耦;
降低了主项目的编译时间;
可提取越来越多的基础组件,供公司的其他 Android 项目使用;
业务模块剥离,便于应对产品线的下线;
模块/组件开发过程可比较独立,在各自工程中进行调试即可,不需要在主工程调试。
当然由于项目数量增多,开发过程相对于单项目操作存在以下缺点:
模块/组件开发完成后,需要执行 publish 到本地或者 publish 到远程的操作才能使用最新的模块/组件逻辑;当然也可以在 settings.gradle 中配置其他项目作为 module,便无需进行 publish 操作;
app 打包时需要确保使用的模块/组件均是最新的,步骤没有单项目模式时简单。
后续工作中除了优化以上开发过程中遇到的缺点以及当前技术框架,还需对模块进行不断梳理,划分较细的模块可以合并,而随着业务发展逐渐庞大的模块则应该细分,提高整体项目的灵活度。
作者简介
布恩,铜板街 Android 开发工程师,2016年4月加入团队,目前主要负责 APP 端 Android 日常开发。