Android 系统架构——组件化开发实践

https://rainmonth.github.io/posts/A191027.html

摘要

随着App业务的发展,业务模块会越来越多,而不同模块可能对某一个功能都有依赖,比如分享、登录等,这个时候如果没有将这些通用的功能抽象成组件单独拎出来,就面临着相同功能的代码在不同的模块中都有一分实现,如果需要更改这个功能的实现,就需要在每个模块中都改一遍,因此需要将这些通用功能抽象成组件独立出来,这就是组件化的过程。

结构

切换到项目根目录,使用tree -d -L 1命令查看项目的结构。

1
2
3
4
5
6
7
8
9
10
11
12
.
├── app # 壳子App
├── common # 通用库,包括基础工具类、网络请求等的封装
├── common-res # 通用资源库,包括一些通用的图片、颜色等资源
├── component-app # app module (以module形式存在的组件)
├── component-image # image module (以module形式存在的组件)
├── component-movie # movie module (以module形式存在的组件)
├── component-music # music module (以module形式存在的组件)
├── component-read # read module (以module形式存在的组件)
├── component-video # video module (以module形式存在的组件)
├── magicindicator # 引入的一个第三方控件
└── router # 路由module (主要职责:负责解决不同module(组件)之间的通信)

要解决的问题

组件化的过程中通常会遇到以下这些问题:

  1. 组件单独调试,即在没有集成到APP壳子工程上去之前,我们可以单独编译、运行、调试对应组件;
  2. 组件通信,这里说的通信,指的是APP壳子工程和组件、组件与组件之间的通信,包括页面访问、数据传递;
  3. 组件集成,在各个组件开发好后,如何方便的集成到APP壳子中去;
  4. 组件容错,集成是在某个被集成的组件没有开发好的情况下如何友好的容错;
  5. 组件解耦与代码隔离,这里的接口要求组件A中不直接出现组件B中的某个类的引用

组件单独调试

Android Studio采用的是Gradle来构建项目的,对于一个module,可以对其应用以下插件:

  • APP插件,对应插件id为com.android.application,用来配置一个Android App工程,构建后输出一个apk安装包;
  • Library插件,对应插件id为com.android.library,用来构建一个Android Library工程,构建后输出一个aar包;
  • Test插件,对应插件id为com.android.test,用来构建一个Android Test工程;

显然,单独调试时就要采用APP插件,被别的module依赖时就要采用Library插件,所以只要动态的设置插件id就可以了,而动态配置这个插件id对于gradle来说是超简单的。项目根目录下新建一个base_component.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
// 基本组件的配置信息
// 获取组件名,新建组件module时,请按”component-组件名“格式命名,方便这里处理
def componentName = project.getName().replaceAll("component-", "")
// 是否独立运行,这里的merge为根目录下config.gradle文件中定义的一个数组,表示在编译的时候要被集成的component,
// 根据是否包含着该数组中来判断当前module是作为库项目还是独立运行项目
def isRunAlone = !rootProject.ext.merge.contains(componentName)
if (isRunAlone) {
apply plugin: "com.android.application"
} else {
apply plugin: "com.android.library"
}
def javaVersion = JavaVersion.VERSION_1_8
apply plugin: 'com.jakewharton.butterknife'
apply plugin: 'com.alibaba.arouter'

android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
buildToolsVersion rootProject.ext.android.buildToolsVersion

compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}

defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
if (isRunAlone) {
// 单独运行时指定applicationId
applicationId rootProject.ext.android.organization + "." + componentName
multiDexEnabled true
}
javaCompileOptions {
annotationProcessorOptions {
// includeCompileClasspath true
arguments = [moduleName: project.getName()]
}
}
versionCode 1
versionName "1.0.0"
// 指定资源的文件的前缀
resourcePrefix componentName + "_"
resValue "string", componentName + "_module_name", project.getName()
}
// 如果独立运行的话,需要指定其独立运行特有的文件夹,这里都放在main下的runalone中(包括java文件和资源文件)
if (isRunAlone) {
sourceSets {
main {
// 指定AndroidManifest.xml文件
manifest.srcFile 'src/main/runalone/AndroidManifest.xml'
// 指定Java源文件
java.srcDirs = ['src/main/java', 'src/main/runalone/java']
// 指定资源文件
res.srcDirs = ['src/main/res', 'src/main/runalone/res']
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

android {
lintOptions {
abortOnError false
}
}

// 配置组件项目依赖
dependencies {
// 添加对通用库的依赖
api project(":common")
// test
testImplementation rootProject.ext.dependencies['junit']
testImplementation rootProject.ext.dependencies['junit']
testImplementation rootProject.ext.dependencies['robolectric']
testImplementation rootProject.ext.dependencies['shadows-support']
testImplementation rootProject.ext.dependencies['shadows-multidex']
androidTestImplementation rootProject.ext.dependencies['runner']
androidTestImplementation rootProject.ext.dependencies['espresso']
}
}

根目录下的配置文件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
ext {
// build.gradle 文件中的android模块对应配置
android = [
organization : "${organization}",
applicationId : "${organization}",
buildToolsVersion: "28.0.3",
compileSdkVersion: 28,
minSdkVersion : 19,
targetSdkVersion : 28,
versionCode : 1,
versionName : "1.0.0"
]
// 要合并到主库的组件,未填写的组件可以独立运行
merge = [
// "app",
// "image",
// "movie",
// "music",
// "video",
// "read"
]
// 其他配置
}

有了上面的配置后,如果一个组件如image需要集成进App壳子工程中,取消merge数组中image的注释即可,如果单独运行,注释起来即可;

组件通信

组件通信包括组件间的参数传递,页面跳转,这里将两种实现方式,一种是基于接口+实现的方式来实现的,一种是使用ARouter来实现的。

接口+实现来完成组件通信

为了演示通过接口+实现来完成组件通信,这里新建两个组件:component-music、component-video,分别对应登录组件和分享组件,以及一个用来实现通信的component-base 库,其实最终依赖的是Java的反射机制

component-base库

新建一个类型为Library的component-base的module(名字可以随便取了),改库的职责就是集中处理各个组件需要暴露出来的服务(service),比如music和video要暴露哪些方法给其他组件调用,都可以以接口的形式在这里定义,然后相应的组件中实现这些接口。这里假设music组件和video组件要暴露的服务(即接口)分别为IMusicService和IVideoService,其定义如下:

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
// IMusicService.java
public interface IMusicService {
/**
* 进入music模块的主界面
*/
void goMusicMain();

/**
* 唤起Video模块播放视频界面播放视频
*
* @param musicUrl 视频地址
*/
void playMusic(String musicUrl);
}
// IVideoService.java
public interface IVideoService {
/**
* 进入Video模块的主界面
*/
void goVideoMain();

/**
* 唤起Video模块播放视频界面播放视频
*
* @param videoUrl 视频地址
*/
void playVideo(String videoUrl);

}

上面的两个定义在component-base这个module中,为了兼容(即避免service没有获取到的情况),我在component-base中提供了连个接口的默认实现(空实现)。内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// DefaultVideoService.java
public class DefaultVideoService implements IVideoService {
@Override
public void goVideoMain() {
Log.d("VideoService", "goVideoMain default implementation");
}

@Override
public void playVideo(String videoUrl) {
Log.d("VideoService", "playVideo default implementation");
}
}
// DefaultMusicService
public class DefaultMusicService implements IMusicService {
@Override
public void goMusicMain() {

}

@Override
public void playMusic(String musicUrl) {

}
}

上面四个文件定义好后,我们就要考虑如何几种管理了,这里我新建了一个ServiceFactory,用来管理各组件暴露给外部的服务。内容如下:

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
//ServiceFactory.java
public class ServiceFactory {
private IMusicService musicService;
private IVideoService videoService;

private ServiceFactory() {

}

public static ServiceFactory getInstance() {
return Inner.serviceFactory;
}

private static class Inner {
private static ServiceFactory serviceFactory = new ServiceFactory();
}

public IMusicService getMusicService() {
if (musicService == null) {
musicService = new DefaultMusicService();
}
return musicService;
}

public void setMusicService(IMusicService musicService) {
this.musicService = musicService;
}

public IVideoService getVideoService() {
if (videoService == null) {
videoService = new DefaultVideoService();
}
return videoService;
}

public void setVideoService(IVideoService videoService) {
this.videoService = videoService;
}
}

看下component-base现在的基本结构:

1
2
3
4
5
6
7
8
9
com.rainmonth.componentbase
├── ServiceFactory.java
└── service
├── music // music组件的相关服务
│   ├── DefaultMusicService.java
│   └── IMusicService.java
└── video // video组件的相关服务
├── DefaultVideoService.java
└── IVideoService.java

到此,component-base的基本职责就实现了。

服务的注册

定义好ServiceFactory后,我们就要想办法把各个component自己实现的service注册到ServiceFactory中,前面提到了各个component都依赖于component-base,所以我们是要在component实现了ServiceFactory中定义好接口,然后调用相应的set方法将其引用传递给ServiceFactory即可。

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
// component-music下的MusicServie.java
public class MusicService implements IMusicService {
@Override
public void goMusicMain() {
KLog.d("MusicService", "goMusicMain");
}

@Override
public void playMusic(String musicUrl) {
KLog.d("MusicService", "playMusic with url->" + musicUrl);
}
}
// component-video下的VideoService.java
public class VideoService implements IVideoService {
@Override
public void goVideoMain() {
KLog.d("VideoService", "goVideoMain");
}

@Override
public void playVideo(String videoUrl) {
KLog.d("VideoService", "playVideo with url->" + videoUrl);
}
}

// component-music下的MusicApplication.java
public class MusicApplication extends BaseApplication {
@Override
public void onCreate() {
super.onCreate();
ServiceFactory.getInstance().setMusicService(new MusicService());
}
}
// component-video吓得VideoApplication.java
public class VideoApplication extends BaseApplication {

@Override
public void onCreate() {
super.onCreate();
ServiceFactory.getInstance().setVideoService(new VideoService());
}
}

经过上面一波操作,各个组件的接口服务就已经注册到ServiceFactory中了,只要在需要调用的得房调用ServiceFactory的get方法然后调用即可。如:

1
2
3
4
5
6
7
// music下的随便一个Activity
tvTest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ServiceFactory.getInstance().getVideoService().playVideo("https://www.baidu.com/test.mp4");
}
});
组件Application动态配置问题解决

上面接口服务的注册放在了各个module的Application创建时,但当组件作为子module被主module所依赖是,由于Application的替换原则,组件的Application根本不会被创建,因为就不能注册进去了。当然我们可以在主module的Application中(这里就叫MainApplication)持有各个子module的Application的强引用,然后在调用相关的方法完成注册,但这就耦合起来了。可以采用反射机制来避免强引用。具体操作如下:

  1. 在BaseApplication中定义一个抽象方法,用来供module实现(包括主module和子module):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //BaseApplication.java
    public abstract class BaseApplication extends Application {

    private BaseApplicationDelegate mBaseApplicationDelegate;

    @Override
    public void onCreate() {
    super.onCreate();
    ...
    }

    /**
    * 这是个模板方法,库module只负责实现,真正的调用发生在主module中
    * 初始化module对外提供的服务
    */
    public abstract void initModuleService();
    }
  2. 所有的组件module都继承BaseApplication,并实现其定义的方法,同时将服务注册给自己,方便独立运行时使用。

    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
    // MusicApplication.java
    public class MusicApplication extends BaseApplication {
    @Override
    public void onCreate() {
    super.onCreate();
    initModuleService();
    }

    @Override
    public void initModuleService() {
    ServiceFactory.getInstance().setMusicService(new MusicService());
    }
    }
    // VideoApplication.java
    public class VideoApplication extends BaseApplication {

    @Override
    public void onCreate() {
    super.onCreate();
    initModuleService();
    }

    @Override
    public void initModuleService() {
    ServiceFactory.getInstance().setVideoService(new VideoService());
1
2
3
4
5
6
7
8
9
10
3. 集中各个module的Application类,以方便遍历反射。这里采用在common库中定义一个ServiceConfig类,内容如下:
```java
public class ServiceConfig {
private static final String videoApp = "com.rainmonth.video.config.VideoApplication";
private static final String musicApp = "com.rainmonth.music.config.MusicApplication";
public static final String[] appModules = {
videoApp,
musicApp
};
}

后面如果其他组件有类似服务,只要将其所属的Application的类路径加入到appMoudles数组即可。

  1. 主Module中继承BaseApplication,实现并调用BaseApplication的中的抽象方法,内容如下:
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
// MainApplication.java
public class MainApplication extends BaseApplication {
@Override
public void onCreate() {
super.onCreate();
// 调用方法,通过反射完成各个module下的接口服务注册
initModuleService();
}

@Override
public void initModuleService() {
for (String moduleApp : ServiceConfig.appModules) {
try {
Class clazz = Class.forName(moduleApp);
BaseApplication baseApp = (BaseApplication) clazz.newInstance();
baseApp.initModuleService();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
}

至此,我们就通过接口+实现的方式解决了组件通信的问题了。

ARouter完成组件通信

关于使用ARouter来完成组件间的通信,可以参看Android ARouter使用及源码分析一文来具体了解。
这里就简单介绍下其使用。

添加注解

假设APP壳子中有一个需要调整到component-music下的MusicMainActivity,那么我们需要按如下方式添加注解:

1
2
3
4
5
6
7
/**
* 音乐主页面
*/
@Route(path = RouterConstant.PATH_MUSIC_HOME)
public class MusicMainActivity extends BaseActivity implements View.OnClickListener {
...
}

这里的path是RouterConstant下定义的PATH_MUSIC_HOME常量,其值为/music/home要求至少要有两级,一个表示group,一个表示具体的页面。这里建议将所有的ARouter要使用的PATH值几种设置在route这个module下,方便管理。

使用注解

上面的注解添加完成后,可以调用在要进行通信的地方按如下方式调用:

1
RouterUtils.getInstance().build(RouterConstant.PATH_MUSIC_HOME).navigation();

上面的RouterUtils为ARouter的简单封装,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.rainmonth.router;

import com.alibaba.android.arouter.launcher.ARouter;

/**
* 路由工具类
*/
public class RouterUtils {
public static ARouter getInstance() {
return ARouter.getInstance();
}
}

这样就可以完成组件间的页面跳转了,当然在上面调用navigation时可以传递参数,这里就不详述了。

组件集成

组件集成很简单,将想要集成的组件名字(去掉component-前缀)加入到config.gradle的merge数组中即可将其集成到App module中。

组件容错

上面的实例中已经提到了,可以在服务获取不要是提供一个空实现,或者给与提示,这样就可以避免由于找不到服务而导致App crash。

组件解耦

这里说的解耦指的是主module在不直接访问组件类的情况下使用组件提供的服务,看到上面的接口+实现方法解决通信问题后,很资源的就想到可以用反射来解决这个问题,原理类似,这里就不再赘述了。

总结

以上就是我组件化的一个实践,主要涉及到了一些gradle的基本配置、反射解耦及接口+实现的方式解决通信问题,其实组件化并没有那么复杂。