安卓中进程间通信大家可能都非常了解,我们知道service可以使用aidl方法,远程调用服务实现两个不同app的通信。根据文档也能很快实现功能,但具体是怎么跨越两个进程进行通信的,现在跟着我的脚步分析分析吧。
首先提出产生困惑的几个问题:
- 哪些代码是在哪些进程或线程中运行的。
- 是如何传递数据的。
- 是如何传递方法。
代码角度分析
server端
首先service通过binder机制的通信是属于server-client模式的,server端为Service类,在Service中需要实现onBind方法,该方法返回实现了IBinder的实例,这里类实例也通常是在Service中实现的。
@Override
public IBinder onBind(Intent intent) {
initSum = intent.getIntExtra("initSum", -1);
Log.d("jw", "onBind: Thread:"+Thread.currentThread().getId());
return remoteService;
}
IBinder remoteService = new IMyAidlInterface.Stub() {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
@Override
public int add(int a, int b) throws RemoteException {
Log.d("jw", "add thread: "+Thread.currentThread().getId());
return a+b+initSum;
}
}
client端
在client端,示例代码如下:
ServiceConnection mAddServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
addService = IAddAidlInterface.Stub.asInterface(service);
serviceConnected = true;
tv.setText("bind success");
}
@Override
public void onServiceDisconnected(ComponentName name) {
serviceConnected =false;
}
};
public void bindAddService(View view) {
Intent intent = new Intent();
intent.setClassName("com.yglx.learnservice", "com.yglx.learnservice.MyService");
bindService(intent, mAddServiceConnection, BIND_AUTO_CREATE);
Log.d("jw", "bindAddService thread: "+Thread.currentThread().getId());
}
客户端在和服务器连接成功后,拿到了一个实现了某些接口(即客户端和服务器端端aidl生成的
接口)的实例,之后只需要用这个实例就可以访问相应的接口了。
交互
可以看出直接使用不是很难,咋一看就是这么一回事的:
1, 客户端bindservice。
2, 神秘的力量判断这个service是否有binder,如果没有,代码执行3。如果有binder直接运行4。
3, 服务端运行onbind返回一个binder。
4, 又一股神秘的力量,把binder拿过来,让代码运行5。
5, 客户端运行ServiceConnection中的回调函数,把binder给到客户端。
6, 客户端愉快的开始通过那个binder和服务端通信了。
7, 客户端运行binder的相关方法,实际应该是一个写入虚拟内存的过程。
8, 内核的神秘力量又弄出一个线程来,运行服务端端代码,即告诉服务端可以读虚拟内存的数
据了,然后返回到内核。
9, 内核的神秘力量又把给到的数据,让刚才客户端的线程继续运行下去。
10,客户端和服务器就圆满完成了一次跨进程通信了。
以上流程基本就说明了,在应用层进程间是如何通信的。
进程间都有哪些线程
先讲我们最熟悉的主线程。(这里为了让大家看清都涉及到的线程,所以假设服务端和客户端都在代码上都没有显示的开启线程)
- 当客户端调用binderSevice方法时,这个时候处于客户端主线程。
- 如果service没有启动,则在service的oncreate生命周期中为服务端主线程。
- 如果未有binder绑定过,则服务端回调生命周期方法onbind为服务端主线程。
- 客户端回调ServiceConnection的方法,这个也是客户端主线程。
- 客户端发起binder的相关方法,这个时候在客户端主线程。
- 这时服务端要开始运行协议中的指定的binder方法了,这里要分两种情况,一种是如果客户端和服务端在同一个进程,则此时运行的协议方法也是和客户端一样的线程;如果客户端和服务
端不在同一个进程,则服务端将会有一个新的线程来执行协议的指定方法。 - 客户端得到binder相关方法端返回值,如果有返回值的话,这里是同步进行的,即客户端将阻塞直到服务端返回数据,这里还是客户端的主线程(所以客户端运行远程调用方法时,不应该在主线程中执行)。
到这里,第一个问题解决。
传递数据
说传递数据的时候,先简单说一下自动生成的aidl文件。
接口文件中有一个继承了binder的抽象类Stub,Stub里有一个实现了接口的Proxy代理类。这里我注意到一点,其实我们不管在客户端还是服务端都用的同一个aidl文件生成的,并且放在aidl文件的包必须一致才不会出错。在我的测试代码中,为了在接口文件中能获取一些日志信息,所以我直接把在服务端生成的接口文件拷贝到测试的应用工程中。在对日志分析时,注意到,这个文件其实是有两个功能的,一个是用于服务端的主要是抽象类Stub,另一个是在客户端应用中使用的Proxy代理类。所以在服务器端自己不会作为客户端的话,可以把Proxy类去除。如果客户端不会用作服务器的话,也完全可以把Stub去除,把Proxy类抽离出来就可以。
数据传输中,并不是所有数据都能传输的,除了基本类型外,则是需要实现了parcel的可序列化的类才能传递,和Intent传输的数据类似(Intent其实也是一种进程间通信数据传输的载体)。
- 客户端调用binder的协议方法时,运行Proxy类中的方法。
比如demo中的add方法:
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(a);
_data.writeInt(b);
mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
_reply.readException();
流程是把数据的写入和读出都是通过Parcel,这也就是类需要实现Parcel的原因。
- 客户端调用mRetome.transact之后,经过底层binder的神秘力量,将会调用服务端transact方法,即服务端的Stub里的onTransact方法:
case TRANSACTION_add: {
data.enforceInterface(DESCRIPTOR);
Log.d("jw", "aidl stub onTransact: Thread:"+Thread.currentThread().getId());
int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();
int _result = this.add(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
可以看到是先从data中读取底层传输过来的数据,然后取出对应的数据,调用相应的服务端已经实现了的this.add方法,用reply把数据写回底层binder。
好了,数据传输流程就这样的。
传递方法
传递方法即传递回调方法(本质上传的是一个binder,如同binderService返回一个binder),>即有的时候需要服务端主动发消息,能让客户端收到。这个时候就可以通过客户端把回调接口注册到服务端中,服务端通过RemoteCallbackList把这些接口(binder)保存起来,当需要向客户端发送消息的时候即可遍历里面的接口把数据通知过去。在回调的时候其实就相当于客户端这个时候充当了服务端。
客户端把binder写入到data中
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStrongBinder((((messageReceiver != null)) ? (messageReceiver.asBinder()) : (null)));
mRemote.transact(Stub.TRANSACTION_registerReceiver, _data, _reply, 0);
_reply.readException();
在服务端注册时返回接口,如同binderService获取到Ibinder一样。
case TRANSACTION_registerReceiver: {
data.enforceInterface(DESCRIPTOR);
com.yglx.learnservice.MessageReceiver _arg0;
_arg0 = com.yglx.learnservice.MessageReceiver.Stub.asInterface(data.readStrongBinder());
this.registerReceiver(_arg0);
reply.writeNoException();
return true;
}
总结
好了一开始提出的三个问题也都解决了。本文主要也是在应用层角度对android进程间通信通过service的一些记录,特别在说到binder底层机制上,只用了神秘力量来解释也是为了不偏离应用层这个高度。能够清楚应用层的这些情况,就应该知道在service时,需要注意一些不要在主线程操作,以及时刻注意处理并发问题。
最后附上分析时用的代码,读者可以通过拷贝build里aidl生成的接口文件,然后添加日志的方式,查看线程的情况,当然笔者代码也有部分日志输出代码,记得在选中log输出时对应用选择no-filters。