随着移动项目的不断发展,移动端软件也变得越来越复杂,体积也变得越来越庞大;为了降低软件的复杂度和耦合度,同时也是为了模块重用、团队开发效率等原因,移动端组件化成了众多公司需要探索和实践的技术方向。本文主要介绍震坤行移动端组件化如何实施,希望对从事移动端组件化的同学有所启发。
1
演进之路背景
1.1 什么是组件化
组件化就是由多个独立的子模块组合成一个整体,降低模块间的耦合,这些子模块可以随意拼装组合, 而且子模块可以独立运行。
1.2 组件化的原因
震坤行App之前的架构的单一工程模式,随着App的快速迭代,带来新功能的不断增加,这种架构存在以下一些问题:
◆ 业务之间代码耦合太重,稍微改动一个地方可能会导致多个业务模块受到影响,对开发和维护带来很大的成本;
◆ 随着业务增?带来团队规模扩大,这时就需要注意多人协作开发问题。组件化可以使各业务模块之间相互隔离,支持独立开发、编译、测试;
◆ 单一工程模式下随着代码量的增加会导致编译代码非常慢,影响开发效率;
◆ 公司后续会衍生出一些独立应用,如物流助手等,希望能够直接复用之前代码进行快速开发。
1.3 组件化的目标
本次组件化架构调整的预期目标如下:
1. 提高组件复用性,节省开发和维护成本;
2. 降低业务模块的耦合,业务移植更简单;
3. 业务模块可以单独开发、运行,由各业务线独立负责;
4. 业务模块可以单独编译打包,加快编译速度;
5. 组件之间可以灵活组建,快速生成其他应用;
6. 多个App共用组件,可以保证整个技术方案的统一性。
2
震坤行组件化实践
2.1 架构设计图
宿主层:空壳app,包含一些全局配置和主Activity,不包含任何业务代码。
业务层:包含各业务组件,相互之间依赖隔离,通过路由总线通信。
平台层:包含基础业务SDK和业务无关的通用代码,供上层各业务组件调用。
通过对各个模块进行解耦,上层模块对下层模块单向依赖,不存在跨层级依赖和同层级依赖。业务组件之间通过路由总线和消息总线进行通信。
组件化过程中主要面对的问题包括如下几个:
◆ 组件如何快速抽离;
◆ 组件之间相互隔离,如何进行UI跳转和通信;
◆ 组件如何单独运行。
2.2 如何创建组件
在Android开发中借助Gradle可以很方便的进行代码拆分。在Gradle多模块工程中有一个根目录,每个模块有一个子目录。为了让Gradle知道工程的结构以及每个子目录是什么模块,需要在根目录中添加一个settings.gradle文件,每个模块提供自己的build.gradle文件。
settings.gradle 文件声明了工程的所有模块:
如果要把library模块作为依赖被app模块包含的话,只需要在app模块的build.gradle文件添加如下代码:
1.组件抽离步骤
1. 先抽离common_component,主要封装了项目中需要的基础功能,如Utils包,网络库,数据库部分。所有业务组件都会依赖common_component;
2. 抽离最简单的业务组件,先代码文件然后资源文件。针对所依赖的文件如果没有别的业务组件用到则移动到该业务Module,否则移动到common_component中;
3. 按照2步骤继续抽离其他组件,在抽离的过程中common_component会越来越大,后续我们会对其做精简;
4. 添加路由框架,完善组件间的跳转和通信;
5. 精简common_component,将里面的内容反向抽离到业务组件。比如Utils包里面的文件进行分离,不同的Utils类、方法放到对应的业务组件中;Retro?t里面将相同的业务接口放到同一个API接口中,然后抽离到业务组件中。逐步将common_component精简到不包含不需要的业务代码。
2. AndroidManifest合并
在单一工程结构中,APP运行需要的四大组件和权限注册都放到同一个AndroidManifest文件中。而当每个Module包含自己的AndroidManifest文件,当最终生成App的时候会将多个AndroidManifest合成一个,即最终会将各个AndroidManifest中注册的四大组件合并到一起,合成的地址目录在:
1. 如果在一个功能Module中声明所需要的权限,那么在主Module中就会看到相应的权限;
2. 如果在其他module中都声明相同的权限,最终的AndroidManifest会合并这个重复声明的权限,所以相同的权限只会声明一次。
所以我们将各个业务所需要的四大组件在各自Module的AndroidManifest中声明,一些基础通用的权限在common_component中声明,业务Module单独需要的权限(如录音权限)在自己的AndroidManifest中声明。
3.全局变量设置
Android系统会为每个程序创建一个Application类的对象,Application对象的生命周期是整个程序中最?的,它的生命周期就等于这个程序的生命周期。我们经常会在使用Context的地方使用Application,但是组件化项目中Application定义在壳App中,各业务Module无法访问。
在Android开发中我们会在build.gradle中不同环境配置不同的buildConfigField值,系统编译后会自动生成BuildConfig类,该类包含我们配置的所有buildConfigField值,但是业务组件获得BuildConfig的不是主App的,而且自己Library Project生成的BuildConfig,里面没有主App中设置的值。
在common_component中定义一个Application基类,各业务组件和壳App中的Application都依赖于 它,业务组件通过该基类获得全局Context。同样我们需要将 BuildConfig 的值下方到common_component中供业务组件使用,最终设置如下:
common_component/ZApplication
app/ZKHApplication
4.组件间资源冲突
在组件化过程中,资源文件如color、shape、drawable、layout等都有可能造成资源名冲突,因为不同 模块的开发是分开的,如果项目组没有按照统一规范命名就会出现资源名冲突。我们可以通过resourcePrefix 来避免,设置了该值后该组件中所有的资源名必须以指定的字符串为前缀,否则会报错。但是 resourcePrefix 只能限定xml中的资源,并不能限定图片资源,所以我们也需要手动将图片资源名添加 resourcePrefix。
2.3 统一配置文件
由于每个Module都有自己的build.gradle文件,该文件里面包含工程所需要的各种配置信息,为了统一 各Module版本依赖信息, 以及让后续维护模块配置更简单,我们第一个需要解决的问题就是要多Module之间统一配置文件。目前Android项目管理Gradle依赖主要由下面三种方法:
1. 手动管理;
2. ext方式(Google推荐);
3. kotlin + buildSrc
我们项目中采用第三种方法,该方法与ext方式类似,不过在它基础上增加了IDE自动补全和单击跳转。
1.手动管理
这是最基础的管理依赖方法,每次新增模块都要从原来模块中将配置和依赖信息复制一份以保持一致;而且每次升级依赖的时候都需要大量的手动更改。
module_a/build.gradle
module_b/build.gradle
2.ext方式
Google推荐的依赖管理方法,该方法将所有配置和依赖放在一起统一管理,各个Library Project通过ext来访问
Root-level build.gradle
module_a/build.gradle
module_b/build.gradle
3.kotlin + buildSrc
该方法在ext方式上升级提供了IDE自动补全和点击跳转功能。具体实现需要在项目中创建一个buildSrc模块,然后编写kotlin代码来管理依赖库。
kotlin + buildScr实现
1. build.gradle.kts
2. Dependencis.kt(kotlin代码)
2.4 组件间通信
将工程进行组件隔离之后,需要解决的问题就是组件之间的通信。这个主要由路由和消息总线来实现,其中路由框架主要用于UI跳转和组件之间一对一通信,而事件总线用来多对多通信。
1.路由框架的好处
1. Android?面跳转中显示Intent会导致耦合太大,不适合组件化拆分;隐式Inten使用起来太麻烦,隐式Intent多了之后会让开发人员无从下手,而且会导致AndroidManifest文件增大;
2. 支持动态修改路由,支持全局或者局部降级策略,再用不用担心由于Activity找不到而发生崩溃问题;
3. 支持跳转拦截,可以统一处理登录,埋点等逻辑。如在未登录情况下,打开登陆?面,登录成功后打开刚才想打开的?面;
4. 支持标准URL跳转,可以使Android、iOS、H5统一跳转路径。服务端可以在数据层面控制客户端的跳转逻辑;
5. 支持跨模块API调用,通过控制反转来做组件解耦。
2.路由框架ARouter
ARouter是阿里推出的路由引擎,是一个路由框架,并不是完整的组件化方案,可作为组件化架构的通信引擎。它主要机制是路由+接口下沉:
路由:编译期生成路由表,用来实现UI跳转。
接口下沉:接口集成IProvider并下沉到commmon_component中,组件中实现接口并通过注解暴露服务。
基础功能:1. 添加依赖和配置
如果是Kotlin项目则配置如下:
基础功能:2. 添加注解
基础功能:3.初始化SDK
基础功能:4. 发起路由操作
3.项目中ARouter使用
在common_component中定义各个业务组件的IProvider接口,业务组件做具体实现,在IProvider中定义接口实现路径,所有Activity和Fragment路径以及给其他业务组件调用的方法。common_component中定义 ARouterManager 返回各个业务组件IProvider。
common_component/ARouterManager
common_component/ISkuProvider
sku_component/SkuProvider
sku_component/CategoryFragment
4. ARouter渐进式改造
1. 如上所示,定义全局IAppProvider接口并将AppProvider实现放在App Module中,在组件化完成后该类会被删除;
2. 在IAppProvider中定义所有的路由路径,并添加到App Module里的Activity和Fragment中,这样项目就通过ARouter成功运行起来了;
3. 抽离业务组件并定义对应的IBussinessProvider(如ISkuProvider)并定义我们需要的组件通信方法,将抽离到的业务组件对应的路由路径从IAppProvider抽离到IBussinessProvider中;
4. 需要注意ARouter允许一个Module中存在多个分组,但是不允许多个Module中存在相同的分组, 在业务组件抽离的过程中要注意不要使用相同的分组。
5.消息总线
有了路由框架来使得组件之间通信,为什么还需要消息总线呢?
上面提到ARouter是通过接口下沉来实现通信,接口调用方需要依赖这个接口并且知道哪个组件实现了这个接口,而消息总线发送方只需要发送消息,根本不用关心是否有人订阅了这个消息,即不需要了解其他组件的情况,使得组件之间实现更彻底的解耦。基于接口的方式只能进行一对一的调用,但是基于 消息总线的方式能够提供多对多的通信。
但是消息总线的这些优点也带了对应的缺点。由于发送者和接收者之间互相不知道,导致逻辑分散,出现问题后很难定位;消息总线能很方便的实现App中跨?面的交互,只需要通过发送消息即可,不用理 会麻烦的 startActivityForResult 和 onActivityResult ,这就会导致消息随意发送,大量滥用。使得后续代码维护很麻烦,特别是那种跨组件的消息。这里就需要在项目组中制定相关规范,控制好消息的发送。
6.事件总线EventBus
EventBus 是一个 Android 事件发布/订阅框架,通过解耦发布者和订阅者简化事件传递。既可以用于Android四大组件间的通讯,也可以用于异步线程和主线程间的通讯,支持指定事件处理的线程和优先级,可以发送与粘性广播类似的粘性事件。简化代码,消除依赖关系,加速应用程序开发。
1. 基类中进行绑定
在BaseActivity和BaseFragment中添加EventBus的注册和反注册,为了方便我们使用注解来定义子类是否需要与EventBus绑定。
2. 对Event封装
Event传入泛型指具体的事件类,code用来业务区分
3. 使用
发送消息:
接收消息:
注意事项:
◆不要将所有事件都通过EventBus进行发送;
◆在Activity和Fragment中使用,需要反注册,以免发生内存泄露。
2.5 组件如何独立运行
1. 组件运行模式切换
Android Studio中的Module主要有两种属性,分别为:
1. application属性,可以独立运行的Android程序,也就是我们的APP
2. library属性,不可以独立运行,一般是Android程序依赖的库文件;
Module的属性是在每个组件的文件中配置的,当我们在组件模式开发时,业务组件应处于application属性,这时的业务组件就是一个Android App,可以独立开发和调试;而当我们转换到集成模式开发时,业务组件应该处于library 属性,这样才能被我们的app壳工程所依赖,组成一个具有完整功能的APP。
当我们用AndroidStudio创建一个Android项目后,Gradle会在项目的根目录中生成一个文件gradle.properties:在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来;那么我们可以通过在gradle.properties中定义一个常量值 isRunAlone 来控制组件是否单独运行(true为是,false为否):
然后我们在业务组件的build.gradle中读取 isRunAlone ,但是 gradle.properties 还有一个重要属性:gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换,也就是说我们读到是个String类型的值,而我们需要的是Boolean值,代码如下:
1. 组件单独运行配置
组件单独运行是需要配置一些相关信息的,如Application、启动?面等。
1. 通过修改SourceSets中的属性来指定AndroidManifest文件:
SourceSets属性可以指定哪些源文件或者文件夹下的源文件要被编译,哪些源文件要被排除。所以我们也可以利用上面 isRunAlone 参数来配置不同环境所需要的资源和依赖文件。
2. 通过使用debug目录来指定代码和资源文件:
由于组件单独运行一般只用于开发阶段,debug目录只有在组件单独运行才生效,作为library打包到主app或者单独打aar包时会自动排除debug目录下的代码和资源,对正式代码无污染
◆在component/src下创建debug文件夹;
◆在debug文件夹下创建2个文件夹:java和res;
◆在java文件夹下创建所需要的Application、launchActivity等;
◆在res文件夹下;
◆将Application、launchActivity等注册到AndroidManifest.xml中。
在整个App运行过程中,各个业务组件之间是不相互依赖的、完全隔离的,他们之间由于打包到一起是可以通过路由框架交互的。但是我们在独立运行过程中是没有其他业务组件的,如果有需要其他业务组件则需要添加到依赖配置里面,如项目中用户登录涉及到很多东?,在业务组件单独运行中没法简单实现,这也是一般会将注册登录单独作为一个组件抽离出来,方便其他业务组件独立运行所依赖。这里也是需要isRunAlone参数来区别。
3
演进之路总结
震坤行Android项目中有Lombok、ButterKnife,而且也正在往Kotlin迁移中,所以中间还是踩了一些坑的。在组件化完成后,项目组也会慢慢将各个组件完全Kotlin并移除掉Lombok和Butterknife。
◆Lombok和Kotlin
由于编译顺序,在编译Kotlin的时候Java代码还没有编译,因此Lombok没有自动生成代码导致失败。目前项目中的做法是将Lombok放到Library中(主要是实体类)。由于使用了Kotlin不再需要Lombok的能力,后续项目中会逐步移除Lombok。
◆ButterKnife
具体可以参考ButterKnife in Library projects,如果项目中使用了apply plugin: 'kotlin- kapt则需要将annotationProcessor修改为kapt butterknife_compiler。由于使用了Kotlin不再需要ButterKnife的能力,后续项目中会逐步移除ButterKnife。
◆统一配置文件一定要提前准备好,不然后续各种麻烦,详细参?统一配置文件
◆全局变量设置
Android项目中用到Application的地方很多,对于业务组件的话可以通过 ParentApplication来获得,而在基础Lib中可以定义一个工具类在程序启动时将Application传递给它,供基础Lib中使用。
◆ARouter的使用
先将项目全部转换成Router跳转,然后进行业务抽离,这样就相当于进行了一次解耦,业务抽离过程会快速很多。
◆组件独立运行可以先不实施,在需要的时候各业务线实现即可
最终项目结构图
component_sku结构图
项目组件化的过程也是对项目业务进行梳理过程,业务划分更加清晰,新人接手项目更加容易,按照组件分配开发任更加方便;项目的可维护性更强,提高了开发效率;出现问题了也更好排查,某个组件出现问题直接对该组件进行处理即可;各组件进行需求变更或者代码重构等都不会在畏首畏尾,担心会影响到其他模块,将影响控制在组件内。
4
本篇文章参考
1. Android组件化方案及组件消息总线modular-event实战
2. 有赞微商城 Android 组件化方案
3. 知乎 Android 客户端组件化实践
4. 关于Android业务组件化的一些思考
5. Kotlin+buildSrc for Better Gradle Dependency Management
6. WMRouter:美团外卖Android开源路由框架
7. ARouter 阿里路由框架
8. CC(业界首个支持渐进式组件化改造的Android组件化框架)
9. 安居客 Android 项目架构演进
10. 美团外卖Android平台化架构演进实践
11. 从智行 Android 项目看组件化架构实践