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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容