AIDL中的方法名陷阱

前些日子在调试AIDL时出现了客户端与服务端方法调用不一致的情况。由于之前AIDL开发经验不足,这个问题还是折腾了一会儿,在此记录。

场景重现

在客户端和服务端创建相同的AIDL文件,并且定义两个方法A和B,均返回String以做测试:

1
2
3
4
5
6
interface IMyAidlInterface {

String methodA();

String methodB();
}

服务端的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public IBinder onBind(Intent intent) {
return new IMyAidlInterface.Stub() {
@Override
public String methodA() throws RemoteException {
return "method A from service";
}

@Override
public String methodB() throws RemoteException {
return "method B from service";
}
};
}

客户端调用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private IMyAidlInterface mInterface;

private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
mInterface = IMyAidlInterface.Stub.asInterface(iBinder);
}

@Override
public void onServiceDisconnected(ComponentName componentName) {
mInterface = null;
}
};

public void performA(View view) {
try {
final String resultA = mInterface.methodA();
Log.d(TAG, "执行A -> " + resultA);
} catch (RemoteException e) {
e.printStackTrace();
}
}

public void performB(View view) {
try {
final String resultB = mInterface.methodB();
Log.d(TAG, "执行B -> " + resultB);
} catch (RemoteException e) {
e.printStackTrace();
}
}

依次执行方法A和B,日志如下:
image
没问题,按预期的结果返回了。
后来由于业务的变动,在服务端的AIDL中增加了测试方法,客户端不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 服务端AIDL
interface IMyAidlInterface {

String test();

String methodA();

String methodB();
}

// 服务端实现
@Override
public IBinder onBind(Intent intent) {
return new IMyAidlInterface.Stub() {
@Override
public String test() throws RemoteException {
return "test from service";
}

@Override
public String methodA() throws RemoteException {
return "method A from service";
}

@Override
public String methodB() throws RemoteException {
return "method B from service";
}
};
}

此时在客户端依次执行方法A和B,发现结果竟变成了这样:
image
客户端调用methodA(),服务端执行了test();客户端调用methodB(),服务端执行了methodA(),服务端竟然没有按客户端调用的方法名执行,而是按顺序执行的。

原因探究

我们先看一下AIDL调用过程(底层暂且不表),以上述正常情况的调用为例。mInterface通过IMyAidlInterface.Stub的asInterface方法得到:

1
mInterface = IMyAidlInterface.Stub.asInterface(iBinder);

入参类型为IBinder,由于上述的客户端和服务端为两个应用不在一个进程,iBinder为远端service中的Binder代理(即BinderProxy)。看一下asInterface方法:

1
2
3
4
5
6
7
8
9
10
public static com.robog.aidldemo.IMyAidlInterface asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.robog.aidldemo.IMyAidlInterface))) {
return ((com.robog.aidldemo.IMyAidlInterface) iin);
}
return new com.robog.aidldemo.IMyAidlInterface.Stub.Proxy(obj);
}

如果同进程直接返回IInterface,即当前的Stub对象。不在一个进程返回的是IMyAidlInterface.Stub.Proxy(obj)。看一下静态内部类Proxy的构造方法:

1
2
3
4
5
private android.os.IBinder mRemote;

Proxy(android.os.IBinder remote) {
mRemote = remote;
}

接着执行mInterface.methodA(),即调用Proxy的methodA方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public java.lang.String methodA() 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);
mRemote.transact(Stub.TRANSACTION_methodA, _data, _reply, 0);
_reply.readException();
_result = _reply.readString();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}

实际上调用了mRemote(BinderProxy)的transact方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");

if (mWarnOnBlocking && ((flags & FLAG_ONEWAY) == 0)) {
// For now, avoid spamming the log by disabling after we've logged
// about this interface at least once
mWarnOnBlocking = false;
Log.w(Binder.TAG, "Outgoing transactions from this process must be FLAG_ONEWAY",
new Throwable());
}

final boolean tracingEnabled = Binder.isTracingEnabled();
if (tracingEnabled) {
final Throwable tr = new Throwable();
Binder.getTransactionTracker().addTrace(tr);
StackTraceElement stackTraceElement = tr.getStackTrace()[1];
Trace.traceBegin(Trace.TRACE_TAG_ALWAYS,
stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName());
}
try {
return transactNative(code, data, reply, flags);
} finally {
if (tracingEnabled) {
Trace.traceEnd(Trace.TRACE_TAG_ALWAYS);
}
}
}

通过调用native方法transactNative经过Framework以及Kernel层(有兴趣的同学可以参考这篇文章),最后调用到service端onTransact方法的返回后才会执行结束。我们看一下service端IMyAidlInterface.Stub()中onTransact的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_methodA: {
data.enforceInterface(DESCRIPTOR);
java.lang.String _result = this.methodA();
reply.writeNoException();
reply.writeString(_result);
return true;
}
case TRANSACTION_methodB: {
data.enforceInterface(DESCRIPTOR);
java.lang.String _result = this.methodB();
reply.writeNoException();
reply.writeString(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}

可以看到,onTransact方法中会根据code值执行相应的方法,这些方法即为IMyAidlInterface.Stub()中实现的方法。
总结一下流程(省略底层):
image
现在我们知道服务端会根据code值决定执行哪个方法,我们回过头看一下客户端传的code值:

1
2
3
// int FIRST_CALL_TRANSACTION  = 0x00000001
static final int TRANSACTION_methodA = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_methodB = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

该code值根据方法顺序依次加1,查看文件可以发现服务端的code值与之一致,此时方法调用没有问题。
再看一下服务AIDL文件中新增test方法后的code值:

1
2
3
static final int TRANSACTION_test = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_methodA = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_methodB = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);

此时客户端如果调用methodA(客户端code值为1),服务端会调用code值为1的方法,即test方法。

总结

  1. 谷歌为Android的跨进程通讯设计了Binder机制,并提供了一套傻瓜式的接口(AIDL接口)让我们方便使用。AIDL的作用是为了简化代码的书写,如果不嫌麻烦我们完全可以抛弃AIDL,自己用代码实现。
  2. Binder本身能读懂的是为int类型的code而非方法名,AIDL接口中的方法名申明只是为了我们方便使用,在内部会把这些接口请求翻译成Binder明白的请求。
  3. 如果为了测试需要在服务端增加方法,为了不影响客户端调用,方法最好加在最后。当然,尽量保持客户端与服务端的AIDL文件的一致性。
  4. 我们知道transact方法会等待onTransact方法返回后才会结束(onTransact方法一般在Binder_n线程中执行)。因此,客户端最好在线程中调用AIDL方法,不然一旦服务端的实现比较耗时不仅会影响体验,更可能会导致ANR。

参考

  1. 彻底理解Android Binder通信架构
  2. 一篇文章了解相见恨晚的 Android Binder 进程间通讯机制