前言
在项目的开发过程中,随着开发人员的增多及功能的增加,代码会越来臃肿,功能间代码耦合也会越来越严重,项目编译的时间也会很长,为了解决以上问题,模块化/组件化就应运而生了。这篇文章我就结合自己在项目中的组件化重构之路来总结下模块化需要解决的问题?以及如何解决?
组件化开发需要解决的问题
在实现组件化的过程中,同一个问题可能有不同的技术路径可以解决,但是需要解决的问题主要有以下几点:
- 如何拆分项目
- 如何满足单个业务模块的单独运行及调试的需求
- 如何满足模块间的数据通信
- 如何满足模块间的页面跳转
- 如何真正实现解耦,降低开发人员的失误,真正实现模块间代码和资源的相互隔离
- 如何实现组件Application的动态配置
以上就是实现组件化的过程中我们要解决的主要问题,下面我们会一个一个来解决,最终实现比较合理的组件化开发。
一、拆分项目
以下分层是我在项目中的分层实践,仅供参考:
- 宿主层:App壳,负责集成业务模块
- 业务模块:将项目的每个大功能模块拆分成一个个单独的moudle
- 业务组件:业务模块中的公共业务组件,比如进度查询,资料填写,以及人脸识别和OCR组件。
- 底层SDK:各个业务线沉淀的一些公共组件,比如网络,图片,日志,风控,崩溃,UI
二、组件单独调试
1. 动态配置组件的工程类型
在 AndroidStudio 开发 Android 项目时,使用的是 Gradle 来构建,具体来说使用的是 Android Gradle 插件来构建,Android Gradle 中提供了三种插件,在开发中可以通过配置不同的插件来配置不同的工程。
- App 插件,id: com.android.application
- Library 插件,id: com.android.libraay
- Test 插件,id: com.android.test
区别比较简单, App 插件来配置一个 Android App 工程,项目构建后输出一个 APK 安装包,Library 插件来配置一个 Android Library 工程,构建后输出 aar 包,Test 插件来配置一个 Android Test 工程。我们这里主要使用 App 插件和 Library 插件来实现组件的单独调试。这里就出现了第一个小问题,如何动态配置组件的工程类型?
我们可以创建一个config.gradle文件,在里面定义一个项目全局变量isRelease,用于动态切换:组件化模式 / 集成化模式,然后根据该变量,使用不同的插件:
1 | if (isRelease) { |
2. 动态配置组件的 ApplicationId 和 AndroidManifest 文件
除了通过依赖的插件来配置不同的工程,我们还要根据 isRelease 的值来修改其他配置,一个 APP 是只有一个 ApplicationId 的,所以在单独调试和集成调试时组件的 ApplicationId 应该是不同的;一般来说一个 APP 也应该只有一个启动页, 在组件单独调试时也是需要有一个启动页,在集成调试时如果不处理启动页的问题,主工程和组件的 AndroidManifes 文件合并后就会出现两个启动页,这个问题也是需要解决的。
ApplicationId 和 AndroidManifest 文件都是可以在 build.gradle 文件中进行配置的,所以我们同样通过动态配置组件工程类型时定义的 isRelease 这个全局变量的值来动态修改 ApplicationId 和 AndroidManifest。首先我们要新建一个 AndroidManifest.xml 文件,加上原有的 AndroidManifest 文件,在两个文件中就可以分别配置单独调试和集成调试时的不同的配置:
其中 AndroidManifest 文件中的内容如下:
1 | // main/manifest/AndroidManifest.xml 单独调试 |
然后在 build.gradle 中通过判断 isRelease 的值,来配置不同的 ApplicationId 和 AndroidManifest.xml 文件的路径
1 | // login 组件的 build.gradle |
到这里我们就解决了组件化开发时遇到的第二个问题,实现了组件的单独调试与集成调试,并在不同情况时使用的不同配置。
三、模块间数据通信
面向接口编程(不推荐)
该方式是在组件中实现接口并将实现类设置到公共库中的service中,将公共接口下沉到公共库中,这样就可以通过调用公共库的service来实现跨模块通信,但是这样做的缺点是公共库会越来越臃肿,违背了组件化开发的初衷。
接口+路由
我们可以通过路由地址查找到接口的实现类,然后通过反射调用的方式我们就可以实现跨模块的通信了,对于这种实现方式ARouter的IProvider就已经实现。
如何对外暴露接口
解决了通信手段的问题,我们就得考虑另一个问题,为其他模块提供的接口+数据结构 我们应该放在哪里?下沉到公共模块吗?或者另外新建一个module用来维护这些接口+数据结构?但是这样一来,成本就有些大了,也不方便。
在微信的模块化文章中提出了一个解决方法,将你要暴露的接口+数据结构 甚至其他想要暴露的文件都.api化 ,即将你要暴露的文件的后缀名改为api,然后通过特定的方法将api后缀的文件拷贝出来,在module外部重新组成一个新的module(也可称为SDK),而想要使用的模块只需要调用这个SDK即可。当然,拷贝文件和组件SDK是完全自动化的,并非手工,这样才能节省成本。
由于微信的模块化文章中没有涉及到.api化的具体实现,所以根据这种思路,我使用了其他方法来实现要达到的效果。具体思路如下:
- 创建一个名为_exports的文件夹,需要对外暴露的文件都放在里面
- 将_exports文件夹打包成jar
1 | /** |
- 将jar发布到远程Maven仓库,我这里做演示,只是发布到本地仓库
1 | artifacts { |
- 在需要的模块调用jar
1 | if (!isRelease) { |
这里需要注意的是:在集成调试的时候,需要使用compileOnly,只在编译期依赖jar包,运行时不需要,否则会报错。
四、模块间页面跳转
模块间的页面跳转,直接使用ARouter就能实现,使用比较简单,读者可以自行搜索,当然我们也可以根据项目不同的需求,站在巨人的肩膀上,自己动手实现一个更加精简高效的路由框架,这样我们才能对路由框架有一个更清楚的认识。
五、模块间代码和资源的相互隔离
代码隔离
我们希望的组件依赖是只有在打包过程中才能直接引用组件中的类,在开发阶段,所有组件中的类我们都是不可以访问的。只有实现了这个目标,才能从根本上杜绝开发过程中直接引用组件中类的问题。这个问题我们可以通过 Gradle 提供的方式来解决,Gradle 3.0 提供了新的依赖方式 runtimeOnly ,通过 runtimeOnly 方式依赖时,依赖项仅在运行时对模块及其消费者可用,编译期间依赖项的代码对其消费者时完全隔离的。
1 | // 主项目的 build.gradle |
资源隔离
我们可以在每个组件的 build.gradle 中添加 resourcePrefix 配置来固定这个组件中的资源前缀。不过 resourcePrefix 配置只能限定 res 中 xml 文件中定义的资源,并不能限定图片资源,所以我们在往组件中添加图片资源时要手动限制资源前缀。并将多个组件中都会用到的资源放入 Base 模块中。这样我们就可以在最大限度上实现组件间资源的隔离。
六、组件Application的动态配置
在使用组件化开发后,业务模块的Application就会被宿主的Application所替换,如果某些模块中需要做一些初始化的操作,只能强引用到宿主的Application中,那么有什么办法可以降低解耦呢?ARouter的IProvider的方式显然可以满足Application的解耦,并且这种方式还可以支持我们可以在不同的时机去做子模块的初始化,也就是可以实现子模块的懒加载,这样我们也可以提高启动速度。