当发起一次AIDL调用时,是如何进行进程间切换的?都经过了哪些步骤?有哪些重要方法?
下面我们用一个例子来具体看一下
这里假定我们声明了一个aidl方法,如下:
interface IMyAidlInterface {
WeatherEntity queryWeather(in RequestData request, in ICallBack callback);
}
下面看一下,一次完整的IPC过程是怎样的,中间都发生了哪些事情
第一步:绑定服务,获取服务端binder引用
Client绑定了service后,首先会拿到server的一个Binder引用:
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 我们这里拿到的对象其实就是其Stub的内部类Proxy对象
myAidlInterface = IMyAidlInterface.Stub.asInterface(service);
上面这步转化,会根据当前client和server是否为同一个进程,如果是不同进程,则返回其内部Proxy代理对象
第二步:客户端发起发起调用
result = myAidlInterface.queryWeather(request, callback);
调用目标方法queryWeather()之后,会按照上面所述走到Proxy.queryWeather()中,逻辑如下:
IMyAidlInterface.Stub.Proxy.queryWeather():
@Override
public com.example.myapplication.WeatherEntity queryWeather(com.example.myapplication.RequestData request,
com.example.myapplication.ICallback callback) throws android.os.RemoteException {
// 获取两个新的Parcel对象,_data是入参,_reply是返回值
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
com.example.myapplication.WeatherEntity _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((request!=null)) {
_data.writeInt(1);
// 序列化参数1 RequestData进_data中
request.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
// 序列化参数2 Callback进_data中
_data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));
// 发起远程调用,remote其实就是我们在onServiceConnected中,获取到的IBinder对象,
// 也就是server端的Binder对象,也就是说在这里将请求转交给了server去处理
mRemote.transact(Stub.TRANSACTION_queryWeather, _data, _reply, 0);
_reply.readException();
// 这里readInt判断是否为0,确认请求是否成功,因为在server端,会判断返回值result是否为null,
// 如果为null则writeInt(0),否则写1
if ((0!=_reply.readInt())) {
_result = com.example.myapplication.WeatherEntity.CREATOR.createFromParcel(_reply);
} else {
_result = null;
}
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
首先初始化两个parcel:传入参数_data和返回参数_reply,
将方法中所有的参数都写入_data中,普通对象调用的是它的writeToParcel方法,
如果参数是一个aidl接口即继承了IInterface接口,则调用的是Parcel.writeStrongBinder方法。将所有参数都写入后,就开始调用mRemote.transact()方法,这里的mRemote其实就是我们在onServiceConnected中, 获取到的IBinder对象,即在server端的Service.onBind方法中返回的Binder对象。在这一步将请求转交给了server去处理。
第三步:Binder.transact()
下面看下Binder的transact(),这个方法携带code(也就是目标方法标示),入参data和返回值reply:
/**
* Default implementation rewinds the parcels and calls onTransact. On
* the remote side, transact calls into the binder to do the IPC.
*/
public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply,
int flags) throws RemoteException {
if (false) Log.v("Binder", "Transact: " + code + " to " + this);
if (data != null) {
data.setDataPosition(0);
}
boolean r = onTransact(code, data, reply, flags);
if (reply != null) {
reply.setDataPosition(0);
}
return r;
}
这里做了两件事:
在调用onTransact()发起调用前后,对参数data和返回值reply重置position,然后调用onTransact(),这下就到了我们服务端中.aidl自己实现的onTransact()方法了。
这个方法如果返回false,表示服务端没有目标方法,原因下面会分析
第四步:服务端onTransact()
服务端的onTransact()方法逻辑如下:
// 根据传入的code参数确定需要调用哪个方法
case TRANSACTION_queryWeather: {
data.enforceInterface(descriptor);
com.example.myapplication.RequestData _arg0;
if ((0!=data.readInt())) {
// 反序列化参数1 RequestData
_arg0 = com.example.myapplication.RequestData.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
com.example.myapplication.ICallback _arg1;
// 反序列化参数2 Callback
_arg1 = com.example.myapplication.ICallback.Stub.asInterface(data.readStrongBinder());
// 然后调用我们在服务端service中实现的目标方法,得到result
com.example.myapplication.WeatherEntity _result = this.queryWeather(_arg0, _arg1);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
// 将result序列化进reply中
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
reply.writeInt(0);
}
return true;
}
在服务端的onTransact()方法中,会根据传入的code参数调用对应方法,如果没有code相对应,则调用父类也就是Binder.onTransact()方法。
Binder.onTransact()中,判断code如果为预置的几种,返回true,否则返回false,那我们传入的code肯定不是预置的,所以这时候会返回false。
这时,可以明确一点:
如果在调用时,mRemote.transact()返回false,则表示服务端没有目标方法。
第五步,service中IMyAidlInterface.Stub子类实现的各个方法
经过上面的调用,逻辑就已经被我们在服务端service处理完了,拿到结果reply后,通过Binder将其返回给client端。到这里为止,一次IPC调用就结束了。
上面的流程可以用下图来说明:
IPC调用时,序列化和反序列化不匹配的问题
这里有一个问题需要注意:从上面的过程可以看出,在一次IPC调用中,一个方法的所有参数,都是写在同一个Parcel中的, 这就要求我们必须同步更新client和server端的序列化字段,保证二者一致,否则就会出现因为序列化和反序列化不匹配,导致空指针等问题。
为了理解上面的问题,我们需要对Parcel有一个简单的认识。
Parcel可以用于跨进程传输数据,它提供了一套机制,可以将序列化后的数据写入一个共享内存,其他进程通过Parcel从共享内存中读出字节流并反序列化成对象。
Parcel是一块连续的内存,并且会根据需要自动扩展其大小。也就是说写入数据时,如果系统发现已经超出了Parcel的存储空间,它会自动申请所需内存并扩展dataCapacity。
在我们向parcel里读写值的时候,parcel内部会维护一个dataPosition,类似于数组的索引,
序列化/反序列化都是严格按照这个position来执行的,我们可以通过setDataPosition()方法来设置该值。
例如上面的例子,我们有两个参数request和callback,假设client的request多了一个字段test, 这两个参数按照定义顺序依次写入parcel中,那在server反序列化时,原本position应该是callback的, 现在变成了新增字段test,这就会导致callback无法正常序列化而变成null,影响正常功能。
如何规避因为序列化和反序列化不匹配导致的问题?
- 在序列化时,可以通过手动setDataPosition(),通过控制position 将新增字段写进固定的没有使用过的位置,从而使其不影响反序列化
- 在写aidl接口时,如果无法做到同时更新client和server,那可以考虑将常改变的参数,例如request定义在后面,例如test(callback, request),从而避免因为request的改变影响callback的序列化。
在AIDL调用中,如何判断server端的目标方法是否存在?
通过上面的分析可以明确,当客户端在调用时,mRemote.transact()返回false,则表示服务端没有目标方法。
在系统根据aidl自动生成的java类中,该方法的返回值是被忽略的,如果要处理这种情况,可以选择自己实现该类,判断如果返回值为false,就抛出异常,然后在方法调用处去处理该异常。
修改后的代码如下:
boolean res = mRemote.transact(Stub.TRANSACTION_queryNumberInfoAsync, _data, _reply, 0);
// If mRemote.transact() return false indicates that the method we call is not exist
// in server, so we need to throw an exception to notify the caller.
if (!res) {
throw new RemoteNoSuchMethodException("The method you are calling queryWeather() is not exist in server!");
}
这里的异常是继承自RuntimeException的自定义异常
上面是对Binder流程的简单分析,如果对AIDL原理不熟悉可以参考我的这篇博客
https://www.jianshu.com/p/a9bf2e513751