一场由 Parcelable 引发的 AIDL 灭门惨案

1,217 阅读4分钟
原文链接: andydev.me

问题背景

前阵子接手了直播模块,有个需求需要在已有的AIDL接口中增加多一个int类型的参数B。由于该AIDL接口中已经有了一个自定义类型的参数A(已经实现Parcelable接口),我便将参数B追加到A的后面。嗯,炒鸡简单的,只是运行之后有问题而已(微笑脸)

有一个诡异的问题:无论B传入什么值,另一方接收到都始终为0(int的默认值),而A接收到的值却是正确的?!

Take it easy! 作为共产主义的接班人,我当然是有办法的啦:

  1. 怀疑是 Freeline 不支持AIDL,改用AS重新build,问题依旧
  2. 怀疑是辣鸡AS的问题,重新启动再build,问题依旧
  3. 怀疑是工具链的版本问题,改为最新版本再次编译,问题依旧
  4. 怀疑是int的问题?将B改为float, boolean等其他的基本类型,问题依旧,取到的始终是默认值
  5. 抱着希望在StackOverflow和Google上逛了一圈,无果
  6. 求助群里的小伙伴,答曰没有遇到过此情况,并向我丢了一连窜的「233333」和一波表情
  7. 辞职

就在我一筹莫展的时候,突然脑子一抽,试着把接口定义中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的内部机制,可以参考这篇文章

blog.csdn.net/qinjuning/a…

小结

这次的坑是因为没有正确实现Parcelable接口导致的。这很不应该,其实我们可以让工具来做这种体力活,比如AS中有个插件叫做「Android Parcelable code generator」就可以一键生成Parcelable代码,或者去Github搜搜Parcelable相关的注解库也行~

程序猿要对自己好点,能用工具完成的事情尽量不要自己写~

踩坑结束!