一场由Parcelable引发的血案

ui8p0q1j 7年前
   <h2>问题背景</h2>    <p>前阵子接手了直播模块,有个需求需要在已有的AIDL接口中增加多一个int类型的参数B。由于该AIDL接口中已经有了一个自定义类型的参数A(已经实现Parcelable接口),我便将参数B追加到A的后面。嗯,炒鸡简单的,只是运行之后有问题而已(微笑脸)</p>    <p>有一个诡异的问题: <strong>无论B传入什么值,另一方接收到都始终为0(int的默认值),而A接收到的值却是正确的?!</strong></p>    <p>Take it easy! 作为共产主义的接班人,我当然是有办法的啦:</p>    <ol>     <li>怀疑是 <a href="/misc/goto?guid=4959009805721920577" rel="nofollow,noindex">Freeline</a> 不支持AIDL,改用AS重新build,问题依旧</li>     <li>怀疑是辣鸡AS的问题,重新启动再build,问题依旧</li>     <li>怀疑是工具链的版本问题,改为最新版本再次编译,问题依旧</li>     <li>怀疑是int的问题?将B改为float, boolean等其他的基本类型,问题依旧,取到的始终是默认值</li>     <li>抱着希望在StackOverflow和Google上逛了一圈,无果</li>     <li>求助群里的小伙伴,答曰没有遇到过此情况,并向我丢了一连窜的「233333」和一波表情</li>     <li>辞职</li>    </ol>    <p>就在我一筹莫展的时候,突然脑子一抽,试着把接口定义中A参数和B参数位置调换。震惊地发现,居然可以了!!</p>    <p>嗯,此篇文章完结,撒花~</p>    <h2>解决方案</h2>    <p>将基本类型的参数放在自定义类型的参数前面,虽然解决了问题,但是治标不治本,只能算个workaround。</p>    <p>不知道大家有没有发现:参数的定义顺序会影响结果这一行为,是不是跟实现Parcelable接口的时候有点类似?Parcelable中如果read的顺序和write的顺序不同的话,产生的结果也不同。</p>    <p>基于这点,我们怀疑问题出在自定义类型的参数A,先来看下相关代码:</p>    <pre>  <code class="language-java">//ILivePlayerService.aidl    interface ILivePlayerService {   //参数A:config, 参数B:type   void setPlayerConfig(in PlayerConfig config, int type)  }  </code></pre>    <pre>  <code class="language-java">//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>    <p>果然, PlayerConfig 没有正确地实现 Parcelable 接口,在写入的时候(详见上面代码中 writeToParcel 方法中的注释)漏掉了变量 anchorId ,而读取的时候却有。</p>    <p>论接手别人的代码是一种怎样的体验?</p>    <p>我们先把 writeToParcel 中漏掉的变量补上,再跑一下看问题是否解决了。</p>    <p>不出所料,那个诡异的问题没有了,可是为什么呢?我们来看下AIDL生成的Java代码:</p>    <pre>  <code class="language-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>    <p>我们可以看到在解析数据的时候, <strong>参数A和参数B都是从data中解析的(共用一个Parcel源)</strong> 其中,解析参数A config 的时候会将 data 传入到自己实现的 createFromParcel 方法中进行处理,如下</p>    <pre>  <code class="language-java">//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>    <p>由于 PlayerConfig 没有正确地实现 Parcelable ,只写入了1个int类型,但是却读取了2个,这就导致了参数A多读取了一个int… 等到参数B想从 data 中读取的时候,就会读取不到数值(返回默认值)…</p>    <p>这个涉及到了Parcel的内部机制,可以参考这篇文章</p>    <p><a href="/misc/goto?guid=4959675699969647215" rel="nofollow,noindex">http://blog.csdn.net/qinjuning/article/details/6785517</a></p>    <h2>小结</h2>    <p>这次的坑是因为没有正确实现Parcelable接口导致的。这很不应该,其实我们可以让工具来做这种体力活,比如AS中有个插件叫做「Android Parcelable code generator」就可以一键生成Parcelable代码,或者去Github搜搜Parcelable相关的注解库也行~</p>    <p>程序猿要对自己好点,能用工具完成的事情尽量不要自己写~</p>    <p>踩坑结束!</p>    <p> </p>    <p>来自:http://andydev.me/2017/05/31/trap-of-android-aidl/</p>    <p> </p>