Android端的进程间通信(AIDL)
在Android系统中,每个应用程序启动时系统都会为其开辟一个新的进程,并运行在自己的进程空间中。而在Android平台中一个进程通常不能访问另一个进程的内存空间。而AIDL就是Android平台提供的实现进程间通信的一种机制。
AIDL(Android Interface Definition Language)是一种接口定义语言(IDL)。为了实现两个进程之间的通信,进程需要把对象分解成操作系统能够理解的数据格式,以达到跨越进程的边界,到达另一个进程后再进行组装,然而如果自己去编写这些解析和组装的代码将会非常困难并且枯燥无味,因此Android通过AIDL来协助实现这一过程(后面介绍的aidl接口里面的Stub内部类),客户端和服务端就通过这样的编程接口达成了共识(也即另一种意义的通信协议的统一),以便通过进程间通信(IPC)来完成相互通讯。
官方文档特意提起何时使用AIDL是必要的。仅当允许其他应用程序通过 IPC 方式访问服务,并且服务需要多线程运行时,才必须用到 AIDL。如果不需要进行跨越多个应用的并发 IPC,就应该用通过实现一个Binder对象来创建接口。或者要进行 IPC 但不需要多线程运行,则可使用Messenger对象来实现接口。
在Android端使用AIDL的具体步骤
一、 在服务端创建.aidl文件
在项目工程里面创建一个.aidl文件,并在文件中用java语言声明一个带有若干方法(这些方法包含自己定义的用于交换数据的具体逻辑)的接口,这些方法都可以带有参数和返回值,参数和返回值可以为任意类型,甚至可以是另一个由 AIDL 生成的接口。每个.aidl文件中必须也只能定义一个接口,且只能包含接口的定义和方法声明。AIDL默认支持以下数据类型:
l java语言所有简单类型(int、long、char、boolean等)
l String和CharSequence,
l List: 该 List 中的所有数据只能是这里列出的类型、其他某个基于 AIDL 生成的接口、已声明的自定义 Parcelable 类。 List 还可被用作“泛型”类(如 List<String>)。虽然方法是通过 List 接口生成的,但是实际收到的实体类其实会是一个ArrayList 。
l Map:该 Map 中的所有数据只能是这里列出的类型、其他某个基于 AIDL 生成的接口、已声明的自定义 Parcelable 类。这里不支持 Map 泛型(比如 Map<String,Integer> 的形式)。 虽然方法是通过 Map 接口生成的,但实际收到的实体类其实会是一个 HashMap。
对于未列入上述列表的类型,即便是定义于接口所在的包中,也必须包含 import 语句。如下图所示的项目工程文件和接口代码示例
package com.scott.aidl; interface IPerson { /** 在这里可以获取服务的进程 ID 并完成一些用于通信的任务 */ String getPid(String someone); String greet(String someone); } |
要把 .aidl 文件保存到项目的 src/目录下,并用java语言编写用于通信的接口,在编译应用程序时,SDK 工具就会在项目的 gen/ 目录中生成 IBinder 接口文件。文件名与 .aidl 文件名相对应,只是后缀名变成了 .java(此java文件即是AIDL协助生成的通信接口)。
关于上述SDK工具协助生成的java接口的说明,此java文件是系统协助生成的,里面封装了一些用于进程间通信具体细节(即前面提到的用于跨越进程的边界而对对象进行的解析和组装)的代码,格式化后的部分关键代码说明如下:
package com.samsung.aidl;
public interface IPerson extends android.os.IInterface { /** 每个系统协助生成的aidl的java代码都会包含一个继承自Binder 的 Stub内部类,用于处理进程间通信的细节 */ public static abstract class Stub extends android.os.Binder implements com.samsung.aidl.IPerson {
public Stub() { this.attachInterface(this, DESCRIPTOR); } /** 此方法比较重要,用于将传入Ibinder对象转换成Iperson对象,此方法用于在客户端调用,来获得Iperson对象,以便进行方法调用以完成进程间的数据交换 */ public static com.scott.aidl.IPerson asInterface(android.os.IBinder obj) { // …… }
public android.os.IBinder asBinder() { return this; } } } public java.lang.String getPid (java.lang.String someone) throws android.os.RemoteException; } |
|
二、 在服务端实现接口并向客户端公布接口
实现由.aidl文件生成的接口,需要继承Binder接口(如前面定义的IPersion.Stub,对应于后面代码的①),并实现由自己定义的接口方法。向客户端公布接口以供绑定需要继承一个Service并实现其onBind()方法,以便返回自定义的Stub类实例(如上所述,对应于后面代码的②)。同时对此Service服务进行注册,并指定android:action属性,以供客户端绑定。代码示例如下
import com.scott.aidl.IPerson;
public final class AIDLService extends Service { /** 实现由.aidl文件生成的接口,需要继承Binder接口,而IPersion.Stub即是一个Binder子类对象,并实现自己定义的接口方法 */ IPerson.Stub mBinder = new IPerson.Stub() { //① @Override public String greet(String someone) throws RemoteException { return "hello, " + someone; } }; /** 向客户端公布接口以供绑定需要 ,以便返回自定义的Stub类实例 */ @Override public IBinder onBind(Intent intent) { //② return mBinder; } } |
此步骤需要注意以下几点(引自AIDL官网文档翻译):
l 调用不一定是在主线程中运行,因此从一开始就要考虑多线程运行,并保证服务是按照线程安全的模式编写的。
l 默认情况下, RPC调用是以同步方式运行的。如果事先知道服务处理完一次请求需要若干毫秒的时间,就不应该在 Activity 的主线程中发起调用。因为这可能会导致应用程序挂起——通常,这时应该在客户端的单独线程中发起调用。
l 所有异常都不会(Exception)发还给调用者。
三、 在客户端调用服务端公布的接口
若客户端要访问服务端接口,其必须有访问接口类的权限,因此,假如客户端和服务端位于不同的应用,客户端的应用必须在其 src/ 目录下拥有一份 .aidl 文件的拷贝(同时要保证包名的一致性,因为系统协助生成的接口中继承了自定义的接口的完整包名,见前面代码标红部分),用于生成供客户端访问 AIDL 方法的 android.os.Binder 接口。这样就间接实现了双方通信协议的一致性。
客户端要成功调用,需要调用bindService()连接到该服务,客户端的 onServiceConnected() 方法将会收到服务的 onBind() 方法返回的 mBinder 实例。此时调用IPerson.Stub. asInterface(service)方法即可返回的IBinder对象转化为对应的自定义接口对象,此后便可对自定义接口对象进行调用,从而达到进程间通信的目的。
/** 在客户端定义的Iperson对象,用于调用自定义的接口方法 */ private IPerson person; /** 当activity和服务绑定的时候回调用onServiceConnected方法,第二个参数service即是系统获取到的服务端返回的IBinder对象,再调用此对象的asInterface方法即可得到IPersion接口对象,可与前文的stub内部类对应 */ private ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { person = IPerson.Stub.asInterface(service); }
@Override public void onServiceDisconnected(ComponentName name) {
} }; |
四、 跨进程传递对象
如果要跨进程传递某个类,可以通过 IPC 接口来实现。但是需要确保在 IPC 通道的对端可以识别该类的代码,该类必须支持 Parcelable 接口(序列化)。支持 Parcelable 接口非常重要,因为这使得 Android 系统可将对象分解为能够跨进程组装的原生数据。创建支持 Parcelable 协议的类的步骤如下:
1.必须实现Parcelable 接口。
2.实现 writeToParcel 方法,参数为当前对象的状态,并写入一个 Parcel中。读取Parcel数据的次序要和这里的write次序一致,否则可能会读错数据。
3.在类中添加一个名为 CREATOR 的静态成员变量(这个CREATOR命名是固定的),即为一个实现了 Parcelable.Creator 接口的对象。
4.最后,创建 .aidl 文件,声明该 Parcelable 类。代码如下所示
import android.os.Parcel; import android.os.Parcelable; /** 实现的Parcelable接口 */ public final class Rect implements Parcelable { public int left; public int top; public int right; public int bottom; /** 在类中添加的一个名为 CREATOR 的静态成员变量,即为一个实现了 Parcelable.Creator 接口的对象。 */ public static final Parcelable.Creator<Rect> CREATOR = new Parcelable.Creator<Rect>() { public Rect createFromParcel(Parcel in) { return new Rect(in); } public Rect[] newArray(int size) { return new Rect[size]; } };
public Rect() { } private Rect(Parcel in) { readFromParcel(in); } public void writeToParcel(Parcel out) { out.writeInt(left); out.writeInt(top); out.writeInt(right); out.writeInt(bottom); }
public void readFromParcel(Parcel in) { left = in.readInt(); top = in.readInt(); right = in.readInt(); bottom = in.readInt(); } } |
Android端AIDL的实现细节和原理
AIDL是一个接口描述文件,用于实现Android平台上面的RPC,在编译的时候系统会自动根据规则生成用于IPC的接口和对象,而作为使用者只需要:1.在服务端Service实现接口;2. 在客户端bindService,onServiceConnected时获取接口对象。这里的接口都是AIDL中描述的接口,其他的细节则在由AIDL生成的同名源码文件中。通过查看gen文件夹下生成的与AIDL文件同名的源码文件,可以了解AIDL的本质,现将源码的主要部分展示如下(说明:此源码文件会在服务端和客户端同时拥有一份,原因见上文):
public interface IPerson extends android.os.IInterface { public static abstract class Stub extends android.os.Binder implements com.scott.aidl.IPerson {
public static com.scott.aidl.IPerson asInterface(android.os.IBinder obj) { if ((obj == null)) { return null; } android.os.IInterface iin = (android.os.IInterface) obj.queryLocalInterface(DESCRIPTOR); if (((iin != null) && (iin instanceof com.scott.aidl.IPerson))) { return ((com.scott.aidl.IPerson) iin); } return new com.scott.aidl.IPerson.Stub.Proxy(obj); } @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { //② switch (code) { case TRANSACTION_greet: { data.enforceInterface(DESCRIPTOR); java.lang.String _arg0; _arg0 = data.readString(); java.lang.String _result = this.greet(_arg0); reply.writeNoException(); reply.writeString(_result); return true; } } return super.onTransact(code, data, reply, flags); } private static class Proxy implements com.scott.aidl.IPerson { private android.os.IBinder mRemote; Proxy(android.os.IBinder remote) { mRemote = remote; } public java.lang.String greet(java.lang.String someone) throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); java.lang.String _result; try { _data.writeInterfaceToken(DESCRIPTOR); _data.writeString(someone); mRemote.transact(Stub.TRANSACTION_greet, _data, _reply, 0); // ① _reply.readException(); _result = _reply.readString(); } finally { _reply.recycle(); _data.recycle(); } return _result; } } static final int TRANSACTION_greet = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); } public java.lang.String greet(java.lang.String someone) throws android.os.RemoteException; } |
首先在上述代码中的接口里(代码中标红部分),有一个在.aidl文件里自己定义的方法;一个继承自Binder的Stub内部类,这个就是我们要在service里面要实现的基类;还有一个proxy的内部类。从使用者的角度来看这个源码,其大体过程如下:最外层是一个与AIDL同名的接口,里面有自己定义的函数声明。客户端使用IPerson.Stub.asInterface,可以看到这个方法返回了一个IPersion对象。另外就是Server端会让Service实现IPersion.Stub,其实是就是实现了IPersion接口,因为Stub也扩展了IPersion接口(见上述代码)。
通过源码分析可以看出,当通讯的双方在同一个进程中时,onServiceConnected传回的对象是Service.onBind()所返回的对象;但如果是跨进程时,则其返回的是一个BinderProxy对象。所以,可以看到在AIDL生成的代码中有如下判断:
android.os.IInterface iin = (android.os.IInterface) obj.queryLocalInterface(DESCRIPTOR); if (((iin != null) && (iin instanceof com.scott.aidl.IPerson))) { return ((com.scott.aidl.IPerson) iin); } return new com.scott.aidl.IPerson.Stub.Proxy(obj); |
这实际上就是判断此通讯是在同一进程中,还是跨进程,因为同一进程传回的对象是Service.onBind()所返回的对象,而此对象必然实现了接口。所以,如果仅是在同一个进程之中,就不会走Binder进程的IPC,而是直接返回Service所提供的对象,直接调用其方法。
当在不同的进程中时,客户端调用Stub.asInterface会返回一个Stub.Proxy对象,调用其中自定greet方法,并调用Binder. transact()来进行远程方法调用(如上述标红的①)。而服务端仅会执行Stub.onTransact()方法(如上述标红的②),然后就会调用Service端的自定义greet方法了,从而对客户端所请求的方法做出响应。对于上层使用者来说,用transact()把函数的信息(参数,标识和开关)发送出去,剩下的就是Binder的工作了,内部还有大量的细节,但是最终会调用到服务端Binder的onTransact()方法,然后调用具体的实现,再传回返回值,这样一个IPC的函数调用就完成了。
就本质来看,AIDL的作用就是对Binder的二个方法:Binder.transact()和Binder.onTransact()进行封装,而其中便封装了上文提到的对传输对象进行解析和组合的代码,以供Client端和Server端进行使用。因为实现transact()和onTransact()方法的方式基本上是相同的,所以就可以用模板来生成具体的代码。理论上讲只需要为Client端生成transact()相关代码,为服务端生成onTransact()代码即可,但因为工具无法准确的确定某一个应用到底是Client端还是Server端,所以它就生成所有的代码,放在一个文件中。这就是所看到的自动生成的文件。