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

老實說,這樣的場景。。。我還沒有遇到過,希望你可以遇到!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容