一场由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>