-
何为AIDL
Android Interface Definition Language即Android接口定义语言
我们知道,因为用户需要,Android系统中可以同时运行着多个进程,为了进程间能够正常的运行,进程之间必须保持独立互不干扰,这就是进程隔离。但是很多时候我们又不得不和另一个进程通信,那么这种进程间的通信就叫做IPC(Inter-Process Communication)。实现IPC的方式有很多种,像管道、信号、消息队列、共享内存、Socket等,RPC(Reomote Procedure Call,译称远程过程调用)也是IPC的一种方式,AIDL使用的就是这种方式。
-
AIDL原理简述
图片来源:Binder学习指南 (ps:这篇文章对Binder机制写的不错,值得一看)
简单来说,就是方法提供方称为server,方法调用方称为client,client和server之间的交互就是IPC。首先server注册到一个称为ServiceManager的地方,client调用时会去查询该server是否已经注册,若已注册则取到server的代理对象并执行要调用的方法,对于调用方来说就和调用自己进程中的方法一样,但是背后的工作确是由Binder驱动转交给真正拥有该方法的server去执行并把结果返回给调用方client。
-
AIDL是如何做的
我们还是从使用开始,一步步揭开AIDL的神秘面纱
-
创建AIDL文件
在src/main下新建文件夹aidl,在其下新建一个.aidl文件,如果你是使用studio的创建AIDL文件功能选项创建的aidl文件,则会看到自动生成了一个basicTypes方法:
这个方法里面的参数就是aidl支持的基本类型(你可以把这个方法删掉也不会有什么问题),除此之外,最好都需要import语句导入(List也可以不用导包,但是是建立在存在jdk的基础上),所以最规范的做法就是除基本类型之外的类型都要导入,和java语法一样,导入为止如下图:
当需要自定义的数据类型时,首先要求这个自定义类型是parcelable类型,并实现Parcelable需要的所有东西,这里值得注意的是,因为AIDL规定除了基本数据类型之外,其他所有类型都需要定向tag显示声明,有in、out、inout三种,基本数据类型默认都是in(只能是in),后面会讲到其作用,这里我们只要记得如果要支持out或者inout定向的话要手动添加readFromParcel()方法,因为这个方法Parcelable接口中没有,需要自行添加:
其次需要定义此类的aidl文件,这个aidl文件因为不是接口所以不会被自动生成(generated):
特别注意: Demo02.aidl的包名要和Demo02.java的包名完全一致!!
然后在要使用这个类的aidl文件中引入:
可以看到pringtList和addDemo02方法中的参数都不是基本类型,所以前面都必须有一个定向tag修饰(这里是in),那么in、out、inout的意义是什么呢?简单来说,根据前面我们提到的进程间通信的机制,通信的双方是client和server,当我们从client传递一个引用类型的数据给server端的时候,如果server端修改了数据,若定向是in,则client的数据不会被改变,若是out或者是inout则client端的数据也会跟着改变(比如说你调用test(a),如果服务端处理时a的某些属性值变了,那客户端的a对象的对应属性值也会跟着改变);同理如果定向修饰为out,则server端不会收到client发送的引用数据类型(比如说你调用test(a),服务端是不会使用这个a的,它只会重新创建一个同样类型的新对象)。这里可以发现,只有引用变量才可能被改变,因为引用数据类型有地址指向,所以这也就是为什么基本数据类型默认是in定向修饰符,而且只能是in,这里其实有一个readFromParcel的操作,后面会看到。关于定向tag,这篇文章讲的很详细:你真的理解AIDL中的in、out、inout么?
好了到现在为止我们已经做好了所有准备工作,现在只需要点击一下studio的rebuild project就会看到在对应文件夹中自动生成的java文件,我猜测studio版本和gradle版本不同,可能生成的目标目录也会不一样,所以不要死记这个路径,但肯定会在build/generated目录下:
下面我们去看看给我们自动生成的java接口类:
可以看到我们的接口继承了一个android.os.IInterface接口:
没什么特别含义,就是为了转化成IBinder类型的。
再来看内部,除了简单的声明了我们aidl中定义的接口方法之外,静态内部抽象类Stub是自动帮我们创建的内容,它继承了android.os.Binder并实现了外部的AIDL接口,它是AIDL的核心。
-
使用AIDL
我们使用常见的Activity和Service的通信来梳理AIDL的工作,首先创建Service:
注意这里的Service可以被多个主题绑定,所以这里的方法要考虑线程安全,其次,因为binder是匿名类实现,同时,它又是私有变量只通过onBind方法暴露,所以这里自定义的方法不能够被外部调用。
再来看一下client端关键代码:
mAidlService是一个AidlTest02类型的变量,在service绑定成功之后,通过AidlTest02.Stub.asInterface(IBinder)方法赋值,我们看看这个方法:
这里出现了两个return分支,本地调用返回和远程调用返回。
-
本地调用
如果在单个App的内部通过bindService实现Activity和Service交互,就会走queryLocalInterface本地获取,此时这里的obj就是我们的Service里面定义的AidlTest02.Stub的匿名类对象binder,通过onBind方法传到了这里,如果不为空则调用它的queryLocalInterface方法去获取一个IInterface类型的对象,这是IBinder接口的的方法,因为我们的Stub继承了Binder,所以我们直接去Binder下面去找这个方法的实现:DESCRIPTOR是Stub内部定义的常量,owner参数就是Stub本身,因为Stub实现了AidlTest02接口,AidlTest02又实现了IInterface接口。
另外,因为是直接使用本地的binder对象,并没有走Proxy逻辑,所以本地调用时定向tag是无效的。
-
远程调用
因为是Android间的系统进程间通信,所以不是传统意义上以网络为传输介质的RPC,这里的远程调用就是App之间的进程通信。我们需要创建两个App(一个项目中的两个Module即可),两个App分别是client端和service端,都需要共同的aidl文件,而且他们的包名要严格相同 。
这里只贴一下另一个Module中Service相关代码:
下面部分截图还是上面的本地调用的,不过原理是一样的,没什么影响,AidlAutoCreateTest03是我新建的远程服务端aidl文件,这里有三个方法分别使用了in、out和inout定向,下面有一些表现出关于in、out和inout的不一样的地方我会用这个新的远程代理服务的一些源码截图说明。
如果是远程调用,那对于这个asInterface方法来说,参数IBinder其实是client的服务端代理,所以这里会返回一个Stub的内部类Proxy对象,它的构造方法里的IBinder就是服务端注册给client的代理。若现在我们调用getDemo02()方法,其实调用的就是Proxy的getDemo02方法:
取得了两个Parcel对象,_data是用来保存Client传递过来的数据的, _reply是用来保存返回给调用方(client)的数据的。上面的是无参数的所以 _data没有存储额外数据的操作,看下面这段代码:
client传递的数据是demo,demo.writeToParcel(_data,0)保存到 _data。
然后通过mRemote.transact方法(mRemote此时是代理Binder)调用服务端的真正对象的方法,这个方法后面做的工作牵扯到了Binder机制的深层逻辑,这里先不讨论,后面单独整理,现在只要知道,调用这个方法之后,最后会走到Stub的onTransact方法里(我们之前在Service里面定义的叫binder的Stub匿名类对象):
看到this.printList、this.getDemo02、this.addDemo02了吧,这调用的就是我们在Service里面定义的那个匿名类对象的对应方法。对于getDemo02方法,因为没有传递数据进来,所以没有从data中取数据的操作,但是有返回值,所以通过_result.writeToParcel(reply,android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE)方法把getDemo02的返回值写到reply;而对于addDemo02方法,会通过Parcelable类型的CREATOR从之前mRemote的transact方法传递进来的data构造出当时客户端传递的真实数据,然后传递给真正的addDemo02()方法,因为没有返回值,所以这里reply并没有执行操作。
中间还有部分代码是Proxy和Stub的之间的数据传递的,包括判断客户端传递的非基本类型参数或引用型返回值是否为null,通过writeInt和readInt是否为0来判断是否有必要将参数继续往下传递。
值得注意的是:
-
out定向
如果AIDL的方法参数是out定向,则服务端不会收到客户端的数据,或者说它不会去取,Proxy中是这样做的:Stub的onTransact方法里是:
可以看到,它是直接new了一个新对象,同样,作为对比,in和inout是这么做的:
这里额外说明一下这个地方:
这就是写回给client传递过来的对象的值的地方,只有out和inout会有,这就是前面说的为什么如果有out或者inout定向的话要给自定义Parcelable类手动加上readFromParcel方法的原因。
-
in定向
in定向修饰的话就是参数会使用客户端传递过来的,Proxy中:
Stub中:
同时,它不会将参数保存到reply中,Proxy中也不会有dd.readFromParcel(reply)的操作。
-
inout定向
inout没什么特殊的,就是兼备了in和out。
-
总结
到此为止,我们顺利地梳理了一遍AIDL从创建aidl文件到使用AIDL机制来实现跨进程通信地流程,不过其中还有bindService方法之后Android框架做了什么、onTransact方法中return true之后Binder是如何去做的问号,后面有时间会研究一下。