提前锁票 InfoQ 最具价值感的视频栏目 | InfoQ 大咖说 了解详情
写点什么

从智行 Android 项目看组件化架构实践

2019 年 5 月 08 日

从智行 Android 项目看组件化架构实践

一、前言

智行火车票早期以火车票业务起步,随着整体的业务发展和扩张,先后增加了汽车票、机票和酒店模块,逐渐打造成了一个提供出行、旅行和住宿一站式预订服务的 OTA 平台。


在业务扩张过程中,之前 Android 项目单一工程的架构模式慢慢暴露出一些问题,例如业务间耦合较多,整体项目编译耗时等,渐渐无法满足业务开发需求。


为了解决面临的问题,综合主流的 Android 项目架构方案,团队选择了组件化架构方案对项目进行了调整和实践,抽离出基础组件库、独立的业务模块,实现了各独立业务的拆分和独立运行,可以单独进行需求开发,发版时再合并到一起编译打包和发布。


在组件化架构实践过程中,团队解决了组件化调整中遇到的一些难题,对组件化技术在 Android 项目中的应用有一定的参考价值和实践经验。同时根据业务需求,还实现同一个项目进行多个应用差异化适配打包的功能,便于开发和维护团队旗下的其他应用。


二、概述

本文主要根据智行 Android 团队在组件化架构调整中的实践过程以及最终的实践成果,从以下几个方面来进行阐述:


  • 为什么要进行组件化架构调整

  • 组件化结构调整的实施步骤

  • 组件化调整过程中遇到的难题以及解决方案

  • 组件化架构调整的成果


2.1 组件化调整的原因和目标

如前面提到的,在调整之前,项目是单一工程的架构模式,这也是常见的 Android 项目架构模式,但是一旦项目整体业务增多,扩张出相对较为独立的业务模块,这种架构就会带来一些问题,例如:


  • 业务间代码层面耦合太重,业务之间隔离不明确:由于各业务间代码存在较多的耦合,经常出现某个业务线功能开发迭代影响其他业务线,出现代码冲突,影响其他业务功能。

  • 项目整体源码较多,编译耗时久:各业务开发人员主要开发各自业务线需求,但是需要编译整个项目,耗时较多,影响开发效率。

  • 多应用差异化适配方案不完善:在业务扩张过程中,还衍生出一些独立应用,例如智行旗下的订票助手、智行机票等应用,实际是使用同一个项目打包,更改一些主题配色和首页入口,进行差异化的编译打包。之前使用的多应用打包方案存在一些的问题,逐渐无法满足实际需求。


参考技术社区的 Android 架构方案,以及结合项目实际情况和业务场景,我们选择了组件化方案来进行架构的调整。


Android 项目组件化,最早是冯森林老师在 2016 年 MDCC 大会上的《回归初心,从容器化到组件化》演讲中提出来的,当时该方案刚提出,实际应用到项目中的还是比较少得,毕竟一般的公司项目业务不是很复杂,项目结构也是较为单一,没有使用组件化的必要。


但对于此时的智行 Android 项目而言,正是组件化架构最适合实践的项目,多个业务线,项目整体比较庞大,业务间不必要的耦合过多,因此组件化架构的调整方案也就应运而生。


在进行调整之前,团队也定下了调整预期的目标:


1)业务解耦,使得各业务模块可以独立运行,同时可以组合编译打包


2)拆分基础组件,抽离出基础组件 Library


3)各业务间通信和业务交叉调用的实现


4)实现多应用差异化适配打包


以上大的目标点主要是来解决之前遇到的问题,也是项目架构调整的首要目的。


2.2 组件化架构调整的整体规划

2.2.1 基础组件的拆分

智行 Android 项目的基础组件主要分为业务基础组件和功能基础组件,其中业务基础组件包含登录组件、自定义 View 组件、项目网络层组件等,这些和业务有关联,提供给各业务模块的基础组件,根据具体情况拆分成 aar 或者 library,像登录,基础网络层这样较为稳定的组件,一般直接打包成 aar,减少编译耗时。而像自定义 View 组件,由于随着版本迭代会有较多变化,就直接以源码形式抽离成 Library。


基础组件的调整相对较为简单,主要就是按照功能或者业务拆分成 Library ,处理好之前的引用的地方即可,但是对于拆分出来的 Library 的质量和后续维护工作是要求相对较高的,作为基础的组件,是需要为各业务模块提供基础的功能的,重要性是相对较高的。


基础组件库的编译版本设置一般是和主工程同步的,为了方便后续升级和维护配置,可以使用如下的方式来实现 library 使用同一份配置:


ext.libDefaultConfig = {    minSdkVersion 19    targetSdkVersion 25    javaCompileOptions {      annotationProcessorOptions {        includeCompileClasspath = true      }    }}
复制代码


定义一个通用的 DefaultConfig 配置,设置统一的 SDK 版本信息和编译选项,在 Library 的 build.gralde 文件中使用如下方式即可应用到配置:


android {      ...    defaultConfig libDefaultConfig}
复制代码


这样既可以保证基础组件库的编译配置统一,也方便后期统一修改和升级。


对于再基础点的组件,例如 Support Library、json 库等,绝大多数基础组件都会使用到。为了避免每个独立基础组件都去引入对应的依赖,还要尽可能得保证版本的统一,我们使用了一个空壳 Library 来一次性引入这些基础的依赖组件。


这个 Library 叫做 BaseDependencies,然后其他的基础组件去依赖 BaseDependencies,这样就可以保证基础组件对这些基础的依赖版本做到一致,后续的升级改动位置也相对较为集中。如此调整后的依赖如下图所示:



当然这样的也有一定的弊端,就是每个基础组件都可能存在引入冗余的依赖,对于后续可能需要提供给第三方的基础组件,还需要进行改动才可以独立出来。


2.2.2 业务模块的拆分和配置

业务模块的调整是组件化中最重要的,这里的模块也是组件,对于这类组件的调整目标就是做到能够独立运行。业务模块的调整主要分两步:


1)业务模块的拆分独立


2)业务模块的配置


由于之前各业务的代码以不同包名来进行区分,各业务代码也是比较集中的,拆分出来还是相对较为容易的。遇到两个业务模块都会使用到的类的话,就将对应的类下沉到 Base Module,当然这种情况也是尽可能去避免,否则 Base Module 会越来越臃肿,如果不加以控制,那么业务模块就变成了一个空壳,失去了组件化原本的意义。


拆分成独立模块业务,彼此之间是平级的关系,无依赖关系,从而从结构层面达成了分离的目的,避免了之前一不小心就出现的类相互引用,耦合严重的问题。



业务模块拆分成独立的 Library 以后,就是对其进行配置,这也是组件化的关键步骤,既要使得各个业务模块可以独立运行,又要保证各个模块作为整体 App 的一部分,关键就在于不同场景下给每个业务模块应用不同的插件类型。


独立运行时,需要使用:


apply plugin: 'com.android.application'

复制代码


而分独立运行时,则需要使用:


apply plugin: 'com.android.library'

复制代码


为了方便业务模块的在这两种模式下的快速切换和统一调整,我们使用了以下的设置方式:


// 项目的 build.gradle 中配置模块是否独立运行def isSingleCompile = falseext.isSingleCompile = isSingleCompile
if (isSingleCompile) { ext.COMPLIEMODE = 'com.android.application'} else { ext.COMPLIEMODE = 'com.android.library'}
// 业务模块的 build.gradle 中应用apply plugin: COMPLIE_MODE ```
复制代码


这样在切换时,只需要修改 isSingleCompile 的值,就可以在独立运行和作为模块运行之间切换。


当业务模块独立运行时,还需要配置独立的 Application 和启动页,以及一些特殊的资源文件,这里同样是根据 isSingleCompile 的值来配置 sourceSets 中的属性:


sourceSets {    main {        manifest.srcFile MANIFEST_FILE        res.srcDirs = RESOURCES        java.srcDirs = JAVA_SOURCES        ...    }}
复制代码


这里配置内容不再赘述,可以参考 Android 官网的说明进行设置,主要是针对独立运行时配置 Manifest 文件和添加入口页面的调整。


2.2.3 业务间的通信

对于拆分成独立部分的业务模块而言,彼此业务出现关联的场景还是比较多的,比如火车票推荐酒店,就需要从火车票模块跳转到酒店模块。


对于这样的业务场景,除了之前提到的将部分业务下沉到 BaseModule 以外,针对页面间跳转,我们采用了路由的方式。由于跳转的场景可能是原生页面、网页和 React Native 页面,我们制定了一套规则来进行通用的跳转。


跳转的链接按照以下的格式来实现:


sy://suanya.cn/xxx?url=xxx&type=1
复制代码


其中的 type 则是跳转的类型, url 参数的值就是实际的跳转的地址。对于 网页和 React Native 页面,url 的值是比较容易直观的,对于原生页面,则引入了 ARouter 实现,对于需要传递的参数的场景,则采用 query parameters 的方式进行传递,在统一的地方进行处理,转换成 ARouter 传参的形式。


2.3 组件化架构调整中遇到的一些问题

2.3.1 业务模块的 Manifest 文件维护

在之前提到,业务模块独立运行时需要指定 Application 和启动页面,Manifest 文件内容如下:


<application       android:name=".ModuleApplication"       android:label="@string/app_name">
<!-- 模块单独运行需要的 activity --> <activity android:name=".ModuleLaunchActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".ModuleHomeActivity"/> <!-- 模块单独运行需要的 activity -->
<activity android:name=".activity.ModuleXXXActivity"/> ...</application>
复制代码


其中的 ModuleApplication、ModuleLaunchActivity 和 ModuleHomeActivity 合并打包时,根据 sourceSets 的设置,是不会编译进来的,需要调整 AndroidManifest 文件,最简单的办法就是写两份 AndroidManifest 文件,通过 sourceSets 中的 manifest.srcFile 来指定。但是这样存在一个问题,例如添加一个 ModuleXXXActivity,这个在独立和非独立运行时都需要的 Activity,则需要在两个 AndroidManifest 中都添加一次,这样显然是不够合理的,对开发而言是不友好的。


我们通过 manifest merge 规则找到解决办法,业务模块只需要维护独立运行时的一份 AndroidManifest 文件即可,在合并的 App 的 AndroidManifest 中,对 application 节点,使用 replace 操作:


<application       android:name=".MainApplication"       android:theme="@styleAppTheme"       tools:replace="android:theme,android:name,">
复制代码


而对于只有模块独立运行才使用到的 activity ,则采用 remove 操作进行移除:


<activity    android:name="com.xx.xxx.ModuleLaunchActivity"    tools:node="remove"/><activity    android:name="com.xx.xxx.ModuleHomeActivity"    tools:node="remove"/>
复制代码


如此操作之后,就可以保证最终合并打包时,业务模块设置的 application 被替换成 app 的,而模块独立运行才应用到的 activity 则被移除了,只需要维护一份模块的 AndroidManifest 文件即可。


2.3.2 多应用差异化配置打包

在前面也提到,我们的业务场景对于同一个项目打包出不同的应用,这种需求的我们使用 ProductFlavors 进行了实现。


通过设置不同的 ProductFlavors,通过 manifestPlaceholders 来配置每个应用差异化的参数,例如接入微信的 appId,地图的 key 等。对于每个应用使用不同的主题色和资源问题,则采用在对应的 ProductFlavors 文件夹中以同名文件、同名资源名称的方式进行覆盖设置,这种同名的资源最终以 ProductFlavors 文件夹中的设置为准。


除此以外,还需要针对每个应用的签名配置进行设置:


productFlavors.all { flavor ->    signingConfigs.create(flavor.name, getSigningConfigsByFlavorName(flavor.name))    flavor.signingConfig signingConfigs.getByName(flavor.name)}
复制代码


需要定义 getSigningConfigsByFlavorName 方法来根据 flavor name 获取到对应的 signingConfigs。


三、组件化架构的实践成果

根据之前设定的目标,组件化调整后基本都完成的预期的目标:


1)业务模块分离,从结构层面做到了代码隔离,减少了之前不必要的耦合


2)基础组件的拆分,按照业务和功能拆分出基础组件,便于后期开发和维护


3)简单实现了业务间的通信,实现了跨模块的多种类型的通用跳转


4)实现同一个项目多应用差异化适配打包,支持主题适配等


整体项目进行组件化调整以后,模块的划分更为清晰,结构上实现了代码隔离,减少了耦合。业务模块支持独立运行和整体打包,单个模块完整编译耗时约 20 秒左右,合并打包完整编译整个项目耗时约 1 分钟,极大地提升了开发效率。


作者简介

陈杰,智行火车票高级开发工程师,目前主要负责智行火车票 Android 客户端的架构和公共基础业务开发,热衷于 Android 技术的研究和开源分享。


本文转载自公众号携程技术中心(ID:ctriptech)


原文链接:https://mp.weixin.qq.com/s/_v2NMSQmZA9HLVq03AzbWQ


2019 年 5 月 08 日 08:005211

评论 1 条评论

发布
用户头像
我这边有个疑问,智行各个业务模块已经独立抽出,然后各个模块之间的数据肯定避免不了有依赖。那么关于这一部分数据是下沉到BaseModule中了还是有另外的方式处理?
比如:旅馆模块需要部分汽车出行模块的数据。
2019 年 11 月 08 日 15:37
回复
没有更多了
发现更多内容

当Tomcat遇上Netty,我这一系列神操作,同事看了拍手叫绝

小Q

Java 学习 程序员 架构 面试

高交会现场:众多区块链项目亮相,“家谱链”惊艳全场

WX13823153201

架构训练营-week8-数据结构与算法,网络,IO

于成龙

极客大学架构师训练营 架构训练营

【概念篇】你真正了解越来越火的“数据驱动” 吗?

Java架构师迁哥

目标检测-框架之darknet-数据读取

Dreamer

搞微服务用阿里开源的 Nacos 真香啊!

云流

阿里巴巴 编程 开源项目

SpringBoot启动原理

云流

编程门槛 框架设计 spring Boot Starter】

双“11”搞促销?本文教你用贪心算法来盘他!

Java架构师迁哥

spring-注入配置

Isuodut

Reactor详解之:异常处理

程序那些事

响应式 reactor 程序那些事 响应式系统 响应式架构

LeetCode题解:剑指 Offer 22. 链表中倒数第k个节点,使用栈,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

【Mycat】Mycat核心开发者带你看尽Mycat三大核心配置文件!!

冰河

分布式数据库 中间件 mycat

技术干货:Apache Pulsar 在移动云上的应用

Apache Pulsar

大数据 开源 云原生 Apache Pulsar

甲方日常 50

句子

工作 随笔杂谈 日常

MySQL 的 join 功能弱爆了?

程序员历小冰

MySQL postgres 多表join

架构师训练营 week8 作业

陈皓07

【DevOps实践】企业应用场景众多,怎样选择合适的代码分支模型?

嘉为蓝鲸

git DevOps 软件开发 持续交付 代码管理

阿里首发MySQL“完美日记”,基础+优化+事务+集群+锁+主从复制+安全备份

Java架构追梦

Java MySQL 数据库 架构 面试

面试官问我redis数据类型,我回答了8种

云流

数据库 学习 java面试

腾讯WeMap,一颗“孢子”的数智化之旅

脑极体

要求自愿降薪,员工内心普遍满意:“服从”是如何发生的?

脑极体

Java动态修改LOGGER日志级别

Zhendong

Java Arthas

终于,阿里P9耐不住寂寞,以多年经验总结了地表最强SQL宝典

周老师

Java 编程 程序员 架构 面试

面试蚂蚁金服,首战被MySQL惨虐,熬夜啃透这份阿里面经复盘一个月再战拿下P7offer

比伯

Java 程序员 架构 面试 阿里

阿里突遭断网断电!双11最惊险一幕刚刚曝光

Java架构师迁哥

iptables 端口转发

田振宇

Java批量导入去除重复数据并返回结果,我差点就被放倒了

小Q

Java 学习 程序员 架构

Pulsar Summit Asia 2020 | 场景案例论坛(下):多行业,多场景

Apache Pulsar

大数据 开源 Apache Pulsar

一次完整的JVM堆外内存泄漏故障排查记录

Zhendong

双“11”搞促销?用贪心算法来盘他!

王磊

算法

「架构师训练营」第 4 周作业

小黄鱼

极客大学架构师训练营

打造 VUCA 时代的 10 倍速 IT 团队

打造 VUCA 时代的 10 倍速 IT 团队

从智行 Android 项目看组件化架构实践-InfoQ