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

用過aidl的同學(xué)艇炎,可能見過下面的寫法:

interface IInterface {
    void foo0(in int input);
    void foo1(out IDTParcel parcel);
    void foo2(inout IDTParcel parcel);
}

不知道你有沒有好奇過這里的 in / out / inout 是什么意思呢耸成?

directional tag

官網(wǎng)一查叛拷,只找到一點(diǎn)點(diǎn)信息:

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 (定向標(biāo)簽?)的概念忿薇,指的是which way the data goes (數(shù)據(jù)以哪種方式流動裙椭?)躏哩,啥意思?從概念到解釋都不是人話揉燃;不滿意的你繼續(xù)搜索相關(guān)博客......

directional tag 不是什么震庭?

在說清楚它是什么之前,先聊聊directional tag不是什么:

如果你搜索aidl in out這幾個關(guān)鍵詞你雌,會有很多文章出來,很多文章的結(jié)論是這樣的:

AIDL中的定向 tag 表示了在跨進(jìn)程通信中數(shù)據(jù)的流向

  • 其中 in 表示數(shù)據(jù)只能由客戶端流向服務(wù)端
  • out 表示數(shù)據(jù)只能由服務(wù)端流向客戶端
  • inout 則表示數(shù)據(jù)可在服務(wù)端與客戶端之間雙向流通二汛。

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.

(為了避免部分追求“效率”的讀者只讀關(guān)鍵詞婿崭,文中錯誤的結(jié)論都會加中劃線)

上面的結(jié)論聽著很有道理,但你可能會發(fā)現(xiàn)一個問題:接口回調(diào)的場景無法實(shí)現(xiàn)了肴颊!

在aidl中氓栈,如果client向server注冊一個Callback(如下代碼所示),server會在某些場景回調(diào)client婿着,這時候數(shù)據(jù)流向是server => client, 按照上面的邏輯授瘦,這個result數(shù)據(jù)無法到達(dá)client,因?yàn)閕nt數(shù)據(jù)的directional tag只能是in(后面會講到)竟宋, 而in只能支持client到server的數(shù)據(jù)傳輸方向

//aidl file
interface ICallback {
    void onResult(int result);
}

//aidl file
interface IController {
    void registerCallback(ICallback callback);
}

但是提完,如果使用過AIDL,會發(fā)現(xiàn)接口回調(diào)是可以正常工作的(驗(yàn)證demo地址結(jié)果如下)丘侠,否則我們早就發(fā)現(xiàn)這個高頻使用場景的異常了徒欣。

D/directional tag: server register callback
D/directional tag: client onResult: 1

結(jié)論和事實(shí)有沖突,假設(shè)(上面的結(jié)論)一定有問題!

大家得出這個錯誤結(jié)論是情有可原的蜗字,畢竟對于大多數(shù)開發(fā)者打肝,AIDL“聽得多,用得少”挪捕,第一個人在寫Demo驗(yàn)證的時候場景特殊粗梭,基于這個特殊場景得出的結(jié)論就是錯誤的。
其實(shí)這也是刺激我寫下本文的原因级零,因?yàn)槿W(wǎng)瀏覽量最高的博客(幾乎)全都講錯了断医,真是生氣又驕傲~

那么 directional tag 到底是什么呢?
下面我們就一步一步來驗(yàn)證:

源碼之下

要弄清楚究竟發(fā)生了什么妄讯,源碼之下毫無秘密孩锡。

為了避免部分同學(xué)一臉懵逼,這里補(bǔ)充一點(diǎn)關(guān)于AIDL的前置知識:

AIDL作為一種跨進(jìn)程通信的方案亥贸,底層依賴Binder躬窜,跨進(jìn)程通信時會調(diào)用AIDL中定義的方法,會把 caller(調(diào)用者炕置,后文只用caller)的參數(shù)數(shù)據(jù) copy 到 callee(接收者荣挨,后文只用callee)男韧,然后在callee進(jìn)程中調(diào)用另外一個代理對象的相同方法,這個邏輯由Binder框架封裝默垄;使用者上層看起來此虑,感覺是直接調(diào)用了對方進(jìn)程中對象的方法。

AIDL文件在編譯后會生成2個重要的實(shí)現(xiàn)類:

  • Stub
    callee被調(diào)用時口锭,會通過Stub.onTransact(code, data, reply, flag)間接地調(diào)用本地對象(Local Binder)的對應(yīng)方法朦前。

  • Proxy
    caller調(diào)用AIDL方法時,最終通過Proxy調(diào)用remote.transact(code, _data, _reply, flag)鹃操,然后通過Binder機(jī)制調(diào)用到遠(yuǎn)程的相應(yīng)方法韭寸。

    上面的onTransact() 和 transact() 方法都是Binder定義的方法,更底層的跨進(jìn)程邏輯由Binder機(jī)制實(shí)現(xiàn)荆隘,就不是本文的重點(diǎn)了恩伺。

有了這些基礎(chǔ)知識,下面我們寫一個AIDL文件椰拒,看一下對應(yīng)的方法做了什么事情晶渠,全部代碼請看這里

//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編譯后的關(guān)鍵代碼如下:

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數(shù)據(jù)寫入_data
        state.writeToParcel(_data, 0);
    } else {
        _data.writeInt(0);
    }
    //傳輸數(shù)據(jù)燃观,并調(diào)用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())) {
        //根據(jù)傳入的data創(chuàng)建State對象
        _arg0 = com.littlefourth.aidl.State.CREATOR.createFromParcel(data);
    } else {
        _arg0 = null;
    }
    //調(diào)用callee實(shí)現(xiàn)的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數(shù)據(jù)
    mRemote.transact(Stub.TRANSACTION_transOut, _data, _reply, 0);
    //讀取返回值
    _result = _reply.readInt();
    if ((0 != _reply.readInt())) {
        //讀取callee更新后的state數(shù)據(jù)
        state.readFromParcel(_reply);
    }
    return _result;
}

//Stub(callee)
case TRANSACTION_transOut: {
    com.littlefourth.aidl.State _arg0;
    //直接創(chuàng)建新的State對象
    _arg0 = new com.littlefourth.aidl.State();
    //調(diào)用callee實(shí)現(xiàn)的transOut()
    int _result = this.transOut(_arg0);
    //寫入返回值
    reply.writeInt(_result);
    if ((_arg0 != null)) {
        //寫入標(biāo)志位, caller根據(jù)這個數(shù)據(jù)判斷有沒有寫入state數(shù)據(jù)
        reply.writeInt(1);
        //寫入state數(shù)據(jù)(不管數(shù)據(jù)是否更新褒脯,都會寫入全量數(shù)據(jù))
        _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數(shù)據(jù)到_data
        state.writeToParcel(_data, 0);
    } else {
        _data.writeInt(0);
    }
    //傳輸數(shù)據(jù),并調(diào)用callee的transInOut()
    mRemote.transact(Stub.TRANSACTION_transInOut, _data, _reply, 0);
    _reply.readException();
    _result = _reply.readInt();
    if ((0 != _reply.readInt())) {
        //讀取callee更新后的state數(shù)據(jù)
        state.readFromParcel(_reply);
    }
    return _result;
}

//Stub(callee)
case TRANSACTION_transInOut: {
    com.littlefourth.aidl.State _arg0;
    if ((0 != data.readInt())) {
        //根據(jù)data創(chuàng)建State對象
        _arg0 = com.littlefourth.aidl.State.CREATOR.createFromParcel(data);
    } else {
        _arg0 = null;
    }
    //調(diào)用callee實(shí)現(xiàn)的transInOut()
    int _result = this.transInOut(_arg0);
    //寫入返回值
    reply.writeInt(_result);
    if ((_arg0 != null)) {
        //寫入標(biāo)志位, caller根據(jù)這個數(shù)據(jù)判斷有沒有寫入state數(shù)據(jù)
        reply.writeInt(1);
        //寫入state數(shù)據(jù)(不管數(shù)據(jù)是否更新仪壮,都會寫入全量數(shù)據(jù))
        _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 到底是啥憨颠?

根據(jù)源碼和 demo 的驗(yàn)證結(jié)果,我們可以得出結(jié)論了:

Directional Tag Desc
in 數(shù)據(jù)從 caller傳到 callee积锅,callee 調(diào)用結(jié)束后不會把數(shù)據(jù)寫回 caller 中爽彤。
out caller 數(shù)據(jù)不會傳入 callee(因?yàn)榫蜎]有寫數(shù)據(jù)), callee 調(diào)用結(jié)束后(不管數(shù)據(jù)有沒有更新)會把數(shù)據(jù)寫回 caller 中。
inout 數(shù)據(jù)從 caller 傳到 callee缚陷,callee 調(diào)用結(jié)束后(不管數(shù)據(jù)有沒有更新)會把數(shù)據(jù)寫回 caller 中适篙。

提了這么多次 caller 和 callee ,是不想把它們與 client 和 server 混淆箫爷;因?yàn)?client 與 server 可以互相調(diào)用嚷节,AIDL文件編譯后的代碼是一樣的,client 與 server 在作為 caller 或 callee 時執(zhí)行的(AIDL層)邏輯是相同的虎锚,所以不能說in / out / inout 是明確地表示 client 到 server 的方向(或者相反)硫痰。

這個 out 有什么用呢?

讀到這里窜护,估計(jì)你已經(jīng)弄清楚 directional tag 是什么了效斑,但有一個疑問:

out 有什么用呢?caller 連數(shù)據(jù)都不發(fā)送柱徙?卻要讀 callee 寫回來的數(shù)據(jù)缓屠?

這個疑問太合理了奇昙,畢竟很多用過 AIDL 的朋友從來沒有注意過這里的區(qū)別,然后在部分編譯報錯時根據(jù)提示填入一個 in敌完,發(fā)現(xiàn)邏輯挺正常的储耐,然后就結(jié)束了,也沒出過問題滨溉。

在回答這個問題之前什湘,有另一個要先解決:

> 為什么要有 directional tag 這個東西?

在同一個進(jìn)程中調(diào)用方法時不需要 directional tag 這種東西晦攒,為什么在跨進(jìn)程的場景就需要這個東西呢禽炬?

在同一個進(jìn)程中,對象屬性的修改直接體現(xiàn)到之后的上下文中勤家,因?yàn)樗鼈冊L問了相同的內(nèi)存地址。
在Binder的跨進(jìn)程機(jī)制中柳恐,(從上面的源碼也可以看出)每一次調(diào)用都要把數(shù)據(jù)從 caller 復(fù)制到 callee, 并不是同一塊內(nèi)存伐脖,callee 對數(shù)據(jù)的修改也就不會(自動地)體現(xiàn)在 caller 的數(shù)據(jù)中。這個跨進(jìn)程數(shù)據(jù)傳遞過程叫marshaling(翻譯為數(shù)據(jù)編組乐设?讼庇,總之是比序列化還要重的過程),做marshling比較耗性能近尚,前面的官方文檔也提到過:

Caution: You should limit the direction to what is truly needed, because marshalling parameters is expensive.

回到問題蠕啄,為什么要有 directional tag 呢?因?yàn)榭邕M(jìn)程通信默認(rèn)不能同步數(shù)據(jù)更新戈锻,如果想要做到這一點(diǎn)歼跟,要把所有的參數(shù) marshaling 過程處理成與 directional tag 為 inout 時相同的效果,而 marshaling 操作又比較耗性能格遭,使用 directional tag 的概念可以讓開發(fā)者選擇最適合當(dāng)前場景的 tag哈街。

> 什么場景適合 in 呢?

如果你去實(shí)踐 directional tag拒迅,會發(fā)現(xiàn)基本數(shù)據(jù)類型骚秦、String 等參數(shù)只能使用in,使用 out / inout 時會在編譯期報錯:

'out int integer' can only be an in parameter

為什么這樣設(shè)計(jì)呢璧微?

因?yàn)闆]有意義作箍!

我們在 Java 中執(zhí)行方法時,方法中對于基本類型的參數(shù)修改不會更改外部變量前硫,因?yàn)樗且淮?copy胞得,String 類型雖然原因不一樣,但是結(jié)果也是不會體現(xiàn)开瞭。

所以在這個場景中懒震,我們并不期待方法中對(基本數(shù)據(jù)類型)參數(shù)的修改會體現(xiàn)在外部變量中罩息。這時候使用 in (也只能使用 in )可以滿足我們的需求。

事實(shí)上个扰,這里不需要考慮那么多瓷炮,默認(rèn)用 in 也就對了。

> 什么場景適合 out 呢递宅?

在弄清楚 out 之后娘香,我的第一想法是為什么不用返回值呢(畢竟都是 callee 往 reply 中寫數(shù)據(jù))?

經(jīng)過一些細(xì)節(jié)的推敲办龄,發(fā)現(xiàn)了這樣設(shè)計(jì)的好處:

  • 使用返回值需要重新創(chuàng)建一個對象烘绽,這個開銷比較大。
  • 使用返回值如果不創(chuàng)建新對象俐填,就只能使用原有對象安接,這時原有對象可能不希望被更改,或者更改邏輯需要自定義英融,無法支持盏檐。
  • 使用返回值在多個 out 參數(shù)的場景實(shí)現(xiàn)非常麻煩,需要再包一層對象驶悟。

就好比胡野,Java 中最底層的數(shù)組復(fù)制方法 System.arrayCopy(src, srcPos, dest, destPos, int length) 沒有返回一個新的數(shù)組,而是將目的數(shù)據(jù)作為參數(shù)傳入痕鳍,一方面在最底層頻繁創(chuàng)建數(shù)組并不明智硫豆;另一方面,業(yè)務(wù)需求可能是增量地添加數(shù)據(jù)笼呆,這個場景中如果每次都需要創(chuàng)建新數(shù)組并且搬移舊數(shù)據(jù)熊响,就會造成性能災(zāi)難了。

上面列出的問題使用 out 參數(shù)可以很好地解決诗赌;另外耘眨,如果返回值表示了操作的狀態(tài),而此時還需要根據(jù)狀態(tài)返回數(shù)據(jù)境肾,使用 out 也讓邏輯更清晰了剔难,數(shù)據(jù)更新的操作也封裝在了 Parcelable.readFromParcel()中,方便自定義數(shù)據(jù)更新的細(xì)節(jié)奥喻。

public void readFromParcel(Parcel reply) {
    int temp = reply.readInt();
    Log.d(T, "read new value  " + temp);
    value = temp;
}

深入之后偶宫,全是細(xì)節(jié),實(shí)踐的時候會發(fā)現(xiàn)只有 Parcel 和 集合類型的參數(shù)可以使用 out 和 inout环鲤,并且需要顯示標(biāo)識出 tag纯趋;可以想象設(shè)計(jì)者為了易用性和性能也是煞費(fèi)苦心。

回到問題:什么場景適合 out 呢?

caller 需要 callee 處理過的數(shù)據(jù)吵冒,同時參數(shù)較多纯命、數(shù)據(jù)結(jié)構(gòu)復(fù)雜或增量更新。

回到這一節(jié)的問題:這個 out 有什么用呢痹栖?
out 的作用就是在上面的場景中為你提供最佳性能的解決方案亿汞!

老實(shí)說,這樣的場景揪阿。疗我。。我還沒有遇到過南捂,希望你可以遇到吴裤!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市溺健,隨后出現(xiàn)的幾起案子麦牺,更是在濱河造成了極大的恐慌,老刑警劉巖鞭缭,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枕面,死亡現(xiàn)場離奇詭異,居然都是意外死亡缚去,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門琼开,熙熙樓的掌柜王于貴愁眉苦臉地迎上來易结,“玉大人,你說我怎么就攤上這事柜候「愣” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵渣刷,是天一觀的道長鹦肿。 經(jīng)常有香客問我,道長辅柴,這世上最難降的妖魔是什么箩溃? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮碌嘀,結(jié)果婚禮上涣旨,老公的妹妹穿的比我還像新娘。我一直安慰自己股冗,他們只是感情好霹陡,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般烹棉。 火紅的嫁衣襯著肌膚如雪攒霹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天浆洗,我揣著相機(jī)與錄音催束,去河邊找鬼。 笑死辅髓,一個胖子當(dāng)著我的面吹牛泣崩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播洛口,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼矫付,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了第焰?” 一聲冷哼從身側(cè)響起买优,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挺举,沒想到半個月后杀赢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡湘纵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年脂崔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梧喷。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡砌左,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铺敌,到底是詐尸還是另有隱情汇歹,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布偿凭,位于F島的核電站产弹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏弯囊。R本人自食惡果不足惜痰哨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望匾嘱。 院中可真熱鬧作谭,春花似錦、人聲如沸奄毡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锐秦,卻和暖如春咪奖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酱床。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工羊赵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扇谣。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓昧捷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親罐寨。 傳聞我的和親對象是個殘疾皇子靡挥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容