项目地址:ModularizationArchitecture
个人主页:SpinyTech
欢迎各位读者 Star Follow



ModularizationArchitecture 是Android平台上一种基于路由的多进程、组件化架构:它通过共有路由,在不引用其他模块的前提下,实现不同模块,不同进程之间的通信。它适合中型App架构的团队协作,并行开发,业务线解耦,白盒测试等场景。

1. 关键词定义

  • 主App Module:Android studio中的可运行的Module,项目的主Module
  • 模块Library Moudule:Android studio中的Library Module,模块化中的单一业务或功能组件
  • 测试App Module:Android studio中的可运行的Module,用于测试某一个模块Library的可运行的壳子Module
  • Action:跨模块调用的具体实现
  • ActionResult:Action调用后返回的结果
  • Provider:Action簇,将一组Action放到一起,便于注册
  • RouterRequest:调用Action时的请求信息
  • RouterResponse:Action调用完成之后的响应信息
  • LocalRouter:单进程本地局域路由器
  • WideRouter:多进程广域路由器
  • LocalRouterConnectService:本地路由器Service,用于和广域路由器连接,进行AIDL跨进程通信

2. 开始使用

2.1 在项目中集成

首先我们需要在主App Moudule所有模块Library Moudule的build.gradle中都需要添加

1
compile 'com.spinytech.ma:macore:0.2.1'

2.2 创建自定义Application

创建自定义的Application继承自MaApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CustomApplication extends MaApplication {

// 注册多个进程的本地路由器,在2.7.1中有详细介绍
@Override
public void initializeAllProcessRouter() {
}

// 注册Application逻辑,详见2.3
@Override
protected void initializeLogic() {
//第一个参数表示进程名,第二个表示初始化优先级,第三个是对应的Application逻辑类,在需要的时候会反射初始化
registerApplicationLogic("com.spinytech.maindemo",999, MainApplicationLogic.class);
}

// 标记该App是否是多进程的,如果是多进程则返回true,否则返回false
@Override
public boolean needMultipleProcess() {
return false;
}
}

请注意:
registerApplicationLogic("com.spinytech.maindemo",999, MainApplicationLogic.class)中的包名一定要用常量传入。请不要使用context.getPackageName()方法代替,否则多进程无法正确注册application逻辑。

然后在AndroidManifest.xml中加入

1
2
3
4
5
6
7
<application
...
android:name="xxx.xxx.CustomApplication"
...
>
...
</application>

2.3 创建自定义ApplicationLogic

initializeLogic方法中,需要对Application和实际的ApplicationLogic进行分离,之所以分离,是因为如果引入多进程,Application会初始化多次,如果将逻辑分离,则每次Application初始化,会针对不同的进程,初始化不同的逻辑。

ApplicationLogic的生命周期和Application是相同的,都具有onCreateonTerminateonLowMemory等。

创建自定义ApplicationLogic继承自BaseApplicationLogic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CustomApplicationLogic extends BaseApplicationLogic {
@Override
public void onCreate() {
// 注册Provider,详见2.4
LocalRouter.getInstance(mApplication).registerProvider("util",new UtilProvider());
}

@Override
public void onTerminate(){}

@Override
public void onLowMemory(){}

@Override
public void onTrimMemory(int level){}

@Override
public void onConfigurationChanged(Configuration newConfig) {}

}

2.4 实现Provider

Provider的数据结构实际上就是一个HashMap,它的功能就是将多个Action绑到一起,便于本地局域路由器进行注册。

1
2
3
4
5
6
public class UtilProvider extends MaProvider {
@Override
protected void registerActions() {
registerAction("md5",new MD5EncryptAction());
}
}

Provider创建好了之后,就在本地局域路由器中进行注册,通常是在ApplicationLogiconCreate方法中进行注册(可以参考2.3)。

1
LocalRouter.getInstance(mApplication).registerProvider("util",new CustomProvider());

第一个参数”util”是provider key,可以自定义,以后在调用的时候需要用到。

2.5 实现Action

每一个Action都是一个对外提供调用的具体实现。我们需要对其进行继承,重写。

Action中有两个方法需要重写,一个是isAsync(),一个是invoke()

isAsync()需要告诉调用者,该Action是否是耗时异步的,如果是耗时的(比如网络操作、IO操作等),则返回true,如果非耗时的,则返回false。之所以加上这个判断,是因为有一些调用可能是在主线程进行,我们需要判断这个调用是否是耗时的,从而决定是否要新开工作进程进行调用。

invoke()方法就是实际调用的实现方法。该方法的入参有上下文Context和一组请求数据HashMap<String,String>,该方法返回AcationResult作为结果返回给调用者。
AcationResult的数据结构是这样的

1
2
3
4
5
{
code:xxx,
msg:xxx,
data:xxx
}

整个方法执行过程类似于Web应用,入参是一组key-value键值对,进行一系列处理后,返回处理结果。

下面我们举个简单的MD5加密例子,创建自定义MD5EncryptAction继承自MaAction

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
public class MD5EncryptAction extends MaAction {

@Override
public boolean isAsync(Context context, HashMap<String, String> requestData) {
return false;
}

@Override
public MaActionResult invoke(Context context, HashMap<String, String> requestData) {
String result = "";
if(!TextUtils.isEmpty(requestData.get("params_1"))){
result +=requestData.get("params_1");
}
if(!TextUtils.isEmpty(requestData.get("params_2"))){
result +=requestData.get("params_2");
}

// MD5Util为模块下自定义的工具类,这里就省略了。
result = MD5Util.md5(temp);

MaActionResult result = new MaActionResult.Builder()
.code(MaActionResult.CODE_SUCCESS)
.msg("success")
.data(result)
.object(null)
.build();
return result;
}
}

Action创建好后,可以直接在Provider中进行注册

1
registerAction("md5",new MD5EncryptAction());

第一个参数”md5”是action key,可以自定义,以后在调用的时候需要用到。

2.6 调用

前面几节,我们已经把一切都定义好了,这节我们就来调用。

1
2
3
4
5
6
RouterResponse response = LocalRouter.getInstance(MaApplication.getMaApplication())
.route(context, RouterRequest.obtain(context)
.provider("util")
.action("md5")
.data("params_1", "Hello")
.data("params_2", "World"));

首先通过

1
LocalRouter.getInstance(MaApplication.getMaApplication())

拿到LocalRouter的实例,然后通过

1
route(Context context,RouterRequest request)

方法进行调用。

2.6.1 创建RouterRequest

1
2
3
4
5
6

RouterRequest.obtain(context)
.provider("util")
.action("md5")
.data("params_1", "Hello")
.data("params_2", "World");

这段代码的意思是,找到注册名为”util”的provider,然后再找到其注册名为”md5”的action。传入两个参数,key分别为”params_1”、”params_2”。

在2.6中的route方法中,将RouterRequest传入进去,即可完成调用。

注意:RouterRequest使用了对象内存池技术,使用过后会被回收,所以请不要持有RouterRequest的引用,以免发生不必要的错误。

2.6.2 处理RouterResponse

2.6中的route方法会返回RouterResponse实例。该方法是非阻塞方法,每次调用都会立即返回RouterResponse结果。通过RouterResponseisAsync()方法可以得知该次调用是否是异步的,通过RouterResponseget()方法可以返回该次调用的结果。请注意,get()方法有可能会造成阻塞,这取决于isAsync(),所以,请在调用get()方法前进行判断,不要在主线程调用阻塞的get()方法。

2.7 开启多进程模式

在2.2中,自定义的Application中,重写needMultipleProcess()方法,并将返回值设为true。

1
2
3
4
@Override
public boolean needMultipleProcess() {
return true;
}

2.7.1 创建连接Service,注册本地路由

涉及到多进程,所以我们需要利用AIDL来进行进程间的通信。所以,应用内的每条进程,都需要创建一个连接Service,用来和广域路由进行通信。

创建自定义的Service继承自LocalRouterConnectService,不用做任何处理。

1
public class MainRouterConnectService extends LocalRouterConnectService {}

在AndroidManifest.xml中进行注册

1
<service android:name="xxx.xxx.MainRouterConnectService"/>

然后再在自定义的Application中的initializeAllProcessRouter()方法中,将该进程的连接Service注册给广域路由。

WideRouter.registerLocalRouter方法中第一个参数代表注册的进程名,第二个参数是对应连接Service的类名。

1
2
3
4
5
@Override
public void initializeAllProcessRouter() {
WideRouter.registerLocalRouter("com.spinytech.maindemo",MainRouterConnectService.class);
WideRouter.registerLocalRouter("com.spinytech.maindemo:music",MusicRouterConnectService.class);
}

2.7.2 多进程调用

多进程调用与2.6中本地调用基本一样,只是在生成RouterRequest的时候,需要多加一个Domain,Domain就是需要调用的进程名。(在obtainRouterRequest的时候,如果不加domain,则默认domain是当前进程名)

1
2
3
4
5
RouterRequest
.obtain(context)
.domain("com.spinytech.maindemo:music")
.provider("music")
.action("play");

2.8 RouterRequest中携带附件Object、RouterResponse中携带返回Object (v0.2.0新增)

在v0.2.0版本中,我们新增了RouterRequest中可以添加附件Object的功能(仅在单进程中有效)。

RouterRequest中,新增了object(Object attachment)方法,来向请求中添加附件object。

在抽象类MaAction中,我们新增了两个方法

1
2
Public boolean isAsync (context context, HashMap <String, String> requestData, Object object)`  
Public MaActionResult invoke (Context context, HashMap <String, String> requestData, Object object)

通过判断RouterRequest中,是否有携带附件Object,LocalRouter会去调用不同的invoke方法。

相关代码:

1
2
RouterResponse.mIsAsync = attachment == null? TargetAction.isAsync (context, params): targetAction.isAsync (context, params, attachment);
MaActionResult result = attachment == null? TargetAction.invoke (context, params): targetAction.invoke (context, params, attachment);

在返回结果的时候,可以利用MaActionResult.Builder中的object(Object returnedObject)方法,向调用者返回一个对象实例,利用RouterResponsegetObject()方法来获取返回的Object。

请注意:由于进程间内存不共享,所有的附件Object均只能在本进程内传递。跨进程的附件Object会默认被无视,请不要跨进程带附件Object调用。

具体实现请参考:AttachObjectAction.java

3. 原理

整个架构都是基于路由的原理来实现的,具体的实现原理,可以参考之前的一篇关于架构思考的文章,
《Android架构思考》,本项目基本的设计思路都在这篇文章中有所体现。

4. 存在缺陷

  • 暂不支持多个相同ShareUid的Apk之间的调用。
  • 需要继承自MaApplication,对原有Application代码有改动。

5. 未来版本计划

  • 加入Action失效开关,使其可以由服务端动态控制
  • 加入Action热替换功能
  • 优化消息传递过程中的打包拆包过程
  • 加入SokectAction,实现Action的即时通信

6. 版本

v0.2.1

  • 修复了同步方法中RouterResponse无法正常获取code、message、data的BUG。

v0.2.0

  • 增加了request中携带object的方法
  • 支持单一进程中,传入和返回附件object功能
  • 修复了一些小bug
  • maindemo中增加了新功能示例

原创文章,转载请先联系作者:spiny.tech@gmail.com