用过aidl的同学,可能见过下面的写法:
interface IInterface {
void foo0(in int input);
void foo1(out IDTParcel parcel);
void foo2(inout IDTParcel parcel);
}
不知道你有没有好奇过这里的 in / out / inout 是什么意思呢?
directional tag
去官网一查,只找到一点点信息:
All non-primitive parameters require a directional tag indicating which way the data goes. Either
in
,out
, orinout
(see the example below).Primitives,
String
,IBinder
, and AIDL-generated interfaces arein
by default, and cannot be otherwise.Caution: You should limit the direction to what is truly needed, because marshalling parameters is expensive.
哦,原来这里的 in / out / inout 属于 directional tag (定向标签?)的概念,指的是which way the data goes
(数据以哪种方式流动?),啥意思?从概念到解释都不是人话;不满意的你继续搜索相关博客......
directional tag 不是什么?
在说清楚它是什么之前,先聊聊directional tag不是什么:
如果你搜索aidl in out这几个关键词,会有很多文章出来,很多文章的结论是这样的:
AIDL中的定向 tag 表示了在跨进程通信中数据的流向
其中 in 表示数据只能由客户端流向服务端out 表示数据只能由服务端流向客户端inout 则表示数据可在服务端与客户端之间双向流通。
Stack Overflow上也一样:
Here it goes,
- Its only a directional tag indicating which way the data goes.
in - object is transferred from client to service only used for inputsout - object is transferred from client to service only used for outputs.inout - object is transferred from client to service used for both inputs and outputs.
(为了避免部分追求“效率”的读者只读关键词,文中错误的结论都会加中划线)
上面的结论听着很有道理,但你可能会发现一个问题:接口回调的场景无法实现了!
在aidl中,如果client向server注册一个Callback(如下代码所示),server会在某些场景回调client,这时候数据流向是server => client, 按照上面的逻辑,这个result数据无法到达client,因为int数据的directional tag只能是in(后面会讲到),
而in只能支持client到server的数据传输方向
//aidl file
interface ICallback {
void onResult(int result);
}
//aidl file
interface IController {
void registerCallback(ICallback callback);
}
但是,如果使用过AIDL,会发现接口回调是可以正常工作的(验证demo地址结果如下),否则我们早就发现这个高频使用场景的异常了。
D/directional tag: server register callback
D/directional tag: client onResult: 1
结论和事实有冲突,假设(上面的结论)一定有问题!
大家得出这个错误结论是情有可原的,毕竟对于大多数开发者,AIDL“听得多,用得少”,第一个人在写Demo验证的时候场景特殊,基于这个特殊场景得出的结论就是错误的。
其实这也是刺激我写下本文的原因,因为全网浏览量最高的博客(几乎)全都讲错了,真是生气又骄傲~
那么 directional tag 到底是什么呢?
下面我们就一步一步来验证:
源码之下
要弄清楚究竟发生了什么,源码之下毫无秘密。
为了避免部分同学一脸懵逼,这里补充一点关于AIDL的前置知识:
AIDL作为一种跨进程通信的方案,底层依赖Binder,跨进程通信时会调用AIDL中定义的方法,会把 caller(调用者,后文只用caller)的参数数据 copy 到 callee(接收者,后文只用callee),然后在callee进程中调用另外一个代理对象的相同方法,这个逻辑由Binder框架封装;使用者上层看起来,感觉是直接调用了对方进程中对象的方法。
AIDL文件在编译后会生成2个重要的实现类:
Stub
callee被调用时,会通过Stub.onTransact(code, data, reply, flag)间接地调用本地对象(Local Binder)的对应方法。Proxy
caller调用AIDL方法时,最终通过Proxy调用remote.transact(code, _data, _reply, flag),然后通过Binder机制调用到远程的相应方法。上面的onTransact() 和 transact() 方法都是Binder定义的方法,更底层的跨进程逻辑由Binder机制实现,就不是本文的重点了。
有了这些基础知识,下面我们写一个AIDL文件,看一下对应的方法做了什么事情,全部代码请看这里。
//aidl file: State
parcelable State;
//aidl file: IController
interface IController {
int transIn(in State state);
int transOut(out State state);
int transInOut(inout State state);
}
AIDL文件IController
编译后的关键代码如下:
in
//Proxy(caller)
public int transIn(com.littlefourth.aidl.State state) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
if ((state != null)) {
_data.writeInt(1);
//将state数据写入_data
state.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
//传输数据,并调用callee的transIn()
mRemote.transact(Stub.TRANSACTION_transIn, _data, _reply, 0);
//读取返回值
_result = _reply.readInt();
return _result;
}
//Stub(callee)
case TRANSACTION_transIn: {
com.littlefourth.aidl.State _arg0;
if ((0 != data.readInt())) {
//根据传入的data创建State对象
_arg0 = com.littlefourth.aidl.State.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
//调用callee实现的transIn()
int _result = this.transIn(_arg0);
//写入返回值
reply.writeInt(_result);
return true;
}
输出日志:
caller value before transIn(): 1
callee transIn(), value: 1
callee set value to 2
caller value after transIn(): 1
out
//Proxy(caller)
public int transOut(com.littlefourth.aidl.State state) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
//_data中没有写入state数据
mRemote.transact(Stub.TRANSACTION_transOut, _data, _reply, 0);
//读取返回值
_result = _reply.readInt();
if ((0 != _reply.readInt())) {
//读取callee更新后的state数据
state.readFromParcel(_reply);
}
return _result;
}
//Stub(callee)
case TRANSACTION_transOut: {
com.littlefourth.aidl.State _arg0;
//直接创建新的State对象
_arg0 = new com.littlefourth.aidl.State();
//调用callee实现的transOut()
int _result = this.transOut(_arg0);
//写入返回值
reply.writeInt(_result);
if ((_arg0 != null)) {
//写入标志位, caller根据这个数据判断有没有写入state数据
reply.writeInt(1);
//写入state数据(不管数据是否更新,都会写入全量数据)
_arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
reply.writeInt(0);
}
return true;
}
日志输出:
caller value before transOut(): 1
callee transOut(), value: -1000
callee set value to 2
read new value 2
caller value after transOut(): 2
inout
//Proxy(caller)
public int transInOut(com.littlefourth.aidl.State state) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
if ((state != null)) {
_data.writeInt(1);
//写入state数据到_data
state.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
//传输数据,并调用callee的transInOut()
mRemote.transact(Stub.TRANSACTION_transInOut, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
if ((0 != _reply.readInt())) {
//读取callee更新后的state数据
state.readFromParcel(_reply);
}
return _result;
}
//Stub(callee)
case TRANSACTION_transInOut: {
com.littlefourth.aidl.State _arg0;
if ((0 != data.readInt())) {
//根据data创建State对象
_arg0 = com.littlefourth.aidl.State.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
//调用callee实现的transInOut()
int _result = this.transInOut(_arg0);
//写入返回值
reply.writeInt(_result);
if ((_arg0 != null)) {
//写入标志位, caller根据这个数据判断有没有写入state数据
reply.writeInt(1);
//写入state数据(不管数据是否更新,都会写入全量数据)
_arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
reply.writeInt(0);
}
return true;
}
日志输出:
caller value before transInOut(): 1
callee transInOut(), value: 1
callee set value to 2
read new value 2
caller value after transInOut(): 2
directional tag 到底是啥?
根据源码和 demo 的验证结果,我们可以得出结论了:
Directional Tag | Desc |
---|---|
in | 数据从 caller传到 callee,callee 调用结束后不会把数据写回 caller 中。 |
out | caller 数据不会传入 callee(因为就没有写数据), callee 调用结束后(不管数据有没有更新)会把数据写回 caller 中。 |
inout | 数据从 caller 传到 callee,callee 调用结束后(不管数据有没有更新)会把数据写回 caller 中。 |
提了这么多次 caller 和 callee ,是不想把它们与 client 和 server 混淆;因为 client 与 server 可以互相调用,AIDL文件编译后的代码是一样的,client 与 server 在作为 caller 或 callee 时执行的(AIDL层)逻辑是相同的,所以不能说in / out / inout 是明确地表示 client 到 server 的方向(或者相反)。
这个 out 有什么用呢?
读到这里,估计你已经弄清楚 directional tag 是什么了,但有一个疑问:
out 有什么用呢?caller 连数据都不发送?却要读 callee 写回来的数据?
这个疑问太合理了,毕竟很多用过 AIDL 的朋友从来没有注意过这里的区别,然后在部分编译报错时根据提示填入一个 in,发现逻辑挺正常的,然后就结束了,也没出过问题。
在回答这个问题之前,有另一个要先解决:
> 为什么要有 directional tag 这个东西?
在同一个进程中调用方法时不需要 directional tag 这种东西,为什么在跨进程的场景就需要这个东西呢?
在同一个进程中,对象属性的修改直接体现到之后的上下文中,因为它们访问了相同的内存地址。
在Binder的跨进程机制中,(从上面的源码也可以看出)每一次调用都要把数据从 caller 复制到 callee, 并不是同一块内存,callee 对数据的修改也就不会(自动地)体现在 caller 的数据中。这个跨进程数据传递过程叫marshaling(翻译为数据编组?,总之是比序列化还要重的过程),做marshling比较耗性能,前面的官方文档也提到过:
Caution: You should limit the direction to what is truly needed, because marshalling parameters is expensive.
回到问题,为什么要有 directional tag 呢?因为跨进程通信默认不能同步数据更新,如果想要做到这一点,要把所有的参数 marshaling 过程处理成与 directional tag 为 inout 时相同的效果,而 marshaling 操作又比较耗性能,使用 directional tag 的概念可以让开发者选择最适合当前场景的 tag。
> 什么场景适合 in 呢?
如果你去实践 directional tag,会发现基本数据类型、String 等参数只能使用in,使用 out / inout 时会在编译期报错:
'out int integer' can only be an in parameter
为什么这样设计呢?
因为没有意义!
我们在 Java 中执行方法时,方法中对于基本类型的参数修改不会更改外部变量,因为它是一次 copy,String 类型虽然原因不一样,但是结果也是不会体现。
所以在这个场景中,我们并不期待方法中对(基本数据类型)参数的修改会体现在外部变量中。这时候使用 in (也只能使用 in )可以满足我们的需求。
事实上,这里不需要考虑那么多,默认用 in 也就对了。
> 什么场景适合 out 呢?
在弄清楚 out 之后,我的第一想法是为什么不用返回值呢(毕竟都是 callee 往 reply 中写数据)?
经过一些细节的推敲,发现了这样设计的好处:
- 使用返回值需要重新创建一个对象,这个开销比较大。
- 使用返回值如果不创建新对象,就只能使用原有对象,这时原有对象可能不希望被更改,或者更改逻辑需要自定义,无法支持。
- 使用返回值在多个 out 参数的场景实现非常麻烦,需要再包一层对象。
就好比,Java 中最底层的数组复制方法 System.arrayCopy(src, srcPos, dest, destPos, int length)
没有返回一个新的数组,而是将目的数据作为参数传入,一方面在最底层频繁创建数组并不明智;另一方面,业务需求可能是增量地添加数据,这个场景中如果每次都需要创建新数组并且搬移旧数据,就会造成性能灾难了。
上面列出的问题使用 out 参数可以很好地解决;另外,如果返回值表示了操作的状态,而此时还需要根据状态返回数据,使用 out 也让逻辑更清晰了,数据更新的操作也封装在了 Parcelable.readFromParcel()中,方便自定义数据更新的细节。
public void readFromParcel(Parcel reply) {
int temp = reply.readInt();
Log.d(T, "read new value " + temp);
value = temp;
}
深入之后,全是细节,实践的时候会发现只有 Parcel 和 集合类型的参数可以使用 out 和 inout,并且需要显示标识出 tag;可以想象设计者为了易用性和性能也是煞费苦心。
回到问题:什么场景适合 out 呢?
caller 需要 callee 处理过的数据,同时参数较多、数据结构复杂或增量更新。
回到这一节的问题:这个 out 有什么用呢?
out 的作用就是在上面的场景中为你提供最佳性能的解决方案!
老实说,这样的场景。。。我还没有遇到过,希望你可以遇到!