写点什么

「Android Binder」AIDL 中的 in / out 到底是啥?

用户头像
李小四
关注
发布于: 2021 年 04 月 03 日
「Android Binder」AIDL中的 in / out 到底是啥?

用过 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, or inout (see the example below).

Primitives, String, IBinder, and AIDL-generated interfaces are in 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 fileinterface ICallback {    void onResult(int result);}
//aidl fileinterface IController { void registerCallback(ICallback callback);}
复制代码


但是,如果使用过 AIDL,会发现接口回调是可以正常工作的(验证demo地址结果如下),否则我们早就发现这个高频使用场景的异常了。


D/directional tag: server register callbackD/directional tag: client onResult: 1
复制代码


结论和事实有冲突,假设(上面的结论)一定有问题!


大家得出这个错误结论是情有可原的,毕竟对于大多数开发者,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 file: Stateparcelable State;
复制代码


//aidl file: IControllerinterface 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(): 1callee transIn(), value: 1callee set value to 2caller 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(): 1callee transOut(), value: -1000callee set value to 2read new value  2caller 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(): 1callee transInOut(), value: 1callee set value to 2read new value  2caller value after transInOut(): 2
复制代码

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()中,方便自定义数据更新的细节。


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 的作用就是在上面的场景中为你提供最佳性能的解决方案!


老实说,这样的场景。。。我还没有遇到过,希望你可以遇到!

发布于: 2021 年 04 月 03 日阅读数: 22
用户头像

李小四

关注

Android Engineer 2018.05.01 加入

好奇心患者

评论

发布
暂无评论
「Android Binder」AIDL中的 in / out 到底是啥?