本篇包含进程间通信——AIDL所涉及到的知识的自我总结(内容详细)
通过前段时间对AIDL的学习以及最近一些资料的查阅,特在此总结一下我所理解到的AIDL,下文将从以下几个方面来分析。
- WHY(目的)
- Support(支持的数据类型)
- How(如何使用)
- What's The Meaning(AIDL生成的java文件各部分的含义)
- Permission(客户端调用时的权限验证)
- Best Practice(使用连接池管理)
注:在学习AIDL之前需要有一定的基础,需要了解序列化以及Service的使用。
一、WHY
首先进程间通信(IPC,Inter-process Communication)的方式有很多,例如Bundle、文件共享、AIDL、Messenger、ContentProvider以及Socket,方法是多种多样,而我们更应该根据自己的情况去选择合适的方式,下面列举出每种方式的适用场景:
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件间的进程通信 |
文件共享 | 简单易用 | 不适合高并发场景,无法及时通信 | 无高并发访问,交换简单数据,实时性不高 |
AIDL | 功能强大,支持一对多并发通信,支持及时通信 | 使用复杂,需要处理线程同步 | 一对多且有远程调用需求 |
Messenger | 功能一般,支持一对多串行,支持实时通信 | 不能很好处理高并发,不支持远程调用,数据只能使用Message传输 | 低并发的一对多即时通信,无须直接返回结果,无远程调用 |
ContentProvider | 数据访问方面功能强大,支持一对多并发数据共享 | 受约束的AIDL,主要提供数据的增删改查 | 一对多进程间数据共享 |
Socket | 功能强大,支持一对多并发实时通信 | 实现复杂,不支持直接的远程调用 | 网络数据交互 |
注:该表格信息来自《Android开发艺术探究》
从上表格我们可以看出,AIDL出现的目的就是为了解决一对多并发及时通信了,虽然socket也支持,但其更多应用于网络数据交互,所以在Android中,google为我们提供了AIDL来方便开发者实现该功能。
二、Support
在进程间通信中,对象的引用是无法传递的,因为其内存地址都不在一个区域内,所以数据的传输都需要序列化后才行,基础数据类型自然是没问题,系统提供的ArrayList以及HashMap也实现了序列化,所以对于自定义的对象实现序列化就只能实现serializable或者parcelable接口,其中前者是java提供的,后者是Android提供的且效率更高,所以在之后的数据序列化中都只使用实现parcelable接口的方法即可,总的来说支持的数据类型包括:
- 基本数据类型:int、long、char、boolean、double等
- 系统已实现类型:String、CharSequence、ArrayList、HashMap(后两个的每个元素也得被AIDL支持才行)
- 正确实现parcelable接口的对象
- 所有AIDL本身
对于自定义实现parcelable接口的类以及AIDL都必须通过import导入完全限定名才可以使用,而实现parcelable接口的类也需要创建一个与之对应的aidl文件才能被识别,例如:
#Person.java文件
package net.arvin.androidart.aidl;
//省略导入的包
public class Person implements Parcelable {
//省略具体内容
}
#Person.aidl文件
package net.arvin.androidart.aidl;parcelable Person;
其中aidl的包名要相同才能正确的找到相应的java文件。
除此之外还有一个知识点那就是对于非基础数据类型的类型,在aidl中作为参数使用时有三个标记,分别时in、out、inout,表示数据的流向:
名称 | 含义 |
---|---|
in | 表示能读取到传入的对象中的值,而修改后不会修改传入对象的内容 |
out | 表示读取到传入的对象为空,修改后会把传入对象的内容改为修改的内容 |
inout | 表示既能读取到对象,又能修改对象,效果就好比统一进程中对象的传递一样 |
三、How
这里的实现时基于AS的(Eclipse只是创建的文件放置的目录不同,具体细节这里就不再介绍),对于如何实现我认为有以下三部分:
- 定义AIDL接口
- 在服务端返回其Binder以及所需实现的接口
- 在客户端拿到Binder进行调用
下面就一步一步的来介绍如何实现,现在就以客户端需要向服务端添加和查询人物信息为例,其中人物信息只包含名称和年龄即可(这里就不使用基本数据类型了,因为基本数据类型会少了定义AIDL支持的对象这一步)
1、定义AIDL接口
a、创建aidl目录
在src/main下创建一个aidl文件夹,再创建一个包,例如net.arvin.androidart.aidl,建议让所有的aidl在同一个包下,会更方便。
b、定义Person对象并实现parcelable接口
定义和序列化的实现这里就不贴代码了,序列化可通过AS插件生成,需要注意的是这时候只支持in那种标记的数据流向,若要支持out,则还需定义readFromParcel(Parcel in)方法,具体原因在解释每一部分的含义时会介绍。只需要注意,需要在a中所创建的包下创建Person.aidl,然后也需要在main/java下的net.arvin.androidart.aidl包中创建Person.java,其主要原因是gradle默认的java文件都在main/java文件夹下(这里就不提供修改方法了,修改后就可以让.java文件定义在aidl的文件下就可以了),这里就先这样麻烦一点吧。
c、定义通信的AIDL接口
代码很简单,就是在刚才创建的Person.aidl包下创建IPersonCount.aidl文件(文件名可随便定义,后缀却必须为.aidl),内容如下:
package net.arvin.androidart.aidl;
import net.arvin.androidart.aidl.Person;
interface IPersonCount {
boolean addPerson(in Person person);
List getPersons();
}
d、生成Binder
上面三步写好后就可以点击build->make project,如果没有编译出错,那么恭喜你,第一步已经成功了。那么你就可以在你的那个module的build/generated/source/aidl/debug/packagename/下看到所生成的IPersonCount.java了,其中有个静态内部类Stub就是之后需要的Binder了,其他的我们就暂时先不看。
2、在服务端返回其Binder以及所需实现的接口
注意,对于进程间通信有两种情况,一种在同一个应用内,另一种是在不同的应用内;同一应用就没啥可说就可少了接下来的一步,若是不同应用内,我们还需要把第一步中定义的所有文件原封不动的拷贝到另一个应用中,并编译,假如第一步中我们在服务端已经定义好了,那么接下来我们就需要把Person.java、Person.aidl、IPersonCount.aidl三个文件在保证包名不变的情况下拷贝过去并编译,就能生成好同样的IPersonCount.java文件了,这一步一定需要仔细。接下来就开始在服务端返回binder,而我们最开始接触到binder,就是在Service中,然后在bindService的时候就会用到Binder,所以我们就需要定义Service。
这个也很简单,就是定义一个类继承Service,然后实例化IPersonCount.Stub这时候需要实现刚才定义的addPerson和getPerson方法,具体代码这里也不贴了,很简单,然后在Service的onBind中返回刚才实例化的IPersonCount.Stub,然后再在清单文件中去声明改Service,即可。
3、在客户端绑定服务,拿到Binder,并使用
这时候我们回到刚才的客户端中,在Activity中通过bindService去绑定Service,其中Intent设置的ComponentName为服务端中定义的Service的报名和完全限定名,另外ServiceConnection在onServiceConnected的时候会带有刚才服务端返回的binder,这时候我们就可以通过IPersonCount.Stub.asInterface(iBinder)获取到Binder,例如叫做iPersonCount,然后就可以在Activity中使用iPersonCount.addPerson(person)和iPersonCount.getPersons()方法了。
这样整个使用过程就完成了,整体来说还是比较简单的,主要需要注意的就是对于书写aidl文件时类型的引用。最后会附上项目地址。
四、What's The Meaning
在定义AIDL接口一节中说到,最终会生成一个java文件,他其实才是我们应用的核心,AIDL其实只是一种开发语言,方便我们生成那个java文件,帮我们完成了其中每次都差不多的重复工作,虽然系统能为我们完成这一个工作,我们还是应该了解其中每部分到底是做什么的。
先来看看IPersonCount.java这个类,包含了什么东西,代码不少,这里还是不贴了,可自己对照着看,这里只提供一个我所整理的简单的类图:
IPersonCount类关系图
这样一看其实就很清晰了:
- IPersonCount是一个接口,继承自IInterface接口,里边有一个Stub抽象静态内部类以及aidl中定义的方法;
- Stub是继承自Binder并实现了IPersonCount的一个抽象静态内部类,里边有一个静态内部类、三个属性和四个方法,还有两个;
- Proxy是Stub的静态内部类,有一个属性和五个方法,后两个是对IPersonCount接口的实现。
然后一个一个的介绍每个属性和方法的含义:
1、IPersonCount类
这个类就是一个接口,比较简单就不介绍了。
2、IPersonCount.Stub类
a 、DESCRIPTOR:
这是Binder的唯一标识,一般都是当前AIDL生成的类名;
b、TRANSACTION_XXX:
以这个开头的都是统一定义这个方法的code,通过code就能知道调用的是哪个方法;
c、asInterface:
是将传入的Binder转化为相应的AIDL所定义的接口,看其实现可发现queryLocalInterface用于区分是统一进程调用的,还是不同进程调用的,其具体实现在Binder中,在构造函数中赋值,若是同一进程,则使用的其实就是这个Binder,若是不同进程,则调用创建一个Proxy代理,用于调用服务端的onTransact方法,执行具体的实现;
d、asBinder:
这是返回当前对象,因为该对象在服务端实例化后就是一个Binder,这里使用这个方法来定义,我猜想是为了更清晰的表达吧,同时要调整Binder我们也只需要重写这个方法就可以了,也方便;
e、onTransact:
首先了解一下这些个参数的意思:
- code(表示客户端调用的是哪个方法);
- data(表示序列化的传入参数);
- reply(表示序列化回应相关的数据);
- flags(这是一个可选的参数,默认都是0,可以传1,表示立即就有返回,不会等待,只应用在调用者与被调用者在不同的进程中)。
再看其返回值,返回false表示调用不成功,而下文中的权限则可在这里去配置;
再看看其方法的具体实现,首先通过switch区分调用的是哪个方法,然后执行相应的操作,而每个方法其实其流程都差不多,首先获取参数,然后调用该方法对应的实现,最后再通过reply中处理数据。
读取数据,会根据定义的这个方法的类型去读取,如果是基本类型则调用相应的read方法就能获得,例如data.readInt、data.readString等,若是对象则通过readInt判断不为0,再使用那个对象序列化中的CREATOR去createFromParcel来获得对象;
执行相应方法,就是调用该方法,若有返回值则用相应的对象接收;
操作reply,如果有返回值则就通过reply写入,aidl所支持的类型都有相应的write方法,例如:writeTypedList、writeToParcel、writeInt等等;
到这里这个类就差不多结束了,其中对于参数数据流向的标记,下文中再介绍,核心的东西其实就在onTransact中,而与其它的关联则是通过asInterface联系;
3、IPersonCount.Stub.Proxy类
其实这个在上文中有个简单的说明,它的作用就是在asInterface时实例化一个对象返回,当在客户端调用相应方法时,先获取传入数据data,执行transact方法,再处理一下reply用以响应,有返回值的则返回。接下来也依次描述一下各个成员以及方法的含义。
a、mRemote
这个就是asInterface中传入的binder,这里存储起来方便使用;
b、asBinder
返回binder;
c、getInterfaceDescriptor
返回描述与Stub一致;
d、实现接口中定义的方法
这里以addPerson为例,因为数据流向标记中inout会包含in和out两种标记,所以这里看到的是以inout数据流向的代码,下文会逐一分析哪部分是干什么的。
public boolean addPerson(net.arvin.androidart.aidl.Person person) throws android.os.RemoteException {
//前两行是初始化data和reply,data是用来传递参数数据的,reply是用于返回数据以及写入out流向的数据的
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
//这里定义result,会根据相应方法定义对应类型,如果没有返回则不定义该变量
boolean _result;
try {
//这一步都有,将DESCRIPTOR作为令牌记录下档前读取的是哪个Binder,是用来在onTransact时验证
_data.writeInterfaceToken(DESCRIPTOR);
//这一部分是in数据流向的操作,不同的对象写入data的方式不同,如果只是out流向则不包含这一部分
if ((person != null)) {
_data.writeInt(1);
person.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
//这一部分就是调用远程binder中的onTransact方法,其方法中会直接调用onTransact方法,传入的参数和上文中onTransact方法的参数含义一致
mRemote.transact(Stub.TRANSACTION_addPerson, _data, _reply, 0);
_reply.readException();
//这一部分就是从reply中获得result,不同类型获取方式不同,用于最后返回;
_result = (0 != _reply.readInt());
//这一部分就是out数据流向的操作,当然也是不同类型,操作方式不同,如果只是in标记则没有这一部分,需要注意的是写出时的顺序要和写入时数据的顺序相同
if ((0 != _reply.readInt())) {
person.readFromParcel(_reply);
}
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
通过上边方法的注释,我觉得对于这个方法就差不多了,其实就是读取数据,调用远程方法,处理返回数据。
这里再细看细看一下在onTransact方法中不同数据流向代码会是怎样,这里还是以addPerson为例,数据流向也为inout。
case TRANSACTION_addPerson: {
//这一部分很明显就对应着proxy中data的writeInterfaceToken
data.enforceInterface(DESCRIPTOR);
//这一部分就是in数据流向的操作,不同类型之行方法不同,如果只有out则只会初始化一个对象实例,若传入的是空,对应着proxy中data读取就是0,这里也就赋值为空了
net.arvin.androidart.aidl.Person _arg0;
if ((0 != data.readInt())) {
_arg0 = net.arvin.androidart.aidl.Person.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
//这一步是数据逻辑执行的核心,会去调用我们在服务端实现的方法
boolean _result = this.addPerson(_arg0);
reply.writeNoException();
//这一步是将result写入,如果没有返回值则没有这一步
reply.writeInt(((_result) ? (1) : (0)));
//这一步是out数据流向的操作,是将这里操作完的传入的参数写入到reply中,以便在proxy中写出
if ((_arg0 != null)) {
reply.writeInt(1);
_arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
reply.writeInt(0);
}
//这里返回true表示调用成功
return true;
}
这样联通起来一看,就想通,目前来看就对其内部数据存储以及读取时的规则不是很清楚,但是对于使用确实知道都会一一对应有写入就有写出。
这里边还有一个技术点就是调用时,线程的切换问题,当客户端调用服务端的代码时由于不知道远程操作是否耗时,所以尽量都在子线程中调用,而服务端的执行代码,即使是耗时操作也可以不用开新的线程,因为其本身已经在Binder的线程池中调用了;而若是服务端要调用客户端,则同样的道理,只是反过来了,具体的例子会在最后实践中应用。
五、Permission
如果任何一个客户端脸上了我们的服务端,而执行了这些方法,显然时不安全的,而我们也是可以通过一些方法去限制访问,总结起来就两点
- 通过Service连接时限制
- 通过调用onTransact时限制
而我们限制的点,也是多种多样,由于没在项目中使用过,别人的意见是设置权限以及限制包名来限制访问,接下来就依次说说具体的实现。
a、通过Service连接时限制
我们就以权限为例,这里现在清单文件中自定义一个permission:
然后我们在Service中,在onBind方法返回参数前作判断,例如:
int check = checkCallingOrSelfPermission("net.arvin.androidart.permission.BinderPoolService");
if (check == PackageManager.PERMISSION_DENIED) {
return null;
}
这样如果在客户端调用时没有申明这个权限就只会返回空,这样就完成了简单的限制。除此之外对于Service的连接限制还有其它办法,我还没实际运用过就不再介绍了,可以自行查询。
b、通过调用onTransact时限制
从上文的介绍中可以知道,该返回如果返回false则就不会调用成功,所以我们就可以在不符合的条件返回false,方法也很简单,我们只需要重写onTransact方法中去判断即可,例如:
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
int check = checkCallingOrSelfPermission("net.arvin.androidart.permission.BinderPoolService");
if (check == PackageManager.PERMISSION_DENIED) {
return false;
}
String packageName;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
if (!packageName.startsWith("net.arvin")) {
return false;
}
}
return super.onTransact(code, data, reply, flags);
}
逻辑也很简单,就不介绍了,就是限制权限和包名,都通过就执行我们刚才的代码就可以了。
六、Best Practice
注:标题党了一下,毕竟我还没在实践中用过,都是看着别人这么介绍的,自己总结后也确实觉得这样的实践方式还不错,所以才起了这个标题
上面已经简单介绍过AIDL的使用以及客户端和服务端的调用,当时客户端在调用服务端的时候都是直接调用,没有开子线程,如果服务端执行的操作是耗时较长的,则就会出现ANR了。而这一点也只需开线程就能解决就不再详细介绍了,下面着重介绍的是:
- 服务端回调客户端
- 连接池的使用
a、服务端回调客户端
我们发现其实之前的调用都是单向的,往往有时我们希望服务端有的数据变化了能通知客户端,而不用一直去调用才知道,这时候这种方法就派上用场了。
其实原理和在同一个进程中时是一样的,就是客户端向服务端注册一个回调,而当有更新的时候服务端再调用回调就可以了。
当然这个回调也得使用AIDL来定义,保证定义的接口客户端与服务端同步,如下:
// IOnNewPersonIn.aidl
package net.arvin.androidart.aidl;
import net.arvin.androidart.aidl.Person;
interface IOnNewPersonIn {
void onNewPersonIn(in Person newPerson);
}
// IPersonCount.aidl
package net.arvin.androidart.aidl;
import net.arvin.androidart.aidl.Person;
import net.arvin.androidart.aidl.IOnNewPersonIn;
interface IPersonCount {
boolean addPerson(in Person person);
List getPersons();
void registerListener(in IOnNewPersonIn listener);
void unregisterListener(in IOnNewPersonIn listener);
}
然后我们再服务端去实现这个注册与反注册,其中存储用普通的List来存的话直接反注册时无效的,要用RemoteCallbackList
@Override
public void registerListener(IOnNewPersonIn listener) throws RemoteException {
mListeners.register(listener);
}
@Override
public void unregisterListener(IOnNewPersonIn listener) throws RemoteException {
mListeners.unregister(listener);
}
private void onNewPersonIn(Person newPerson) {
int size = mListeners.beginBroadcast();
for (int i = 0; i < size; i++) {
IOnNewPersonIn onNewPersonIn = mListeners.getBroadcastItem(i);
try {
if (onNewPersonIn != null) {
onNewPersonIn.onNewPersonIn(newPerson);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
mListeners.finishBroadcast();
}
这里onNewPersonIn的调用需要在子线程,具体代码中开了个线程去处理,这里就不展示了。
客户端需要做的就是通过binder注册这个回调,然后再转换到主线程中去操作,如下:
private IOnNewPersonIn.Stub onNewPersonIn = new IOnNewPersonIn.Stub() {
@Override
public void onNewPersonIn(Person newPerson) throws RemoteException {
Message message = mHandler.obtainMessage();
message.what = 0;
message.obj = newPerson;
mHandler.sendMessage(message);
}
};
private IPersonCount iPersonCount;
try {
if (iPersonCount != null) {
iPersonCount.registerListener(onNewPersonIn);
}
} catch (RemoteException e) {
e.printStackTrace();
}
@Override
public void handleMessage(Message msg) {
if (msg.what == 0) {
Person person = (Person) msg.obj;
edAge.setText(person.getAge()+"");
edName.setText(person.getName());
}
}
这也是部分代码,具体的操作也很简单,连接得到IpersonCount后,再注册回调,回调中通过Handler处理消息转化到主线程再显示出来。
这样这一部分的大体逻辑就已经出来了,最主要就是Listener的存储那里以及回调的处理要转换到主线程。
b、连接池的使用
如果你按照这种例子多写几个会发现,怎么一个连接的就得写一个Service,这岂不是不好很繁琐,而且Service开多了对性能也有影响,所以我们引入了中间人,帮助我们通过code去连接我们需要的AIDL对应的Binder,而Binder我们通过AIDL就已经实现了大多的逻辑,只需要再去实现定义的具体方法即可。
首先定义AIDL接口
// IBinderPool.aidl
package net.arvin.androidart.aidl;
// Declare any non-default types here with import statements
interface IBinderPool {
/**
* @param binderCode, the unique token of specific Binder
* @return specific Binder who's token is binderCode.
*/
IBinder queryBinder(int binderCode);
}
实现生成的Binder
public class BinderPoolImpl extends IBinderPool.Stub {
public static final int BINDER_COMPUTE = 0;
public static final int BINDER_PERSON_COUNT = 1;
@Override
public IBinder queryBinder(int binderCode) throws RemoteException {
IBinder binder = null;
switch (binderCode) {
case BINDER_PERSON_COUNT: {
binder = new PersonCountImpl();
break;
}
case BINDER_COMPUTE: {
binder = new IntegerAddImpl();
break;
}
default:
break;
}
return binder;
}
}
逻辑很简单,就是根据这里提供的binderCode去查询返回相应的的binder实例。同理,我们可以把字签在PersonCountService中的binder提取出来。
实现BinderPoolService
public class BinderPoolService extends Service {
@Override
public IBinder onBind(Intent intent) {
return new BinderPoolImpl();
}
}
对于配置那些就不贴出来了。
这样服务端的连接池相关代码就已经写完了,接下来再看看客户端的。
由于连接池是为了通用,为了避免重复创建的开销,所以这里会把连接池作为单例模式使用。代码如下:
public class BinderPool {
private static final String TAG = "BinderPool";
public static final int BINDER_COMPUTE = 0;
public static final int BINDER_PERSON_COUNT = 1;
private Context mContext;
private IBinderPool mBinderPool;
private static BinderPool sInstance;
private BinderPool(Context context) {
mContext = context.getApplicationContext();
connectBinderPoolService();
}
public static BinderPool getInstance(Context context) {
if (sInstance == null) {
synchronized (BinderPool.class) {
if (sInstance == null) {
sInstance = new BinderPool(context);
}
}
}
return sInstance;
}
private synchronized void connectBinderPoolService() {
Intent service = new Intent();
service.setComponent(new ComponentName("net.arvin.androidart",
"net.arvin.androidart.multiProcess.BinderPoolService"));
mContext.bindService(service, mBinderPoolConnection,
Context.BIND_AUTO_CREATE);
}
public IBinder queryBinder(int binderCode) {
IBinder binder = null;
try {
if (mBinderPool != null) {
binder = mBinderPool.queryBinder(binderCode);
}
} catch (RemoteException e) {
e.printStackTrace();
}
return binder;
}
private ServiceConnection mBinderPoolConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBinderPool = IBinderPool.Stub.asInterface(service);
try {
mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
}
};
private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
Log.w(TAG, "binder died.");
mBinderPool.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0);
mBinderPool = null;
connectBinderPoolService();
}
};
}
代码也不多,很好理解,里边主要就是三个事:
- 实现单例
- 连接服务端获取Binder
- 断开后重连
前两个技术点没什么好说的都差不多,第三个重连机制,使用的是Binder的DeathRecipient接口回调,使用方法也很简单,就是在获取到binder后linkToDeath,其中传入DeathRecipient接口的实现,若是被断开连接后回调,先调用unlinkToDeath,再将binder设置为空,再重新连接,就实现了整个过程。
然后是客户端对BinderPool的使用
mBinderPool = BinderPool.getInstance(this);
iPersonCount = IPersonCount.Stub.asInterface(mBinderPool.queryBinder(BinderPool.BINDER_PERSON_COUNT));
第一句获取Binder池实例,第二句就是通过binder池获取对应的binder,再获取到相应的接口,这样就能正常使用了,这里就不贴具体调用代码了。
到这里,整篇就结束了,若有差错,请多多指教。
Demo源码
AIDL的Demo