「Android Binder」AIDL 中的 in / out 到底是啥?
用过 aidl 的同学,可能见过下面的写法:
不知道你有没有好奇过这里的 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 inputs
out - 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,会发现接口回调是可以正常工作的(验证demo地址结果如下),否则我们早就发现这个高频使用场景的异常了。
结论和事实有冲突,假设(上面的结论)一定有问题!
大家得出这个错误结论是情有可原的,毕竟对于大多数开发者,AIDL“听得多,用得少”,第一个人在写 Demo 验证的时候场景特殊,基于这个特殊场景得出的结论就是错误的。其实这也是刺激我写下本文的原因,因为全网浏览量最高的博客(几乎)全都讲错了,真是生气又骄傲~
那么 directional tag 到底是什么呢?下面我们就一步一步来验证:
源码之下
要弄清楚究竟发生了什么,源码之下毫无秘密。
为了避免部分同学一脸懵逼,这里补充一点关于 AIDL 的前置知识:
AIDL 作为一种跨进程通信的方案,底层依赖 Binder,跨进程通信时会调用 AIDL 中定义的方法,会把 caller(调用者,后文只用 caller)的参数数据 copy 到 callee(接收者,后文只用 callee),然后在 callee 进程中调用另外一个代理对象的相同方法,这个逻辑由 Binder 框架封装;使用者上层看起来,感觉是直接调用了对方进程中对象的方法。
AIDL 文件在编译后会生成 2 个重要的实现类:
Stubcallee 被调用时,会通过 Stub.onTransact(code, data, reply, flag)间接地调用本地对象(Local Binder)的对应方法。
Proxycaller 调用 AIDL 方法时,最终通过 Proxy 调用 remote.transact(code, _data, _reply, flag),然后通过 Binder 机制调用到远程的相应方法。
上面的 onTransact() 和 transact() 方法都是 Binder 定义的方法,更底层的跨进程逻辑由 Binder 机制实现,就不是本文的重点了。
有了这些基础知识,下面我们写一个 AIDL 文件,看一下对应的方法做了什么事情,全部代码请看这里。
AIDL 文件IController
编译后的关键代码如下:
in
输出日志:
out
日志输出:
inout
日志输出:
directional tag 到底是啥?
根据源码和 demo 的验证结果,我们可以得出结论了:
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()中,方便自定义数据更新的细节。
深入之后,全是细节,实践的时候会发现只有 Parcel 和 集合类型的参数可以使用 out 和 inout,并且需要显示标识出 tag;可以想象设计者为了易用性和性能也是煞费苦心。
回到问题:什么场景适合 out 呢?
caller 需要 callee 处理过的数据,同时参数较多、数据结构复杂或增量更新。
回到这一节的问题:这个 out 有什么用呢?
out 的作用就是在上面的场景中为你提供最佳性能的解决方案!
老实说,这样的场景。。。我还没有遇到过,希望你可以遇到!
版权声明: 本文为 InfoQ 作者【李小四】的原创文章。
原文链接:【http://xie.infoq.cn/article/f67ea5c347d104614da2e424e】。文章转载请联系作者。
评论