移动架构<第十二篇>:Android组件化架构
模块化、组件化、插件化是当前Android工程的三大架构,市场上组件化和插件化使用最为广泛。
起初,Android工程都是单一分层
结构,所谓单一分层
就是:整个工程一个Module,业务逻辑都写在app模块中。单一分层
使项目失去架构感,业务间高耦合,多人联合开发的项目特别容易出现那种业务高度耦合的情况,为了解决高度耦合问题并且更加容易管理,本文重点说明Android组件化架构
。
什么是模块化、组件化、插件化?
模块化
:根据不同的关注点,将一个项目的可以共享的部分抽取出来,形成独立的Module,这就是模块化。模块化不只包含公共部分,当然也可以是业务模块。(最小单位是模块)
组件化
:组件化是建立在模块化思想上的一次演进,一个变种。组件化本来就是模块化的概念。核心是模块角色的可转化换性,在打包时,是library;调试时,是application。(最小单位是组件)
插件化
:严格意义来讲,其实也算是模块化的观念。将一个完整的工程,按业务划分为不同的插件,来化整为零,相互配合。可以实现apk 的动态加载,动态更新,比组件化更灵活。(插件化的最小单位是apk)
组件化架构模型?
组件化的分层结构可以用以下模型来表示:

组件化是一个思想,所以绘制架构模型是第一步。
- App壳工程:负责管理各个业务组件和打包APK,没有具体的业务功能;
- 业务组件层:根据不同的业务构成独立的业务组件(business模块);
- Main组件:属于比较特殊的业务组件,指定APP启动页面、主界面,一般与app壳工程直接依赖;
- 功能组件层:对上层提供基础功能服务,它们的类型是Android Library或者Java Library;
- Common组件:Common组件是比较特殊的功能组件,主要用于管理app权限、资源、三方依赖以及工具类,
为什么要组件化?
- 各个组件专注自身功能的实现,模块中代码高度聚合,只负责一项任务,也就是常说的单一责任原则;
- 各业务研发可以互不干扰、提升协作效率;
- 业务组件可进行拔插,灵活多变;
- 业务组件之间将不再直接引用和依赖,各个业务模块组件更加独立,降低耦合;
- 加快编译速度、提高开发效率;
如何区分业务组件和功能组件?
业务组件:一般情况下,业务组件都存在一个或者多个Activity,但是这样并不容易理解,一句话理解就是:AndroidManifest.xml中存在Activity、Service、Provider、或者其它注册信息的就是业务组件。
功能组件:为业务提供功能基础的组件,一句话理解就是:AndroidManifest.xml中没有任何注册代码,只有包名定义的就是功能组件,功能组件都是一个Android Library
或者Java Library
。(其中,Common组件比较特殊,它的AndroidManifest.xml文件中含有权限信息)
Android组件化项目实施
[第一步]
确认环境要求
由于不同Gradle版本的代码存在差异性,建议使用的Android Studio的最低版本是3.4.1。
[第二步]
搭建业务组件层和功能组件层
在工程中新建business
文件夹作为业务组件的根目录,新建function
文件夹作为功能组件的根目录,在business
文件夹中新建A业务组件、B业务组件、C业务组件、D业务组件、Main业务组件,在function
文件夹中新建A功能组件、B功能组件、C功能组件、D功能组件和common功能组件。在新建Module时,业务组件选择的Module类型为:Phone & Tablet Module
,而功能组件选择的Module类型一般为Android Library
。
目录结构如下:

同时,需要保证settings.gradle文件中的配置的正确性,在settings.gradle文件中需要加入:
// app壳工程
include ':app'
// 业务组件
include ':business:maincomponent'
include ':business:acomponent'
include ':business:bcomponent'
include ':business:ccomponent'
include ':business:dcomponent'
// 功能组件
include ':function:functionA'
include ':function:functionB'
include ':function:functionC'
include ':function:functionD'
include ':function:common'
或者
// app壳工程
include ':app'
// 业务组件
include ':maincomponent'
project(':maincomponent').projectDir = new File(rootDir, 'business/maincomponent')
include ':acomponent'
project(':acomponent').projectDir = new File(rootDir, 'business/acomponent')
include ':bcomponent'
project(':bcomponent').projectDir = new File(rootDir, 'business/bcomponent')
include ':bcomponent'
project(':bcomponent').projectDir = new File(rootDir, 'business/bcomponent')
include ':ccomponent'
project(':ccomponent').projectDir = new File(rootDir, 'business/ccomponent')
include ':dcomponent'
project(':dcomponent').projectDir = new File(rootDir, 'business/dcomponent')
// 功能组件
include ':functionA'
project(':functionA').projectDir = new File(rootDir, 'function/functionA')
include ':functionB'
project(':functionB').projectDir = new File(rootDir, 'function/functionB')
include ':functionC'
project(':functionC').projectDir = new File(rootDir, 'function/functionC')
include ':functionD'
project(':functionD').projectDir = new File(rootDir, 'function/functionD')
include ':common'
project(':common').projectDir = new File(rootDir, 'function/common')
后者稍微比较复杂,所以推荐前者。
[第三步]
组件模式和集成模式
组件模式和集成模式的概念,在Android组件化架构中非常重要,组件模式和集成模式主要使用在业务层,每个业务都可以作为一个应用来单独编译,也可以作为一个library和其它业务组件一起打包成apk 。
组件模式:在组件模式下,业务可以单独编译,不需要编译整个工程,加快编译速度;
集成模式:在集成模式下,业务不可以单独编译,只能编译app壳工程;
为了区分这两种模式,需要在gradle配置文件中定义isLibrary
变量,在Android工程的根目录下新建config.gradle
文件,内容为:
ext {
// false: 组件模式
// true :集成模式
isLibrary = true
}
在项目根目录下的build.gradle
中导入该配置文件:
// 将 config.gradle 文件中的内容引入到项目
apply from: 'config.gradle'
在所有的业务组件的build.gradle
中,将
apply plugin: 'com.android.application'
改为
//根据isLibrary变量动态的切换 集成/组件模式
if (isLibrary){
apply plugin: 'com.android.library'
}else{
apply plugin: 'com.android.application'
}
这样,就完成了组件模式和集成模式的最基本的配置。
[第四步]
业务组件包名的处理
当isLibrary
设置为true时(集成模式),业务组件就不能单独编译,它从应用(application)
切换为库(Android library)
,Android Library是不能有applicationId的,所以applicationId只存在于组件模式下。
因此,需要添加判断条件,如下:
maincomponent
if (!isLibrary){
applicationId "com.example.maincomponent"
}
acomponent
if (!isLibrary){
applicationId "com.example.acomponent"
}
bcomponent
if (!isLibrary){
applicationId "com.example.bcomponent"
}
ccomponent
if (!isLibrary){
applicationId "com.example.ccomponent"
}
dcomponent
if (!isLibrary){
applicationId "com.example.dcomponent"
}
[第五步]
版本号、包名、依赖的统一管理
在build.gradle
配置文件中统一管理 版本号、包名、依赖:
ext {
// false: 组件模式
// true :集成模式
isLibrary = false
/**
* android版本
*/
android = [
compileSdkVersion : 29,
buildToolsVersion : "30.0.2",
minSdkVersion : 17,
targetSdkVersion : 29,
versionCode : 1,
versionName : "1.0"
]
/**
* 包名统一管理
*/
appId = ["maincomponent" : "com.example.maincomponent",
"acomponent" : "com.example.acomponent",
"bcomponent" : "com.example.bcomponent",
"ccomponent" : "com.example.ccomponent",
"dcomponent" : "com.example.dcomponent",
]
/**
* 依赖版本管理
*/
versions = [
appcompatVersion : "1.2.0",
constraintlayoutVersion : "2.0.1",
junitVersion : "4.12",
testJunitVersion : "1.1.2",
espressCoreVersion : "3.3.0"
]
/**
* 依赖管理
*/
dependencie = [
"appcompat" : "androidx.appcompat:appcompat:${versions["appcompatVersion"]}",
"constraintlayout" : "androidx.constraintlayout:constraintlayout:${versions["constraintlayoutVersion"]}",
"junit" : "junit:junit:${versions["junitVersion"]}",
"testjunit" : "androidx.test.ext:junit:${versions["testJunitVersion"]}",
"espresso-core" : "androidx.test.espresso:espresso-core:${versions["espressCoreVersion"]}"
]
}
使用如下(将所有业务组件和功能组件下修改配置):
android {
compileSdkVersion rootProject.android.compileSdkVersion
buildToolsVersion rootProject.android.buildToolsVersion
defaultConfig {
if (!isLibrary){
applicationId rootProject.appId["acomponent"]
}
minSdkVersion rootProject.android.minSdkVersion
targetSdkVersion rootProject.android.targetSdkVersion
versionCode rootProject.android.versionCode
versionName rootProject.android.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation rootProject.dependencies["appcompat"]
implementation rootProject.dependencies["constraintlayout"]
testImplementation rootProject.dependencies["junit"]
androidTestImplementation rootProject.dependencies["testjunit"]
androidTestImplementation rootProject.dependencies["espresso-core"]
}
[第六步]
解决AndroidManifest合并问题
功能组件是一个Library,它的AndroidManifest是这样写的:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.functiona">/
</manifest>
但是业务组件比较特殊,它有集成模式和组件模式区分。
在集成模式下,业务组件就是一个Library,所以它的AndroidManifest的写法就是如上写法;
在组件模式下,业务组件就是一个独立的项目,即独立的application;
所以,还需要根据不同模式,区分AndroidManifest的路径。
假设某业务组件原有AndroidManifest是非Library,那么就需要为Library单独指定一个AndroidManifest路径,反之,需要给非Library单独指定一个AndroidManifest路径。

在main下创建一个library文件夹(命名随意),放入一个AndroidManifest。
在某业务组件的build.gradle的配置代码如下:
//资源配置
sourceSets{
main{
if(isLibrary){
// 在集成模式下,使用的manifest文件
manifest.srcFile 'src/main/library/AndroidManifest.xml'
}else{
//在组件模式下,使用的manifest文件
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
[第七步]
Common组件构建
Common组件是功能组件之一,它的主要作用是:管理项目的权限、三方依赖、资源以及工具类等等,业务组件都会和Common组件直接依赖,不管是集成模式还是组件模式,业务组件都要依赖Common组件。
这里有个选择,xml资源以及图片资源是Common组件来管理还是业务组件来管理,有关这个选择,只能说每个人的想法不一样。
个人觉得,放在业务组件中管理比较合适,各大业务组件之间没有任何的交互,使不同作用的资源完全解耦,分类比较清晰,当需求有大量变化的时候,会有很多用不到的资源,如果所有的资源都由Common来管理,那么Common不仅显得臃肿,而且还不利于维护。
当然,资源文件由各业务组件来管理也会存在一定的风险。
风险:资源冲突、资源紊乱。
原因:存在资源重名问题,图片的名称重名、String的名称重名等等,Android会为每个资源都分配一个唯一的ID,这个ID是资源的唯一ID,当项目中存在资源名称重名时,他们的ID也是一样的,这样就会造成资源的冲突,会导致界面上显示错误的资源,也就是资源紊乱现象。
所以,我们不得不约定好资源的命名规范。
[第八步]
组件间资源名称冲突问题
同一个组件,如果资源名称一致,会编译报错,不会有资源冲突问题;
但是,如果是不同组件,很容易造成资源重名。
为了解决这个问题,项目组内必须提前制定一个约束:
(1)图片资源、color、string、style等等相关资源,都以组件名称命令,保证资源ID的唯一;
(2)使用resourcePrefix做进一步的约束;
resourcePrefix的用法如下:
android {
compileSdkVersion rootProject.android.compileSdkVersion
buildToolsVersion rootProject.android.buildToolsVersion
resourcePrefix "acomponent_"
resourcePrefix属性并不能强制修改资源ID的前缀,如果某资源的命名并不是以约定的前缀开头,编译也不会报错。
如果,资源ID没有已约定好的前缀,那么就会报错:

这种报错仅仅是提示作用,并不能导致编译报错。
[第九步]
app壳工程构建
app壳工程没有集成模式和组件模式的概念,它引用的插件永远都是: