问题背景
前阵子接手了直播模块,有个需求需要在已有的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)
}
//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];
}
};
}
果然,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);
}
我们可以看到在解析数据的时候,参数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);
}
由于PlayerConfig没有正确地实现Parcelable,只写入了1个int类型,但是却读取了2个,这就导致了参数A多读取了一个int… 等到参数B想从data中读取的时候,就会读取不到数值(返回默认值)…
这个涉及到了Parcel的内部机制,可以参考这篇文章
小结
这次的坑是因为没有正确实现Parcelable接口导致的。这很不应该,其实我们可以让工具来做这种体力活,比如AS中有个插件叫做「Android Parcelable code generator」就可以一键生成Parcelable代码,或者去Github搜搜Parcelable相关的注解库也行~
程序猿要对自己好点,能用工具完成的事情尽量不要自己写~
踩坑结束!