前言
IPC 系列文章:
建议按顺序阅读。
Android IPC 之Service 还可以这么理解
Android IPC 之Binder基础
Android IPC 之Binder应用
Android IPC 之AIDL应用(上)
Android IPC 之AIDL应用(下)
Android IPC 之Messenger 原理及应用
Android IPC 之服务端回调
Android IPC 之获取服务(IBinder)
Android Binder 原理换个姿势就顿悟了(图文版)
上篇文章分析了Binder作为IPC中的一种在Android里发挥着重要的作用,本篇将从代码的角度分析如何使用Binder进行进程间通信。
通过本篇文章,你将了解到:
1、IPC 基础
2、IBinder/Binder 简介
3、编写跨进程的Service
4、Binder 通信Demo
5、AIDL的引入
1、IPC 基础
网上 绝大部分 \color{Red}{绝大部分} 绝大部分文章在分析了Binder之后立即抛出AIDL概念,然后一堆代码,初看让人比较迷惑,再看也无法完全理解AIDL存在的意义。这里套用一个比较俗的说法:存在即合理(一个东西存在有它存在的理由)。同样适用于程序设计,为什么要用AIDL?它不会凭空想当然的出现,一定是它解决了某些问题。接下来我们一步步分析,自然过渡到使用AIDL。
同一进程里的访问
在Java的世界里,一切都是对象,拿到对象后就可以访问对象的属性和方法。
对象存在于内存的某个区域,怎样拿到对象呢?答案是:引用。
class Student {
private int age;
private String name;
}
private Student getStudent() {
//student 是引用
//该引用指向了堆里面的Student对象
Student student = new Student();
return student;
}
只要拿到了Student引用,就可以操作Student对象。
不同进程里的访问
同一进程里的访问是我们所熟悉的方式,那么不同进程间的访问呢?
假若Student对象在进程B里构造,而想在进程A里使用它。通过上篇文章可知无法直接引用Student,然而可以借助于Binder。
由图可知:
1、进程A调用Binder提供的接口
2、Binder调用进程B的接口
3、通过Binder的连接,A就可以调用B
Binder(驱动)对于上层来说是透明的,这么看起来就像是A直接调用了B的接口。
2、IBinder/Binder 简介
既然Binder机制要为上层提供接口,那么就需要将接口/类暴露出来,比较重要的接口/类是:IBinder/Binder。
IBinder.java:接口
public interface IBinder {
...
//code : 要执行动作的标示
//data : 从客户端往服务端传递的序列化后的数据,不能为空
//reply : 从服务端返回的序列化后的数据,可能为空
//附加操作标记:0-->表示阻塞等待该方法调用结束 1-->表示执行该方法后立即返回
public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags)
throws RemoteException {
...
}
}
Binder.java 抽象类
Binder实现了IBinder:
public class Binder implements android.os.IBinder {
...
//code : 要执行动作的标示
//data : 从客户端往服务端传递的序列化后的数据,不能为空
//reply : 从服务端返回的序列化后的数据,可能为空
//附加操作标记:0-->表示阻塞等待该方法调用结束 1-->表示执行该方法后立即返回
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
int flags) throws RemoteException {
...
}
...
}
可以看出,transact(xx)和onTransact(xx)参数很像,类似于layout(xx)–>onLayout(xx)、draw(xx)–>onDraw(xx) 调用方式,猜测transact(xx)里最终调用了onTransact(xx)。
整理出两者有如下联系:
1、进程B实现了Binder里的onTransact(xx)方法,并挂出IBiner接口,告诉外界可以调用这个接口来获取B提供的服务。
2、进程A获取了IBinder接口,并调用transact(xx),传递消息给B。
3、通过Binder驱动的中转,找到该IBinder是进程B放出来的,于是调用onTransact(xx)。
至此,进程A就可以发送消息给进程B了。
大致流程如下:
3、编写跨进程的Service
上面问题的关键是:进程A如何获取进程B提供的IBinder接口?
明显的这势必又是一次IPC过程,恰好可以借助四大组件之一的Service来完成。
在AndroidMenifest.xml里声明Service的时候:
<service android:name=".MyService"></service>
默认表示该Service与当前App运行在同一进程里。
若要想Service运行在单独的进程里,可增加配置如下:
<service android:name=".MyService" android:process=".hello"></service>
或者
<service android:name=".MyService" android:process=":hello"></service>
其中:
- .hello表示Service运行在名为.hello的进程里
- :hello表示Service运行在名为当前进程名+hello的进程里,举个例子当前进程名为:com.example.myapplication,那么Service运行的进程名为:com.example.myapplication:hello
配置好如上信息之后,开启(显示开启/绑定开启)Service之后,Service就运行在单独的进程里了:
4、Binder 通信Demo
基本的东西准备好了,接着来看看具体的业务。
编写Server端业务
1、声明接口
既然进程B要提供给外界服务,那么最好声明一个接口:
public interface IMyServer{
//提供给外界调用
public void say(String word);
}
2、定义Binder子类
为了获取Binder驱动发过来的消息,需要声明一个Binder类。
public class MyServerBinder extends Binder {
//匿名内部类实现接口
IMyServer iMyServer = new IMyServer() {
@Override
public void say(String word) {
//为方便起见,仅仅打印客户端传递过来的消息
Log.d("IPC", word + " in process:" + SystemUitl.getAppName(null));
}
};
@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
//从序列化后的内容里读取对应的字段值
String word = data.readString();
//收到Client发过来的消息
iMyServer.say(word);
//回复消息给Client
String replyWord = "I'm fine, and you?";
Log.d("IPC", replyWord + " in process:" + SystemUitl.getAppName(null));
reply.writeString(replyWord);
return true;
}
}
继承自Binder,并重写onTransact(xx),该方法里获取了来自客户端(进程A)的消息,将消息提取出来并交给业务接口IMyServer调用。
同时将消息回传给客户端。
SystemUitl.getAppName(null)工具为获取当前运行的进程名。
3、编写Service
Binder准备好之后,需要将Binder传递给客户端,传递的方法是通过Service传递。
public class MyService extends Service {
private MyServerBinder myServerBinder = new MyServerBinder();
@Nullable
@Override
public IBinder onBind(Intent intent) {
//返回IBinder引用给调用者
return myServerBinder;
}
}
在Service里构造了MyServerBinder对象,并在onBind(xx)里将引用传递出去。
当客户端绑定这个Service的时候就能拿到该IBinder引用(注意:此处客户端拿到的引用与服务端不是同一个引用,后续会分析此流程)。
编写Client端业务
此时,Server端已经编写完毕,接着来看客户端如何获取IBinder引用。
1、实现ServiceConnection接口
首先实现一个ServiceConnection接口,用以当绑定关系确立后回调该类里的方法:
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//service 为 Server传递过来的IBinder引用
//构造序列化对象
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
//写入String
String word = "hello server, how are you ?";
Log.d("IPC", word + " in process:" + SystemUitl.getAppName(null));
data.writeString(word);
try {
//传递消息给Server
service.transact(2, data, reply, 0);
//收到Server的回复消息
String responseWord = reply.readString();
Log.d("IPC", responseWord + " in process:" + SystemUitl.getAppName(null));
} catch (Exception e) {
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
在onServiceConnected(xx)拿到IBinder引用:service。
先构造待发送的数据,然后调用IBinder transact(xx)发送数据给服务端。
2、绑定服务端的Service
客户端通过绑定服务端的Service来建立绑定关系。
private void bindService() {
Intent intent = new Intent(MainActivity.this, MyService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
用图来梳理以上流程:
最后来看看通信结果:
从日志结果看:Client与Server分别打印了两条日志,说明通信成功了。
5、AIDL的引入
以上是借助Binder来进行两个进程间简单通信,功能是实现了,但是你可能发现了一些端倪:
1、编写冗余
在onTransact(xx)里只调用了say(xx)一个方法,如果需要调用另一个方法,那么就要对code进行区分:
@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
switch (code) {
case 1:
iMyServer.say(data.readString());
break;
case 2:
iMyServer.say1(data.readInt());
break;
case 3:
iMyServer.say2(data.readFloat());
break;
}
return true;
}
同样的,在客户端发送消息的时候也需要区分code。Server端提供的业务只有几个接口还好,若是十几个接口甚至更多,书写case不仅是个体力活,还容易出错。
2、序列化数据
在客户端发送数据前,先将数据序列化,在服务端接收数据处理前,先将数据反序列化。这个过程也是个重复的体力活,实际上双方都不关心具体的序列化细节,只知道丢个参数进去,弄个参数出来即可。
3、面向对象
客户端先要调用transact(xx),服务端在onTransact(xx)里调用say(xx)方法。
这么看起来有点绕,实际上比较好的方式是客户端"直接"调用服务端的say(xx)方法:
如上,Client看起来直接调用了Server的接口,就像是Client拿到了Server对象引用然后操作之,和在同一进程里的操作一样,符合面向对象操作的习惯,大大降低了违和感。
说了这么多直接使用Binder编码的缺点,那么是否有一种方法能解决上面的问题呢?
没错就是AIDL。
接下来的文章将着重分析AIDL原理及其使用。
本文基于Android 10.0。