Android组件化实践指南

组件化基本概念#

典型的安卓工程目录结构如下,我们将根据图例介绍基本的概念术语,下文不再重复解释概念术语的含义。


图1-典型安卓工程目录
  • 灰色区域是工程根目录,根目录通常代指灰色区域

    名称 用途
    settings.gradle 用于指示 Gradle 在构建应用时应将哪些模块包含在内
    多模块项目需要指定应包含在最终 build 中的每个模块
    顶层build.gradle 用于定义项目中所有模块的构建配置
    buildscript 代码块定义项目中所有模块共用的 Gradle 代码库和依赖项
    gradle属性文件 gradle.propertieslocal.properties
  • 绿色区域是module目录,模块目录通知代指绿色区域

    名称 用途
    模块级build.gradle 为其所在的特定模块配置构建设置
    自定义打包选项(如额外的构建类型和产品变种)
    替换 main/ AndroidManifest.xml顶层 build.gradle 文件中的设置
  • 蓝色区域是源代码集[3]

    名称 用途
    src/main/ 此源代码集包含所有构建变体共用的代码和资源。
    src/buildType/ 创建此源代码集可加入特定构建变体专用的代码和资源。
    src/productFlavor/ 创建此源代码集可加入特定产品变种专用的代码和资源。
    注意:如果配置构建以组合多个产品变种,则可以为变种维度之间的每个产品变种组合创建源代码集目录:src/productFlavor1ProductFlavor2/
    src/productFlavorBuildType/ 创建此源代码集可加入特定构建变体专用的代码和资源。

gradle属性文件#

gradle.properties#

配置项目全局 Gradle 设置,如 Gradle 守护程序的最大堆大小。如需了解详情,请参阅构建环境

local.properties#

为构建系统配置本地环境属性,其中包括:

  • ndk.dir - NDK 的路径。此属性已被弃用。NDK 的所有下载版本都将安装在 Android SDK 目录下的 ndk 目录中。
  • sdk.dir - SDK 的路径。
  • cmake.dir - CMake 的路径。
  • ndk.symlinkdir - 在 Android Studio 3.5 及更高版本中,创建指向 NDK 的符号链接,该符号链接的路径可比 NDK 安装路径短

顶层build.gradle#

顶层build.gradle用途是配置项目全局属性:有两种配置方式直接配置和外部导入

直接配置#

直接在顶层build.gradle形式写入配置ext{}

1
2
3
4
5
6
7
8
buildscript {...}
allprojects {...}
ext {
sdkVersion = 28
supportLibVersion = "28.0.0"
...
}
...

外部导入#

在根目录下 定义config.gradle文件,写入配置ext{},接着在顶级build.gradle中导入该配置

第一步:创建根目录/config.gradle文件,并编写配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
ext {
//android开发版本配置
android = [
compileSdkVersion: 28,
buildToolsVersion: "28.0.0",
applicationId : "com.fly.tour",
minSdkVersion : 15,
targetSdkVersion : 28,
versionCode : 1,
versionName : "1.0",
]
//version配置
versions = [
"support-version": "28.0.0",
"junit-version" : "4.12",
]
//support配置
support = [
"constraint-layout" : "2.0.4",
'support-v4' : "com.android.support:support-v4:${versions["support-version"]}",
'appcompat-v7' : "com.android.support:appcompat-v7:${versions["support-version"]}",
'recyclerview-v7' : "com.android.support:recyclerview-v7:${versions["support-version"]}",
'support-v13' : "com.android.support:support-v13:${versions["support-version"]}",
'support-fragment' : "com.android.support:support-fragment:${versions["support-version"]}",
'design' : "com.android.support:design:${versions["support-version"]}",
'animated-vector-drawable': "com.android.support:animated-vector-drawable:${versions["support-version"]}",
'junit' : "junit:junit:${versions["junit-version"]}",
]
//依赖第三方配置
dependencies = [
//rxjava
"rxjava" : "io.reactivex.rxjava2:rxjava:2.2.3",
"rxandroid" : "io.reactivex.rxjava2:rxandroid:2.1.0",
//rx系列与View生命周期同步
"rxlifecycle" : "com.trello.rxlifecycle2:rxlifecycle:2.2.2",
"rxlifecycle-components" : "com.trello.rxlifecycle2:rxlifecycle-components:2.2.2",
//rxbinding
"rxbinding" : "com.jakewharton.rxbinding2:rxbinding:2.1.1",
//rx 6.0权限请求
"rxpermissions" : "com.github.tbruyelle:rxpermissions:0.10.2",
//network
"okhttp" : "com.squareup.okhttp3:okhttp:3.10.0",
"retrofit" : "com.squareup.retrofit2:retrofit:2.4.0",
"converter-gson" : "com.squareup.retrofit2:converter-gson:2.4.0",
"adapter-rxjava" : "com.squareup.retrofit2:adapter-rxjava2:2.4.0",
"logging-interceptor" : "com.squareup.okhttp3:logging-interceptor:3.9.1",

//glide图片加载
"glide" : "com.github.bumptech.glide:glide:4.8.0",
"glide-compiler" : "com.github.bumptech.glide:compiler:4.8.0",
//json解析
"gson" : "com.google.code.gson:gson:2.8.5",

//Google AAC
"lifecycle-extensions" : "android.arch.lifecycle:extensions:1.1.1",
"lifecycle-compiler" : "android.arch.lifecycle:compiler:1.1.1",
//阿里路由框架
"arouter-api" : "com.alibaba:arouter-api:1.4.1",
"arouter-compiler" : "com.alibaba:arouter-compiler:1.2.2",
"eventbus" : "org.greenrobot:eventbus:3.1.1",
"dagger" : "com.google.dagger:dagger:2.13",
"dagger-compiler" :"com.google.dagger:dagger-compiler:2.13",
"stetho" :"com.facebook.stetho:stetho:1.3.1",
"MultiImageSelector" :"com.github.lovetuzitong:MultiImageSelector:1.2"
]

}

第二步:在顶级build.gradle中导入全局配置apply from: "config.gradle"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}
apply from: "config.gradle"

第三步:访问方式,在绿色区域代表的module区域内,有一个模块级build.gradle文件,在该文件内可以通过rootProject.ext.property_name方式访问全局配置

1
2
3
4
5
6
7
8
9
10
android {
// rootProject.ext.property_name
compileSdkVersion rootProject.ext.compileSdkVersion
...
}
...
dependencies {
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
...
}

模块级build.gradle#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* 构建配置的第一行将Gradle的Android插件应用到这个构建中,并使Android
* 代码块可用来指定特定于Android的构建选项。
*/
apply plugin: 'com.android.application'

/**
* android block是你配置所有android特定构建选项的地方。
*/
android {

/**
* compileSdkVersion指定了Gradle用来编译你的应用程序的Android API级别。
* 这意味着你的应用程序可以使用包含在这个API级别或更低级别的API特性。
*/
compileSdkVersion 28

/**
* buildToolsVersion指定了Gradle用来构建应用的SDK构建工具、命令行工具和编译器的版本。
* 你需要通过SDK管理器下载构建工具。
* 此属性是可选的,因为插件默认使用构建工具的推荐版本。
*/
buildToolsVersion "30.0.2"

/**
* defaultConfig块封装了所有构建变量的默认设置和条目,
* 并且可以从构建系统动态覆盖main/AndroidManifest.xml中的一些属性。
* 你可以配置产品风味,以覆盖不同版本的应用程序的这些值。
*/
defaultConfig {

/**
* applicationId唯一标识要发布的包。
* 但是,你的源代码仍然应该引用main/AndroidManifest.xml文件中package属性定义的包名。
*/
applicationId 'com.example.myapp'

// 定义运行应用程序所需的最低API级别。
minSdkVersion 15

// 指定用于测试应用程序的API级别。
targetSdkVersion 28

// 定义应用程序的版本号。
versionCode 1

// 为应用程序定义一个用户友好的版本名。
versionName "1.0"
}

/**
* buildTypes块是您可以配置多个构建类型的地方。
* 默认情况下,构建系统定义了两种构建类型:调试debug和发布release。
* 调试构建类型没有在默认构建配置中显式显示,但它包括调试工具,并使用调试键进行签名。
* 发布版本构建类型应用了Proguard设置,默认情况下没有签名。
*/
buildTypes {

/**
* 默认情况下,Android Studio使用minifyEnabled配置release build类型来启用代码收缩,
* 并指定默认的Proguard规则文件。
*/

release {
minifyEnabled true // 启用发布构建类型的代码收缩。
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

/**
* 在productversions块中,您可以配置多个产品版本。
* 这允许你创建不同版本的应用程序,可以用它们自己的设置覆盖defaultConfig块。
* 产品样式是可选的,构建系统默认情况下不会创建它们。
*
* 这个例子创建了一个免费和付费的产品风格。
* 每个产品样式都指定了自己的应用程序ID,这样它们就可以同时存在于谷歌Play Store或Android设备上。
* 如果声明产品风味flavors,还必须声明风味维度dimensions并将每个风味分配给风味维度dimensions。
* If you declare product flavors, you must also declare flavor dimensions
* and assign each flavor to a flavor dimension.
*/

flavorDimensions "tier"
productFlavors {
free {
dimension "tier"
applicationId 'com.example.myapp.free'
}

paid {
dimension "tier"
applicationId 'com.example.myapp.paid'
}
}
}

/**
* 模块级构建配置文件中的依赖项块指定仅构建模块本身所需的依赖项。
* 要了解更多信息,请转到添加构建依赖项。
*/
dependencies {
implementation project(":lib")
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
}

源代码集Sourceset#

在蓝色区域[3]已经描述过源代码集的4种类型,以下是四种源代码集的示例,构建系统需要合并来自以下源代码集的代码、设置和资源:

  • src/fullDebug/(构建变体源代码集)
  • src/debug/(构建类型源代码集)
  • src/full/(产品变种源代码集)
  • src/main/(主源代码集)

记住,构建系统会合并上述四种代码集,如果同一文件在上述不同源代码集有冲突,则Gradle会按照以下优先级使用该冲突的文件:

构建变体 > 构建类型 > 产品变种 > 主源代码集 > 库依赖项

更多合并规则可以参考这里[4]

组件化实践#

组件化目标#

每个module可以独立运行也可以合并至某一个宿主apk

组件化步骤#

  1. 根目录创建config.gradle,在里面定义数组,数组存储键值对,key是包名,value是版本

  2. 根目录创建gradle属性文件module.build.gradle,在里面定义所有module的配置,包括android类型(应用、依赖库),android属性{defaultconfi、sourcesets、buildtypes}

  3. 根目录编辑gradle.properties,定义isComponentisLibraries,

  4. 处理宿主module模块,在buid.gradle中,根据isComponent属性,来决定是否引入其他组件,否则不由宿主生成apk

    1. 宿主module如何引用工程根目录的config.gradle定义的数组答:在根目录build.gradle 加入 apply from: "config.gradle"
  5. module模块,在main/module目录下创建新的AndroidManifest.xml

    1. 在新的xml中定义作为module启动的默认ActivityApplication
  6. module模块,在main目录下编辑作为非Application时的AndroidManifest.xml

    1. 在默认的xml中,应该删除启动的默认Activity、关掉Application的配置

    2. 作为非Application时,配置app-name有什么用?-答:可能要参考清单文件的优先级

    3. 作为非Application时,写的默认Activity有什么用?-答:参考优先级

  7. module模块,在build.gralde中,导入config.gradle,在android{defaultConfig中,根据isComponent开启applictionId},在dependecies{}按需导入依赖库

根目录结构#

image-20210608181849470

build.gradle#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:3.5.0"

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}
/**
* 有什么用?
*/
apply from: "config.gradle"

config.gradle#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
ext {

androidinfo = [
compileSdkVersion: 28,
buildToolsVersion: "28.0.0",
applicationId : "com.fly.tour",
minSdkVersion : 15,
targetSdkVersion : 28,
versionCode : 1,
versionName : "1.0",
]

versions = [
"support-version" : "28.0.0",
"junit-version" : "4.12",
"app-version-name": "20210607",
"app-version-code": 20210607,

]

support = [
"constraint-layout" : "2.0.4",
'support-v4' : "com.android.support:support-v4:${versions["support-version"]}",
'appcompat-v7' : "com.android.support:appcompat-v7:${versions["support-version"]}",
'recyclerview-v7' : "com.android.support:recyclerview-v7:${versions["support-version"]}",
'support-v13' : "com.android.support:support-v13:${versions["support-version"]}",
'support-fragment' : "com.android.support:support-fragment:${versions["support-version"]}",
'design' : "com.android.support:design:${versions["support-version"]}",
'animated-vector-drawable': "com.android.support:animated-vector-drawable:${versions["support-version"]}",
'junit' : "junit:junit:${versions["junit-version"]}",
]


jetpack = [

]
dependencies = [ //rxjava
"rxjava" : "io.reactivex.rxjava2:rxjava:2.2.3",
"rxandroid" : "io.reactivex.rxjava2:rxandroid:2.1.0",
//rx系列与View生命周期同步
"rxlifecycle" : "com.trello.rxlifecycle2:rxlifecycle:2.2.2",
"rxlifecycle-components": "com.trello.rxlifecycle2:rxlifecycle-components:2.2.2",
//rxbinding
"rxbinding" : "com.jakewharton.rxbinding2:rxbinding:2.1.1",
//rx 6.0权限请求
"rxpermissions" : "com.github.tbruyelle:rxpermissions:0.10.2",
//network
"okhttp" : "com.squareup.okhttp3:okhttp:3.10.0",
"retrofit" : "com.squareup.retrofit2:retrofit:2.4.0",
"converter-gson" : "com.squareup.retrofit2:converter-gson:2.4.0",
"adapter-rxjava" : "com.squareup.retrofit2:adapter-rxjava2:2.4.0",
"logging-interceptor" : "com.squareup.okhttp3:logging-interceptor:3.9.1",

//glide图片加载
"glide" : "com.github.bumptech.glide:glide:4.8.0",
"glide-compiler" : "com.github.bumptech.glide:compiler:4.8.0",
//json解析
"gson" : "com.google.code.gson:gson:2.8.5",

//Google AAC
"lifecycle-extensions" : "android.arch.lifecycle:extensions:1.1.1",
"lifecycle-compiler" : "android.arch.lifecycle:compiler:1.1.1",
//阿里路由框架
"arouter-api" : "com.alibaba:arouter-api:1.4.1",
"arouter-compiler" : "com.alibaba:arouter-compiler:1.2.2",
"eventbus" : "org.greenrobot:eventbus:3.1.1",
"dagger" : "com.google.dagger:dagger:2.13",
"dagger-compiler" : "com.google.dagger:dagger-compiler:2.13",
"stetho" : "com.facebook.stetho:stetho:1.3.1",
"MultiImageSelector" : "com.github.lovetuzitong:MultiImageSelector:1.2"
]
}

gradle属性文件#

1
2
isComponent=true
isLib=false

module.build.gradle#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* 是组件,当做Application
* 不是组件,当做lib
*/
if (isComponent.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
/**
* 编译sdk配置
*/
compileSdkVersion rootProject.ext.androidinfo.compileSdkVersion
/**
* 每个模块的默认配置
*/
defaultConfig {
minSdkVersion rootProject.ext.androidinfo.minSdkVersion
targetSdkVersion rootProject.ext.androidinfo.targetSdkVersion
versionCode rootProject.ext.androidinfo.versionCode
versionName rootProject.ext.androidinfo.versionName

javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
flavorDimensions "tier"
productFlavors{
free{
/**
* 构建变体的应用ID
*/
applicationIdSuffix ".free"
dimension "tier"
}
vip{
applicationIdSuffix ".vip"
dimension "tier"
}
}

/**
* 定义源代码集默认的文件夹路径
*/
sourceSets {
androidTestDebug{
jniLibs.srcDirs = ['libs']
if (isComponent.toBoolean()) {
// 是组件,独立打包apk,读取特定的xml
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
// 不是组件,合并到宿主apk中,读取默认的xml
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
exclude 'debug/**'
}
}
}
main {
jniLibs.srcDirs = ['libs']
if (isComponent.toBoolean()) {
// 是组件,独立打包apk,读取特定的xml
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
// 不是组件,合并到宿主apk中,读取默认的xml
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
exclude 'debug/**'
}
}
}
}

buildTypes {
debug {
/**
* 根据构建类型 设置应用ID;如果您希望同一设备上同时具有调试版本和发布版本,这会很有用,因为两个 APK 不能具有相同的应用 ID。
*/
applicationIdSuffix ".debug"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dataBinding {
enabled true
}
}

setting.gradle#

1
2
3
4
5
6
7
include ':lib-common-ui'
include ':app-vip'
include ':module-news'
include ':module-live'
include ':module-shop'
include ':app-normal'
rootProject.name = "haier-common-app"

module-news#

news\shop\live同理

build.gradle#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apply from: "../module.build.gradle"
android {
defaultConfig {
/**
* 如果是组件,则使用当前组件的包名,不是组件,则无需包名
*/
if (isComponent.toBoolean()) {
applicationId "com.haier.news"
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
api rootProject.ext.support["design"]
api rootProject.ext.support["support-v4"]
api rootProject.ext.support["appcompat-v7"]
api rootProject.ext.support["recyclerview-v7"]
}

image-20210608182147783

module/AndroidManifest.xml#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.haier.news">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Haiercommonapp">
<activity android:name=".FeedNewsHomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

AndroidManifest.xml#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.haier.news">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Haiercommonapp">
<activity android:name=".FeedNewsHomeActivity">
</activity>
</application>

</manifest>

lib-common-ui#

image-20210608182703542

module/AndroidManifest.xml#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.haier.common.ui">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Haiercommonapp">
<!--仅作为独立apk的时候,才运行Activity,作为测试common-ui的首页-->
<activity android:name=".CommonUiHomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

AndroidManifest.xml#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.haier.common.ui">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Haiercommonapp">
</application>

</manifest>

build.gradle#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apply from: "../module.build.gradle"
android {
defaultConfig {
/**
* 如果是组件,则使用当前组件的包名,不是组件,则无需包名
*/
if (isComponent.toBoolean()) {
applicationId "com.haier.common.ui"
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
api rootProject.ext.support["design"]
api rootProject.ext.support["support-v4"]
api rootProject.ext.support["appcompat-v7"]
api rootProject.ext.support["recyclerview-v7"]
}

app-vip#

image-20210608182315991

AndroidManifest.xml#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.haier.app.vip">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Haiercommonapp">
</application>

</manifest>

build.gradle#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
plugins {
id 'com.android.application'
}

android {
compileSdkVersion rootProject.ext.androidinfo.compileSdkVersion

defaultConfig {
applicationId "com.haier.tft"
minSdkVersion rootProject.ext.androidinfo.minSdkVersion
targetSdkVersion rootProject.ext.androidinfo.targetSdkVersion
versionCode rootProject.ext.androidinfo.versionCode
versionName rootProject.ext.androidinfo.versionName

}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
if (!isComponent.toBoolean()) {
// 新增组件,如果属于app-apk,则引入到这里,将组件以lib形式打包成app-apk
// 新增组件,如果不属于app-apk,则不需要引入到这里,即app-apk只引入自己需要的组件
implementation project(':module-live')
implementation project(':module-shop')
implementation project(':module-news')
}
}

app-normal#

image-20210608182509726

AndroidManifest.xml#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.haier.tft">

<!--宿主app,空壳,仅用于合并清单文件、组件资源、library资源-->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Haiercommonapp" />

</manifest>

build.gradle#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
plugins {
id 'com.android.application'
}

android {
compileSdkVersion rootProject.ext.androidinfo.compileSdkVersion

defaultConfig {
applicationId "com.haier.tft"
minSdkVersion rootProject.ext.androidinfo.minSdkVersion
targetSdkVersion rootProject.ext.androidinfo.targetSdkVersion
versionCode rootProject.ext.androidinfo.versionCode
versionName rootProject.ext.androidinfo.versionName

}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
if (!isComponent.toBoolean()) {
implementation project(':module-shop')
implementation project(':module-live')
}
}

实践总结#

根目录 module目录 lib目录
build.gradle
AndroidManifest.xml
module.build.gradle
gradle.properties
config.gradle
settings.gradle

组件化注意事项#

  • 分别处理独立运行的情况、合并的情况
  • 在模块中引用根目录的config.gradle,需要在根目录的build.gradle通过apply导入才可以,否则会报错,找不到config.gradle中的属性
  • module.build.gradle中定义了defaultConfig,在module模块中的build.gradle也写了defaultConfig,然后通过apply导入了module.build.gradle,两个build.gradledefaultCOnfig中的内容会如何处理?
    • 答:目前看上去是合并了如果值不冲突,则合并成一份;如果值冲突,则不清楚谁先谁后
  • 编写sourceSets的语法,对于jnilibssrcfile赋值方式有所差异,一个带等号,一个不带等号
    • jniLibs.srcDirs=['libs']
    • manifest.srcFile 'src/main/module/AndroidManifest.xml'

组件化技术选型#

组件化技术实现了在Debug调试阶段,每个功能模块可以独立变成APP调试,但在打包编译阶段,其最终还是将所有模块打包成一个APK。

插件化:也叫动态加载技术,分宿主APK和插件APK,宿主APK可以理解为就是安装到手机的主APK(诸如手机淘宝),各个功能模块抽取变成插件APK(诸如饿了么,淘票票),这些插件APK可以随着宿主APK一起编译打包安装到手机上,也可以变成远程APK放在服务器,按需下载安装,实现功能的动态配置。从广义上理解,可以把Android系统当成一个宿主APK,各个安装到手机上的软件当成插件APK,从而组成一个插件化系统。
组件化:组件化技术实现了在Debug调试阶段,每个功能模块可以独立变成APP调试,但在打包编译阶段,其最终还是将所有模块打包成一个APK。
热修复:热修复技术有助于我们在用户无感知的时候修复APK,悄无声息的将Bug修复掉,我们希望热修复它是不新增资源文件,四大组件等操作,只是单纯的解决代码逻辑上的Bug,可以简单理解插件化技术是热修复的高级版

下表格摘自 多个维度对比一些有代表性的开源android组件化开发方案

对比项 CC 得到DDComponentForAndroid ModularizationArchitecture 阿里Arouter
(网上很多组件化方案的路由引擎,如AndroidModulePattern)
聚美组件化方案
(基于聚美Router
ActivityRouter
开源时间 2017-11 2017-9 2017-1 2016-12 2016-9 2016-4
介绍文章 wiki Android彻底组件化方案实践 Android架构思考(模块化、多进程) 开源最佳实践:Android平台页面路由框架Arouter 聚美组件化实践之路 ActivityRouter路由框架:通过注解实现URL打开Activity
通信机制 组件总线 路由 + 接口下沉 组件总线 路由 + 接口下沉 路由 + 接口下沉 路由 + 静态方法
activity跳转
是否支持降级处理
activity变量自动注入 1. 通过apt生成自动注入代码
2. 在onCreate中调用AutowiredService.Factory.getInstance().create().autowire(this);或者继承BaseActivity
1. 通过apt生成解析参数的代码
2. 在onCreate方法中调用ARouter.getInstance().inject(this);实现自动注入
startActivityForResult 支持Activity/Fragment,但不建议使用
建议使用统一的组件调用方式
仅支持Activity 仅支持Activity 仅支持Activity 支持Activity/Fragment 仅支持Activity
调用方式(页面跳转) 同步直接返回结果或异步回调结果:
CCResult result = CC.obtainBuilder("ComponentA").build().call();

String callId = CC.obtainBuilder("ComponentA").build().callAsync(new IComponentCallback(){...});
onActivityResult返回结果:
UIRouter.getInstance().openUri(getActivity(), url, bundle);
RouterResponse response = LocalRouter.getInstance(MaApplication.getMaApplication())
.route(MainActivity.this, RouterRequest.obtain(MainActivity.this)
.domain(“com.spinytech.maindemo:music”)
.provider(“music”)
.action(“shutdown”));
String temp = response.getData();
onActivityResult返回结果:
ARouter.getInstance().build("/test/activity").navigation();
onActivityResult返回结果:
Router.create(url).open(context);
onActivityResult返回结果:
Routers.open(context, url);
调用方式(调用服务) 与页面跳转相同 Router.getInstance().getService(ReadBookService.class.getSimpleName()) 与页面跳转相同 ARouter.getInstance().navigation(HelloService.class).sayHello(); PipeManager.get(LoginPipe.class).logout(); 与页面跳转相同
组件向外提供服务 与页面跳转一致,在IComponent中实现 接口下沉到base中,组件中实现接口并在IApplicationLike中添加代码注册到Router中 与页面跳转一致,实现一个对应的Action并在其所属的Provider中注册即可 接口继承IProvider并下沉到base中,组件中实现接口并通过注解来暴露服务 接口下沉到base中,组件中实现接口并在ApplicationDelegate中向接口管理类注册PipeManager.register(CorePipe.class, new CorePipeImpl()); 在静态方法上加注解来暴露服务,但不支持返回值,且参数固定位(context, bundle)
Fragment组件化支持 在IComponent中实现,并支持后续Fragment内部功能调用 调用服务的方式实现,未支持后续Fragment内部的功能调用 不支持 调用服务的方式实现,未支持后续Fragment内部的功能调用 调用服务的方式实现,未支持后续Fragment内部的功能调用 不支持
组件自动注册方案 TrasnformAPI + ASM扫描组件类(IComponent接口实现类)并注册到ComponentMananger中,无需手动维护组件列表 apt生成各module的路由表
TrasnformAPI + javassist将IApplicationLike的注册代码生成到自定义application.onCreate方法中,无需手动维护组件列表
未实现自动注册,
1. Action列表在其所属的Provider中注册
2. Provider在其所属的ApplicationLogic中注册
3. ApplicationLogic在主app的Application中注册
新版本(1.3.0)开始支持通过插件完成路由注册
1. apt生成各module的路由表
2. TrasnformAPI + ASM扫描路由表并注册到LogisticsCenter中,无需手动维护组件列表
1. apt生成各module的路由表pkg.RouterRuleCreator类
2. 在ComponentPackages中定义所有RouterRuleCreator的包名
3.在BaseApplication中反射所有的包名找到所有路由表RouterRuleCreator
4. 需要手动维护ComponentPackages类中的包名列表
1. apt生成各module的路由表
2. apt在application的module通过Modules注解生成RouterInit进行注册
3. 需要手动维护Modules注解中的组件列表
组件单独运行的方式 切换library/application方式编译,提供2种方式:
1. module/build.gradle中切换ext.runAsApp=trueOrFalse
2. 在local.properties中切换moduleName=trueOrFalse(推荐使用的方式,不会提交到代码仓库中)
切换library/application方式编译,在module/gradle.properties中切换isRunAlone=trueOrFalse 切换library/application方式编译,框架本身没有提供切换方式,开发者自行解决 切换library/application方式编译,框架本身没有提供切换方式,开发者自行解决 组件module始终以library方式编译,额外提供app壳子,可以按需将多个组件依赖进来一起打包。
好处是所有组件调试时包名相同,能满足分享及地图等第三方SDK对包名的要求
切换library/application方式编译,框架本身没有提供切换方式,开发者自行解决
跨app组件调用支持
跨app调用开关及权限设置 / / /
组件app运行时调用其它组件 组件同时安装在设备上即可,实际开发中一般是当前正在开发的组件和主app中的组件互相调用.
通过广播 + Service + LocalSocket实现,没有UrlScheme调用时弹出的选择框
将需要调用的组件一起打包才能调用 组件同时安装在设备上即可,实际开发中一般是当前正在开发的组件和主app中的组件互相调用.
通过AIDL实现
一起打包或者通过urlScheme来统一转发 将需要调用的组件一起打包才能调用 UrlScheme原生支持跨app调用,组件同时安装在设备上即可
通过中介Activity转发:RouterActivity
组件依赖隔离 无需依赖、完全隔离 通过插件实现只在打apk包时才添加依赖,编码期间不能直接调用其它组件的代码,想知道如何实现可以戳这里 无需依赖、完全隔离 未隔离 未隔离 无需依赖、完全隔离
AOP支持 拦截器 + 组件内部Action进行AOP 组件内部Action进行AOP 拦截器AOP
拦截器
组件调用的超时设置
组件调用的取消
动态注册/注销组件
特点 1. 可以跨app调用,初期改造时即可单独编译组件运行
2. 提供统一的组件调用及实现方式(不管是否跨app调用、页面跳转、服务调用、同步/异步调用)
3. 组件自动注册,无需维护
4. 提供了ActionProcessor按需加载的支持
1. 编码期间组件依赖通过插件进行隔离,避免直接调用其它组件的代码
2. 提供了兼容ARouter的方案
3. 组件自动注册,无需维护
1. 可以跨app、app内跨进程调用
2. 组件运行在各自进程中,单独运行与联合打包切换时需要修改进程名称
3. 组件需指定同步实现还是异步实现,调用组件时统一拿到RouterResponse作为返回值,可以自行决定同步还是异步方式调用RouterResponse.getData()来获取结果,但异步获取时需要自己维护线程
1. 阿里出品,使用者众多,QQ群里交流比较活跃
2. 支持分级按需加载
3. 是一个路由框架,并不是完整的组件化方案,可作为组件化架构的通信引擎
组件module可以始终以library方式编译,由统一的app壳子来安装调试,不需要切换library/application编译方式、避免了第三方SDK需要指定包名的问题、自定义权限重复导致安装冲突的问题、误操作导致apk upload到maven仓库的问题等 1. 业内最早的组件化支持库
2. 通过注解静态方法的方式暴露服务
组件定义代码侵入性 新增IComponent接口的实现类来定义组件,侵入性低 注解定义路由及参数自动注入,侵入性高 新增接口实现类,侵入性低 注解定义路由及参数自动注入,侵入性高 注解定义路由,侵入性高 注解定义路由,侵入性高
组件调用代码侵入性
混淆配置 所有下沉接口、框架中相关接口的实现类等 -dontwarn com.spinytech.** 框架中的所有类及框架相关接口的实现类 所有RouterRuleCreator类 框架中的所有类
老项目改造成本评估 一般 一般
方案使用的学习成本评估 一般 一般 一般 一般
后续维护成本评估 一般 一般 一般
QQ群 686844583 693097923 592278657 / 336755078 108895031

其他理论型组件化技术

对比项 ArmsComponent JIMU
开源时间 2018年 2018年
相关文章 Wiki Documents (开发前必看!!!)
文章介绍
辅助代码模板: ArmsComponent-Template
Android彻底组件化方案实践
架构图 img img

组件化参考文章#

名称 大纲
安居客 Android 项目架构演进 - 知乎 (zhihu.com)
滴滴国际化项目 Android 端演进 (trinea.cn)
CC: 业界首个支持渐进式组件化改造的Android组件化开源框架,支持跨进程调用 组件化改造的Android组件化开源框架wiki
CC框架实践(1):实现登录成功再进入目标界面功能
CC框架实践(2):Fragment和View的组件化
CC框架实践(3): 让jsBridge更优雅
得到DDComponentForAndroid Android彻底组件化方案实践
组件化设计思路 浅谈Android组件化
原理解释文章Android彻底组件化方案实践
demo解读文章Android彻底组件化demo发布
ModularizationArchitecture Android架构思考(模块化、多进程)
ModularizationArchitecture 使用教程
阿里ARouter 开源最佳实践:Android平台页面路由框架Arouter
聚美组件化方案Demo
(基于聚美Router)
聚美组件化实践之路
Router:一款单品、组件化、插件化全支持的路由框架
ActivityRouter ActivityRouter路由框架:通过注解实现URL打开Activity
通过 URL 打开 Activity
美柚路由方案RouterKit 这个方案的特别之处在于其组件自动注册的方案:通过apt生成每个module的路由表,然后复制到app的assets目录,运行的时候遍历asset目录,反射对应的activity
组件总线方案ModuleBus 组件化开发跨module交互方式—ModuleBus交互
点击查看
-------------------本文结束 感谢您的阅读-------------------