IBigerBiger的成长之路

Android组件化(二)-通讯与中转核心Router

在前面一篇文章中,有介绍关于组件化相关的知识,同时也提到了其中一个很重要的点,那就是路由Router,对于组件化来说,路由Router是整个架构的核心,那么对于这篇文章我们就来实现一个简单的Router,来实现组件化的核心。

对于Router,我们需要满足哪些功能呢?

1.多Modle间的调用与信息传递

2.多进程间的通讯

接下来我们一点点实现这些功能,当然,我这里的实现是相对简单的,并不会做特别多的考虑与优化,仅仅作为初级的Router的展示作用。

多Modle间调用

首先要思考下关于多Modle间调用包含的情况,我们调用的可能是一个Activity,一个Service,或者是一个方法,亦或者仅仅只是个变量,所以我们可以选择去调用一个方法,而这个方法里面是包含上述的众多操作。

我们首先定义一个关于这个方法抽象类,及Action类

1
2
3
4
5
public abstract class RouterAction {
public abstract RouterResult invoke(Context context, HashMap<String,String> requestData);
}

这个类里面只有一个方法就是invoke,这个方法里面我们定义需要做的操作,所以我们子Modle只要继承这个类并包装好方法就可以了,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static class ModleServiceStartAction extends RouterAction{
@Override
public RouterResult invoke(Context context, HashMap<String, String> requestData) {
Intent i = new Intent(context, Modle1Service.class);
context.startService(i);
return null;
}
}
static class ModleServiceStopAction extends RouterAction{
@Override
public RouterResult invoke(Context context, HashMap<String, String> requestData) {
Intent i = new Intent(context, Modle1Service.class);
context.stopService(i);
return null;
}
}

所以上面就是子Modle中的两个方法调用,分别是启动一个Service与停止一个Service,接下来就是如何找到这个方法了,所以我们需要定义一个Map队列,来将这些方法与对应的标签存储下来,以便在使用的时候可以找到这些方法。

我们定义一个关于存储这些Action类的抽象方法,及Provider类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class RouterProvider {
private HashMap<String,RouterAction> mActions;
public RouterProvider(){
mActions = new HashMap<>();
registerActions();
}
protected void registerAction(String actionName,RouterAction action){
mActions.put(actionName,action);
}
public RouterAction findAction(String actionName){
return mActions.get(actionName);
}
protected abstract void registerActions();
}

这里我们定义了一个Map用来存储相关的Action类,以及用来存储这些Action,以及找到Action的方法,registerActions这个抽象方法里面我们需要将相关Action存储下拉,所以我们子Modle只要继承这个类并包装好方法就可以了,如下

1
2
3
4
5
6
7
8
9
public class ModleProvider extends RouterProvider{
@Override
protected void registerActions() {
registerAction("startService",new ModleServiceStartAction());
registerAction("stopService",new ModleServiceStopAction());
}
}

一个Provider类则代表一个Modle,接下来就是要将这个Provider类(即Modle)与对应的标签存储下来,这样就可以通过这个标签这个Provider类(即Modle),最后通过Action的标签找到Action类,从而执行里面的方法,这样就可以实现多Modle间的相互调用了。

接下来就是对于Provider类(即Modle)的存储了,我们定义了一个RouterManager类,用来存储相关的Provider类

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RouterManager {
public static HashMap<String, RouterProvider> mProviders;
private static RouterManager sInstance;
private Context mContext;
public RouterManager(Context context) {
mContext = context;
mProviders = new HashMap<>();
}
public static synchronized RouterManager getInstance(@NonNull Application context) {
if (sInstance == null) {
sInstance = new RouterManager(context);
}
return sInstance;
}
public void registerProvider(String providerName, RouterProvider provider) {
mProviders.put(providerName, provider);
}
}

这样我们可以在Application初始化的时候,将相关的Provider类(即Modle)的存储下来,如下

1
2
3
4
5
6
7
8
@Override
public void onCreate() {
super.onCreate();
RouterManager routerManager = RouterManager.getInstance(this);
routerManager.registerProvider("modle",new Modle1Provider());
}

接下来就是我们怎么去寻找对应的Provider类的对应Action(即Modle中对应的方法),寻找这个Action需要的信息有Provider对应的标签,Action对应的标签,所以我们定义一个Request类来存储这些信息,如下

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
public class RouterRequest {
private String provider;
private String action;
private HashMap<String, String> data;
public RouterRequest(Builder builder) {
this.provider = builder.provider;
this.action = builder.action;
this.data = builder.data;
}
public static class Builder{
private String provider;
private String action;
private HashMap<String, String> data = null;
public Builder() {
}
public Builder(String provider, String action) {
this.provider = provider;
this.action = action;
}
public Builder data(HashMap<String, String> data){
this.data = data;
return this;
}
}
public HashMap<String, String> getData() {
return data;
}
public String getAction() {
return action;
}
public String getProvider() {
return provider;
}
}

然后RouterManager内增加关于RouterRequest的处理方法,如下

1
2
3
4
5
public void router(Context context,RouterRequest routerRequest){
RouterProvider routerProvider = mProviders.get(routerRequest.getProvider());
RouterAction routerAction = routerProvider.findAction(routerRequest.getAction());
routerAction.invoke(context, routerRequest.getData());
}

可以看到这里首先从我们存储下的Provider类集合获取对于Modle的Provider类,再获取对应的Action类,并调用这个类的invoke方法,从而达到准确调用某个Modle内的某个方法了

接下来看下调用,如下:

1
2
3
RouterManager.getInstance(MainApplication.getMainApplication()).router(getApplicationContext(),new RouterRequest.Builder("modle","startService").build());
RouterManager.getInstance(MainApplication.getMainApplication()).router(getApplicationContext(),new RouterRequest.Builder("modle","stopService").build());

所以初步功能的Modle间调用是实现功能了,同时我们可以传递相关的数据过去,也可以获取插件中的数据等等。

多进程的通讯

多进程通讯相信一般大家都比较清楚,一般是通过AIDL来实现,虽然我最开始认为Router应该仅仅只作为一个中转跳转的作用,但是难免会出现数据处于多线程的情况,比如:某个Modle中的Activity并非处于主进程,这样我们获取到这个Acivity中的某个变量就并非处于正确的情况了,所以对于Router来说加入多进程的通讯机制是很有必要的。

那么接下来我们就来看一下多进程的出现会对我们之前的Router会产生什么样的影响,其实就和上述的情况一样,我们所获取的数据并非绝对准确,那么怎么样才可以正确获取呢?如果我们的Action和需要获取数据的进程处于同一个进程的话,那么就应该是获取到正确的值啊,所以我们首先将Action与需要获取数据的进程放在同一个进程里。

以Activity为例,当需要将一个Activity设置为非主进程,只要在Manifest设置这个Activity的process属性即可,如下

1
2
3
<activity android:name=".ModleActivity"
android:process=":remote"
/>

这样这个ModleActivity就并非处于主的进程了,而是处于packageName:remote进程了(以remote简称),那么怎么将Action也放在remote进程呢?

当App一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么就会创建新的Application,我们可以利用这个特性,当新进程的Application启动时候,初始化Provider,这样Provider与Action都是处于新的进程中,而不是主进程中,这样就可以实现我们的目标了。

所以我们需要做哪些事情?

1.建立一个新的Map包含进程名称与可以初始化Provider的方法

2.在Application创建时候,通过当前的ProcessName与上述队列的进程名称对比,从而去初始化Provider

首先建立一个可以初始化Provider的类,父类如下

1
2
3
4
5
6
public class RouterApplicaitonLogic {
public void onCreate(Application application){
}
}

子Modle只要继承这个类并包装好方法就可以了,如下

1
2
3
4
5
6
7
8
public class ModelApplicationLogic extends RouterApplicaitonLogic{
@Override
public void onCreate(Application application) {
super.onCreate(application);
RouterManager.getInstance(application).registerProvider("modle",new ModleProvider());
}
}

接下来构建新的Map来存储相关的信息,并通过进程名称去加载相对应的ApplicaitonLogic类,如下

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
public abstract class RouterApplication extends Application{
private static RouterApplication sInstance;
private HashMap<String,Class> mApplicaitonLogics = new HashMap<>();
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
mApplicaitonLogics = new HashMap<>();
initializeLogic();
for (int i = 0; i < mApplicaitonLogics.size(); i++){
if (mApplicaitonLogics.get(ProcessUtil.getProcessName(this,ProcessUtil.getMyProcessId()))!=null){
try {
RouterApplicaitonLogic routerApplicaitonLogic = (RouterApplicaitonLogic)mApplicaitonLogics.get(ProcessUtil.getProcessName(this,ProcessUtil.getMyProcessId())).newInstance();
routerApplicaitonLogic.onCreate(this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static RouterApplication getMainApplication(){
return sInstance;
}
protected abstract void initializeLogic();
public void registerApplicaitonLogic(String s,Class routerApplicaitonLogic){
mApplicaitonLogics.put(s,routerApplicaitonLogic);
}
}

接下来就是继承这个RouterApplication,然后实现initializeLogic方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
public class MainApplication extends RouterApplication{
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void initializeLogic() {
registerApplicaitonLogic("com.xxxx.xxxx:remote", ModelApplicationLogic.class);
}
}

所以到这里就实现了对应进程加载对应的Action方法了。

接下来我们照常执行router方法,就会发现报错,错误的原因是找不到对应的Action,为什么呢?其实很简单,主要因为这个Action已经处于remote进程 ,而并非主进程,所以接下来我们面临的问题就是怎么去调用非主进程中的Action呢?

多进程的通讯与调用前面有说过通过AIDL的方法,至于这个方法的流程什么的我这里就不做介绍了,接下来就直接上我们的方案:

首先我们需要见一个AIDL文件,如下

1
2
3
interface IRouterAIDL {
void route(String routerRequest);
}

这里仅仅只有一个方法,当ReBuild后,系统会自动为我们生成对应的JAVA类方法

接下来创建一个Service类,这个类就是上面AIDL文件的具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RouterAIDLService extends Service{
@Nullable
@Override
public IBinder onBind(Intent intent) {
return stub;
}
IRouterAIDL.Stub stub = new IRouterAIDL.Stub() {
@Override
public void route(String routerRequest) throws RemoteException {
RouterManager.getInstance(RouterApplication.getMainApplication()).router(RouterAIDLService.this,new RouterRequest.Builder().json(routerRequest).process(null).build());
}
};
}

所以非主进程的Model只要继承上述的Service,并将这个Service的进程与非主进程的Model进程设置同样的进程即可,这样就可以访问到非主进程中的Action了。

接下来就是处理什么时候启动这个Service了,所以首先需要将子类继承的RouterAIDLService与对于的标签存储下来,然后在前面的RouterRequest中增加process变量,从而来判断是主进程,还是非主进程,同时这个变量可以与前面的标签匹配,从而启动对应的Service。

接下来就是增加RouterManager里面的匹配逻辑了,如下

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
public class RouterManager {
public static HashMap<String, RouterProvider> mProviders;
private HashMap<String, Class> mServices;
private static RouterManager sInstance;
private Context mContext;
private IRouterAIDL mIRouterAIDL;
private RouterRequest mRouterRequest;
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mIRouterAIDL = IRouterAIDL.Stub.asInterface(service);
try {
mIRouterAIDL.route(mRouterRequest.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mIRouterAIDL = null;
}
};
public RouterManager(Context context) {
mContext = context;
mProviders = new HashMap<>();
mServices = new HashMap<>();
}
private void connetService(Class serviceClass){
Intent binderIntent = new Intent(mContext, serviceClass);
mContext.bindService(binderIntent,mServiceConnection,Context.BIND_AUTO_CREATE);
}
private void disconnetService(){
mContext.unbindService(mServiceConnection);
mIRouterAIDL = null;
}
public static synchronized RouterManager getInstance(@NonNull Application context) {
if (sInstance == null) {
sInstance = new RouterManager(context);
}
return sInstance;
}
public void registerProvider(String providerName, RouterProvider provider) {
mProviders.put(providerName, provider);
}
public void registerService(String providerName, Class service) {
mServices.put(providerName, service);
}
public void router(Context context,RouterRequest routerRequest){
if (routerRequest.getProcess() == null) {
RouterProvider routerProvider = mProviders.get(routerRequest.getProvider());
RouterAction routerAction = routerProvider.findAction(routerRequest.getAction());
routerAction.invoke(context, routerRequest.getData());
}else {
mRouterRequest = routerRequest;
if (mIRouterAIDL != null){
disconnetService();
}
Class routerAIDLService = mServices.get(routerRequest.getProcess());
connetService(routerAIDLService);
}
}
}

所以整个逻辑现在就是这个样子的了,当执行route方法,通过RouterRequest的proces变量判断是否是主进程或者是其他进程,如果是主进程还是前面的执行逻辑,如果不是主进程的话,就通过这个proces变量来找到对应进程(以remote进程为例)的RouterAIDLService子类,然后启动这个Service,启动后通过ServiceConnection,获取这个IRouterAIDL的实例,然后调用route方法,调用route方法就执行到remote进程中Service的router方法,这个router则再次调用RouterManager的route方法,那么这个时候的RouterManager的实例也已经是remote进程内的,所以这时候就可以找到正确的Action方法并执行。

所以到这里才能真正的实现了多进程的通讯。

至于返回值通过修改AIDL及部分逻辑即可,这里就不做特别的介绍了

URL Scheme使用

android中的scheme是一种页面内跳转协议,是一种非常好的实现机制,通过定义自己的scheme协议,可以非常方便跳转app中的各个页面;通过scheme协议,服务器可以定制化告诉App跳转那个页面,可以通过通知栏消息定制化跳转页面,可以通过H5页面跳转页面等。

URL Scheme应用场景:

客户端应用可以向操作系统注册一个 URL scheme,该 scheme 用于从浏览器或其他应用中启动本应用。通过指定的 URL 字段,可以让应用在被调起后直接打开某些特定页面,比如商品详情页、活动详情页等等。也可以执行某些指定动作,如完成支付等。也可以在应用内通过 html 页来直接调用显示 app 内的某个页面。综上URL Scheme使用场景大致分以下几种:

  • 服务器下发跳转路径,客户端根据服务器下发跳转路径跳转相应的页面
  • H5页面点击锚点,根据锚点具体跳转路径APP端跳转具体的页面
  • APP端收到服务器端下发的PUSH通知栏消息,根据消息的点击跳转路径跳转相关页面
  • APP根据URL跳转到另外一个APP指定页面

URL Scheme协议格式:

在Android平台而言,URI主要分三个部分:scheme, authority,path, queryString。其中authority又分为host和port。

格式如下:

1
scheme://host:port/path?qureyParameter=queryString

举个例子:

1
xl://goods:8888/goodsDetail?goodsId=10011002

通过上面的路径 Scheme、Host、port、path、query全部包含,基本上平时使用路径就是这样子的。

  • xl代表该Scheme 协议名称
  • goods代表Scheme作用于哪个地址域
  • goodsDetail代表Scheme指定的页面
  • goodsId代表传递的参数
  • 8888代表该路径的端口号

URL Scheme如何使用:

1.在AndroidManifest.xml中对标签增加设置Scheme

1
2
3
4
5
6
7
8
9
10
11
<activity android:name=".GoodsDetailActivity">
<!--要想在别的App上能成功调起App,必须添加intent过滤器-->
<intent-filter>
<!--协议部分,随便设置-->
<data android:scheme="xl" android:host="goods" android:path="/goodsDetail" android:port="8888"/>
<!--下面这几行也必须得设置-->
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
</activity>

2.获取Scheme跳转的参数

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
Uri uri = getIntent().getData();
if (uri != null) {
// 完整的url信息
String url = uri.toString();
Log.e(TAG, "url: " + uri);
// scheme部分
String scheme = uri.getScheme();
Log.e(TAG, "scheme: " + scheme);
// host部分
String host = uri.getHost();
Log.e(TAG, "host: " + host);
//port部分
int port = uri.getPort();
Log.e(TAG, "host: " + port);
// 访问路劲
String path = uri.getPath();
Log.e(TAG, "path: " + path);
List<String> pathSegments = uri.getPathSegments();
// Query部分
String query = uri.getQuery();
Log.e(TAG, "query: " + query);
//获取指定参数值
String goodsId = uri.getQueryParameter("goodsId");
Log.e(TAG, "goodsId: " + goodsId);
}

3.调用方式

网页上

1
<a href="xl://goods:8888/goodsDetail?goodsId=10011002">打开商品详情</a>

原生调用

1
2
Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse("xl://goods:8888/goodsDetail?goodsId=10011002"));
startActivity(intent);

4.如何判断一个Scheme是否有效

1
2
3
4
5
6
7
PackageManager packageManager = getPackageManager();
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("xl://goods:8888/goodsDetail?goodsId=10011002"));
List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0);
boolean isValid = !activities.isEmpty();
if (isValid) {
startActivity(intent);
}

通过上面说的Scheme,我们可以同样可以定义关于我们之前的Router定义的格式,如下

1
scheme://packageName:process/providerName/actionName?qureyParameter=queryString

例子如下

1
2
3
scheme://packageName:remote/modle/startActivity
scheme://packageName:remote/modle/startActivity?qureyParameter=queryString

而解析的方法其实上面也有说到,所以我们可以直接根据网页获取的URI或者服务器下发的URI去做响应的解析,然后通过Router去做响应的操作。

写在后面的话

到这里就完成了关于组件化的核心Router的构建了,其中包含了多Modle直接的跳转,多进程间的通讯,以及关于URL Scheme在Router中的使用,通过Router这个组件,就可以完成真正的组件化,当然我这里所介绍的Router是比较简单的解决方案,并没有根据实际上复杂的业务的验证,但是万变不离其宗,我说介绍的其实就是Router实现的最基础的东西,开发者可以根据自己的实际情况进行响应的封装,peace~~~