一場由Parcelable引發的血案
問題背景
前陣子接手了直播模塊,有個需求需要在已有的AIDL接口中增加多一個int類型的參數B。由于該AIDL接口中已經有了一個自定義類型的參數A(已經實現Parcelable接口),我便將參數B追加到A的后面。嗯,炒雞簡單的,只是運行之后有問題而已(微笑臉)
有一個詭異的問題: 無論B傳入什么值,另一方接收到都始終為0(int的默認值),而A接收到的值卻是正確的?!
Take it easy! 作為共產主義的接班人,我當然是有辦法的啦:
- 懷疑是 Freeline 不支持AIDL,改用AS重新build,問題依舊
- 懷疑是辣雞AS的問題,重新啟動再build,問題依舊
- 懷疑是工具鏈的版本問題,改為最新版本再次編譯,問題依舊
- 懷疑是int的問題?將B改為float, boolean等其他的基本類型,問題依舊,取到的始終是默認值
- 抱著希望在StackOverflow和Google上逛了一圈,無果
- 求助群里的小伙伴,答曰沒有遇到過此情況,并向我丟了一連竄的「233333」和一波表情
- 辭職
就在我一籌莫展的時候,突然腦子一抽,試著把接口定義中A參數和B參數位置調換。震驚地發現,居然可以了!!
嗯,此篇文章完結,撒花~
解決方案
將基本類型的參數放在自定義類型的參數前面,雖然解決了問題,但是治標不治本,只能算個workaround。
不知道大家有沒有發現:參數的定義順序會影響結果這一行為,是不是跟實現Parcelable接口的時候有點類似?Parcelable中如果read的順序和write的順序不同的話,產生的結果也不同。
基于這點,我們懷疑問題出在自定義類型的參數A,先來看下相關代碼:
//ILivePlayerService.aidl
interface ILivePlayerService {
//參數A:config, 參數B:type
void setPlayerConfig(in PlayerConfig config, int type)
}
</code></pre>
//PlayerConfig.java
public class PlayerConfig implements Parcelable {
//省略其他代碼....
String streamUrl;
int roomId;
int anchorId;
public PlayerConfig() {
}
protected PlayerConfig(Parcel in) {
//省略其他代碼....
this.streamUrl = in.readString();
this.roomId = in.readInt();
this.anchorId = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
//省略其他代碼....
dest.writeString(this.streamUrl);
dest.writeInt(this.roomId);
//下面注釋的這句,代碼中是沒有的。問題就出現在這里..
//dest.writeInt(this.anchorId);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<PlayerConfig> CREATOR = new Creator<PlayerConfig>() {
@Override
public PlayerConfig createFromParcel(Parcel source) {
return new PlayerConfig(source);
}
@Override
public PlayerConfig[] newArray(int size) {
return new PlayerConfig[size];
}
};
}
</code></pre>
果然, PlayerConfig 沒有正確地實現 Parcelable 接口,在寫入的時候(詳見上面代碼中 writeToParcel 方法中的注釋)漏掉了變量 anchorId ,而讀取的時候卻有。
論接手別人的代碼是一種怎樣的體驗?
我們先把 writeToParcel 中漏掉的變量補上,再跑一下看問題是否解決了。
不出所料,那個詭異的問題沒有了,可是為什么呢?我們來看下AIDL生成的Java代碼:
//AIDL生成的 ILivePlayerService.java
//為了方便查看,格式了一下代碼
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
//setPlayerConfig方法
case TRANSACTION_setPlayerConfig: {
data.enforceInterface(DESCRIPTOR);
//參數A:config
com.kk.model.PlayerConfig _arg0;
if ((0 != data.readInt())) {
//在解析的時候,會調用PlayerConfig的createFromParcel方法
_arg0 = com.kk.model.PlayerConfig.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
//參數B:type
int _arg1;
_arg1 = data.readInt();
this.setPlayerConfig(_arg0, _arg1);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
</code></pre>
我們可以看到在解析數據的時候, 參數A和參數B都是從data中解析的(共用一個Parcel源) 其中,解析參數A config 的時候會將 data 傳入到自己實現的 createFromParcel 方法中進行處理,如下
//PlayerConfig.java 部分代碼
public static final Creator<PlayerConfig> CREATOR = new Creator<PlayerConfig>() {
@Override
public PlayerConfig createFromParcel(Parcel source) {
//接受到aidl傳入的data
return new PlayerConfig(source);
}
};
protected PlayerConfig(Parcel in) {
//aidl傳入的data在這里解析
this.streamUrl = in.readString();
this.roomId = in.readInt();
this.anchorId = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
//省略其他代碼....
dest.writeString(this.streamUrl);
dest.writeInt(this.roomId);
//下面注釋的這句,代碼中是沒有的。問題就出現在這里..
//dest.writeInt(this.anchorId);
}
</code></pre>
由于 PlayerConfig 沒有正確地實現 Parcelable ,只寫入了1個int類型,但是卻讀取了2個,這就導致了參數A多讀取了一個int… 等到參數B想從 data 中讀取的時候,就會讀取不到數值(返回默認值)…
這個涉及到了Parcel的內部機制,可以參考這篇文章
http://blog.csdn.net/qinjuning/article/details/6785517
小結
這次的坑是因為沒有正確實現Parcelable接口導致的。這很不應該,其實我們可以讓工具來做這種體力活,比如AS中有個插件叫做「Android Parcelable code generator」就可以一鍵生成Parcelable代碼,或者去Github搜搜Parcelable相關的注解庫也行~
程序猿要對自己好點,能用工具完成的事情盡量不要自己寫~
踩坑結束!
來自:http://andydev.me/2017/05/31/trap-of-android-aidl/