近来replugin开发中遇到宿主和插件间需要通信的情形,思来只有进程间通信(IPC)才是比较好的宿主与插件的通信方式。而Android进程间通信主要有2种方式:Messenger和AIDL。
AIDL(Android Interface Definition Language)是Android接口定义语言。AIDL设计用于实现Android进程间通信。Android中每个进程都拥有自己的Dalvik虚拟机(4.4之后是ART虚拟机),以及独立的内存区域,通过AIDL制定的规则,使不同进程可以进行数据交流。
宿主与插件间通信又可细分为,插件主动向宿主发起通信以及宿主主动向插件发起通信2种情况。为了模拟这2中可能的情况,同时对加深对Messenger和AIDL的理解。在宿主和插件中分别使用这两种方式,宿主作为服务端使用Messenger通信,以及插件作为服务端使用AIDL进行通信,分别查看2种方式的使用效果。
一、使用Messenger
1.1 Messenger服务端
1)服务端定义一个Service
<service android:name=".PluginComService" android:enabled="true" android:exported="true">
<intent-filter>
<action android:name="com.hk.daijun.messengerclient"></action>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
在清单文件中配置Service,exported要配置为true,否则别的进程不能绑定该Service,在intent-filter中设置action。
2) 创建Messenger和Handler实例
创建Messenger和Handler实例,通过Handler来处理消息。
private static class MessengerHandler extends Handler {
@Override public void handleMessage(Message msg) { Log.e(TAG, "handleMessage: what="+msg.what);
switch (msg.what) { case 1001:
Log.e(TAG, "handleMessage: 服务端收到消息"+msg.getData().getString("msg"));
Log.e(TAG, "handleMessage: 服务端收到消息"+msg.getData().getString("msgJson"));
String msgJson = msg.getData().getString("msgJson");
Gson gson = new Gson();
PushMsgContent pushMsgContent = gson.fromJson(msgJson,PushMsgContent.class);
if (pushMsgContent != null) {
Log.e(TAG, "handleMessage: PushMsgContent="+pushMsgContent.sendTime+" "+pushMsgContent.msgContent.alarmTime);
}
Messenger mesgerReply = msg.replyTo;
Message msgReply = Message.obtain();
Bundle bundle = new Bundle();
bundle.putString("service_reply","hello,服务端已收到信息,想做什么");
msgReply.what = 2001;
msgReply.setData(bundle);
try {
mesgerReply.send(msgReply);
} catch (RemoteException e) {
Log.e(TAG, "handleMessage: 消息发给客户端,发送失败");
e.printStackTrace();
}
break;
}
super.handleMessage(msg);
}}
private Messenger messenger = new Messenger(new MessengerHandler());
消息传递时可以考虑使用字符串,然后服务端和客户端都用Gson进行解析,因为宿主是第三方开发,插件是自己开发,省去频繁变更接口的麻烦。
3)重写onBind方法
在Service的onBind方法中,用Messenger创建一个IBinder,Service将IBinder在onBind方法中返回给客户端。
1.2 Messenger客户端
1)创建ServiceConnection,绑定服务
客户端创建ServiceConnection实例,绑定服务端第1步中的Service,action要与服务端app中AndroidManifest中的一致,包名(服务端app包名,即AndroidManifest中查看到的包名)也要一致。
private void connectToService(){
Intent intent = new Intent();
intent.setAction("com.hk.daijun.messengerclient");
intent.setPackage("com.hk.daijun.replugintest");//
bindService(intent,serviceConnection,BIND_AUTO_CREATE);
RePlugin.getHostContext().bindService(intent,serviceConnection,BIND_AUTO_CREATE);
}
此处需要主要要用宿主的上下文来绑定服务,如果直接用bindService来启动服务,能启动服务,但解绑时会报错服务未注册。
2)实例化Messenger
服务绑定后,在ServiceConnection中的onServiceConnected方法中,用IBinder来实例化Messenger(引用服务端的Handler),用此Messenger来向服务端发送消息。
3)接收服务端回复
若客户端还想从服务端那边收到回复,则需要再定义一个Messenger,messengerAccept来,在给服务端发送消息时,在Message的replyTo字段中赋值为messengerAccept。
public void sendMessageToPlugin(){
Log.e(TAG, "sendMessageToHost: ");
Message msg = Message.obtain();
msg.what = 1001;
msg.arg1 = 1001;
msg.replyTo = mesgerAccept;
Bundle bundle = new Bundle();
bundle.putString("msg","我要与宿主通信");
bundle.putString("msgJson",getJsonString());
msg.setData(bundle);
try {
mesgerSend.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
Log.e(TAG, "sendMessageToHost:消息发送异常"+e.getMessage());
}
}
接收服务端的回复,创建的Messenger实例mesgerAccept处理服务端的回复。
private void initAcceptMessenger() {
mesgerAccept = new Messenger(new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 2001:
Log.e(TAG, "handleMessage: 收到服务端的回复");
Log.e(TAG, "handleMessage:
service_reply="+msg.getData().getString("service_reply"));
break;
}
super.handleMessage(msg);
}
});
}
4) 实现效果
首先在宿主app中进入插件,然后在插件中绑定宿主Service,然后发送消息。
二、使用AIDL
AIDL是一个缩写,全称是Android Interface Definition Language,要使用AIDL,必须创建一个定义编程接口的.aidl文件。AIDL文件可以分为两种。一种是用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的。一类是用来定义方法接口,以供系统使用来完成跨进程通信的。
AIDL支持的数据类型,包括Java中八种基本数据类型:byte、short、int、long、double、boolean、char,以及String、List、Map,集合里边的所有元素也必须是AIDL支持的数据类型(包括支持的Java数据类型以及自定义的数据类型)。List可以使用泛型、Map不支持泛型。
自定义数据类型,必须实现parcelable接口,还要注意定向tag。定向tag包括三种:in、out、inout。AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。一般通常in就够了。
2.1 客户端(宿主)
1)新建自定义数据类型java bean
其中writeToParcel方法即表示支持定向tag为in,而如果要支持为 out 或者 inout 的定向 tag 的话,还需要实现 readFromParcel()方法,此方法不在Parcelable接口中,需要自己手写。
可以看到自定义的数据类型要实现parcelable序列化接口。因为进程是系统进行资源分配和调度的基本单位,不同的进程有不同的内存区域,java中传递对象一般是传递对象在内存堆上的引用,而由于不同进程间不能访问各自的内存区域,所以传递的引用对方进程是访问不到的。所以要将传递的数据进行序列化。在客户端对这个对象进行序列化的操作,将其中包含的数据转化为序列化流,然后将这个序列化流传输到服务端的内存中去,再在服务端对这个数据流进行反序列化的操作,还原其中包含的数据,从而实现进程间传递数据的目的。
2)增加aidl文件目录
在project视图下,在main目录下新建aidl目录,然后新建package,包名与AlarmMessage的包名路径一致。
3)添加aidl文件
添加后,aidl文件没在bean目录下,aidl的包名要和对应的javabean包名一致,手动移动aidl文件到bean包名下,然后修改包名,声明序列化类AlarmMessage。
4)增加aidl接口文件
增加aidl接口文件内容,这个文件里面主要内容是进程间通信的接口。注意要导入自定义数据类型的包。AIDL默认支持一些数据类型,在使用这些数据类型的时候是不需要导包的,但是除了这些类型之外的数据类型,在使用之前必须导包,就算目标文件与当前正在编写的 .aidl 文件在同一个包下。
配置完成后,将Module plugin1进行build,就会生成AlarmMessageMgr.java文件。
2.2 服务端(插件)
以上都是插件工程中的配置,现在要在宿主工程中也进行相应配置。配置过程也是几乎一样,需要将java文件和aidl文件都从插件工程中复制到宿主工程中,并且包路径要一样。
在将aidl文件和java bean复制到宿主工程中后。在插件中新建Service类,此次通信以插件作为服务端,而宿主app作为客户端,宿主app主动发起通信。插件中新建Service,其中AlarmMessage.Stub就是aidl文件写完后,对插件工程进行重新编译后产生java文件,在onBind中返回给客户端,客户端据此来进行通信。
public class PluginAidlService extends Service {
private static final String TAG = "PluginAidlService";
private List<AlarmMessage> mMsgList = new ArrayList<>();
private final AlarmMessageMgr.Stub mAlarmMgr = new AlarmMessageMgr.Stub() {
@Override
public List<AlarmMessage> getAlarmMessages() throws RemoteException {
return mMsgList;
}
@Override
public void addAlarmMessage(AlarmMessage msg) throws RemoteException {
Log.e(TAG, "plugin service addAlarmMessage: ");
synchronized (this) {
if (mMsgList == null) {
mMsgList = new ArrayList<>();
}
if (msg == null) {
Log.e(TAG, "addAlarmMessage: msg to add is null");
msg = new AlarmMessage();
msg.alarmId = 0;
msg.alarmTime = "1997-10-7 12:00:00";
msg.alarmTitle = "空报警";
}
mMsgList.add(msg);
}
int size = mMsgList.size();
for (int i=0;i<size;i++) {
AlarmMessage alarmMessage = mMsgList.get(i);
Log.e(TAG, "aidl plugin1 : "+String.format("alarmTitle=%s alarmTime=%s",alarmMessage.alarmTitle,alarmMessage.alarmTime));
}
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.e(TAG, "onBind: ");
return mAlarmMgr;
}
而客户端即宿主app通过绑定插件中的PluginAidlService服务来进行通信,需要注意的是,由于这里涉及到宿主app启动插件中的四大组件(Activity、Service、BroadcastReceiver、ContentProvider),所以不能直接使用原生的api来启动插件中的Service,需要调用Replugin中的api来启动。
private void connectToService(){
Log.e(TAG, "connectToService: ");
Intent intent1 = RePlugin.createIntent("plugin1","com.hk.daijun.plugin1.PluginAidlService");
intent1.setAction("com.hk.daijun.replugintest");
intent1.setPackage("com.hk.daijun.plugin1");
try {
PluginServiceClient.bindService(PluginCommunicateActivity.this, intent1, serviceConnection, BIND_AUTO_CREATE);
} catch (PluginClientHelper.ShouldCallSystem e) {
Log.e(TAG, "connectToService: "+e.getMessage());
}
}
同样,通过bindService()来启动插件中的服务,取消绑定也需要用Replugin的api来进行Service解绑。
PluginServiceClient.unbindService(PluginCommunicateActivity.this,serviceConnection);
客户端在Service连接建立后,调用AlarmMessageMgr.Stub.asInterface(service)将服务端返回的Binder对象转换成AIDL接口类型的对象,进而可以调用aidl定义的接口来进行调用服务端的远程方法。
private void initServiceConnection(){
serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.e(TAG, "aidlhost onServiceConnected: ");
mAlarmMgr = AlarmMessageMgr.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.e(TAG, "aidlhost onServiceDisconnected: ");
}
};
}
插件重新build后,宿主以内置模式接入插件,需要将宿主工程后先clean下,清除build目录,再重新编译安装,否则宿主不先clean的话,插件的修改可能并不生效!
实现效果如下,在宿主中发送报警信息,插件中收到信息后,将所有报警信息打印出来。宿主app中主要是2个按钮,一个绑定插件服务,一个发送消息。
还可以模拟插件Service处理完一些逻辑后,如进行网络请求,获取第三方系统账户信息等操作,执行成功后再启动插件中的Activity,需要注意的是,在Service中启动Activity,必须设置FLAG_ACTIVITY_NEW_TASK标签,但FLAG_ACTIVITY_NEW_TASK标签必须配合taskAffinity属性使用,如果不设置taskAffinity属性值,将不会生成新task。所以不设置taskAffinity就不会生成新的task。
三、 AIDL原理
掠下aidl实现进程间通信的主要原理。AIDL的底层是Binder,其本质就是为我们提供了一种快速实现Binder的工具,通过定义AIDL,android studio编译后生成了AlarmMessageMgr.java文件,而原理的关键也就是这个文件,这个文件其实也可以通过手动编写。
public static com.hk.daijun.plugin1.AlarmMessageMgr asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.hk.daijun.plugin1.AlarmMessageMgr))) {
return ((com.hk.daijun.plugin1.AlarmMessageMgr)iin);
}
return new com.hk.daijun.plugin1.AlarmMessageMgr.Stub.Proxy(obj);
}
asInteface方法用于将服务器的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于统一进程,那么返回服务器的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。
然后看下addAlarmMessage(AlarmMessage msg)方法的调用过程,转到方法的实现。
@Override public void addAlarmMessage(com.hk.daijun.plugin1.bean.AlarmMessage msg) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((msg!=null)) {
_data.writeInt(1);
msg.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addAlarmMessage, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
这个方法由客户端调用,运行在客户端,输入的参数msg定向tag是in,代表数据只能由客户端流向服务端。参数信息写入_data中,然后调用transact方法发起RPC(远程过程调用),mRemote就是Binder对象在binder驱动层对应的引用。transact方法调启后,客户端当前线程被挂起,服务端的onTransact方法会被调用。
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getAlarmMessages:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.hk.daijun.plugin1.bean.AlarmMessage> _result = this.getAlarmMessages();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addAlarmMessage:
{
data.enforceInterface(DESCRIPTOR);
com.hk.daijun.plugin1.bean.AlarmMessage _arg0;
if ((0!=data.readInt())) {
_arg0 = com.hk.daijun.plugin1.bean.AlarmMessage.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addAlarmMessage(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
onTransact方法中4个参数根据参数code,来确定调用哪个方法,data代表输入参数,reply代表返回值。客户端调用服务端的远程方法跟调用本地方法差不多,客户端发起的请求要挂起直到服务端进程处理完,所以远程方法如果是比较耗时的,客户端最好不要在UI线程中发起这个远程方法调用,而应该放到子线程中进行。而服务端的Binder方法是运行在Binder的线程池中。
四、总结
Messenger是执行进程间通信(IPC)最简单的方式,服务端Messenger通过Handler将客户端的请求放到消息循环中排队,然后逐个取出进行处理,而客户端要接收结果需要2个Messenger,一个用于发送消息,一个用于接收消息。其底层原理也是Binder,客户端得到服务端的Binder对象在binder驱动层对应的mRemote引用,然后给服务器发消息,实现跨进程通信。这种方式缺点很明显,服务端以串行方式处理客户端的消息,消息处理结果反馈并不及时,不适合服务端、客户端经常通信的场景。而用AIDL可以并发处理客户端的远程调用,远程方法调用就像本地调用一样,立即执行后就有结果返回。但Messenger的特点是简单,不需要想AIDL一样,需要定义aidl文件,还需要将aidl文件拷贝到服务端和客户端。由于引入了Replugin,使用Messenger、AIDL启动宿主/插件的Service,启动方式有所区别,使用时需要注意。AIDL的底层是Binder,其本质只是为我们提供了一种快速实现Binder的工具。