Parcelable実装が間違ってる場合の挙動

  • 6
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

あまり詳しくないプロジェクトのバグ対応してたらかなりハマってかなしかったのでメモです。

Parcelable実装の間違いは気付きにくい

何故なら間違っていても普通に使えてしまう場合もあるからです。

通常のフローでは間違ってても使えてしまう

具体的にはFragmentに値を渡す場合のsetArgumentsなどでParcelableオブジェクトを渡した場合、通常のフローでonCreateなどで値を取り出した場合は、Parcelable定義が正しくなくても普通に値を取得出来てしまいます。

Parcelable実装クラスがこんな感じで

public class Model implements Parcelable {

    public String name;
    public String id;

    public Model() {}

    protected Model(Parcel in) {
        name = in.readString();
        id = in.readString();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeString(id);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<Model> CREATOR = new Creator<Model>() {
        @Override
        public Model createFromParcel(Parcel in) {
            return new Model(in);
        }

        @Override
        public Model[] newArray(int size) {
            return new Model[size];
        }
    };
}

Fragmentのargumentsとしてこんな感じで渡します。

    public static BlankFragment newInstance(Model model, String param2, String param3) {
        BlankFragment fragment = new BlankFragment();
        Bundle args = new Bundle();
        args.putParcelable(ARG_PARAM1, model);
        args.putString(ARG_PARAM2, param2);
        args.putString(ARG_PARAM3, param3);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle arguments = getArguments();
        if (arguments != null) {
            mParam1 = arguments.getParcelable(ARG_PARAM1);
            mParam2 = arguments.getString(ARG_PARAM2);
            mParam3 = arguments.getString(ARG_PARAM3);
        }
    }

上記は正しく動作する例ですが、Modelを以下のように変更したらどうなるでしょうか。

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
//        dest.writeString(id);
    }

これでも動作を一見すると普通に正しい値が取得出来てしまいます。
ところが、開発者向けオプションで「Activityを保持しない」を有効にして、画面を再表示してみるとクラッシュしてしまいました。

 Caused by: java.lang.RuntimeException: Parcel android.os.Parcel@8bb7c65: Unmarshalling unknown type code 7602291 at offset 144
                                                                              at android.os.Parcel.readValue(Parcel.java:2319)
                                                                              at android.os.Parcel.readArrayMapInternal(Parcel.java:2592)
                                                                              at android.os.BaseBundle.unparcel(BaseBundle.java:221)
                                                                              at android.os.Bundle.getParcelable(Bundle.java:786)
                                                                              at me.kirimin.myapplication.BlankFragment.onCreate(BlankFragment.java:42)
                                                                              at android.support.v4.app.Fragment.performCreate(Fragment.java:2068)

おそらくrestore時に初めてParcel,Unparcel処理が実行されるためでしょう。

Parcelable実装間違い時の挙動は分かりにくい

このParcel失敗時の挙動は結構分かりにくいです。
ググってもピンポイントな情報がなかなか見つからず無駄にハマってしまったので書き残しておきます。

writeが抜けている場合

これは上で書いた通りのExceptionが発生します。

readが抜けている場合

    protected Model(Parcel in) {
        name = in.readString();
//        id = in.readString();
    }

この場合はModelオブジェクト自体は取得出来ますが、idがnullのままになってしまいます。
そしてなんとARG_PARAM2とARG_PARAM3のStringも取得に失敗してnullとなってしまいました。

writeが抜けている場合2

public class Model implements Parcelable {

    public String name;
    public String id;
    public List<ModelB> list1;
    public List<ModelB> list2;

    public Model() {}

    protected Model(Parcel in) {
        name = in.readString();
        id = in.readString();
        list1 = in.createTypedArrayList(ModelB.CREATOR);
        list2 = in.createTypedArrayList(ModelB.CREATOR);
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeString(id);
//        dest.writeTypedList(list1);
//        dest.writeTypedList(list2);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<Model> CREATOR = new Creator<Model>() {
        @Override
        public Model createFromParcel(Parcel in) {
            return new Model(in);
        }

        @Override
        public Model[] newArray(int size) {
            return new Model[size];
        }
    };
}

上記のようにメンバ変数にオブジェクトのListを使用していてwriteが抜けている場合には以下のような別のExceptionが発生しました。

Caused by: java.lang.IllegalArgumentException: Duplicate key in ArrayMap: null
                                                                              at android.util.ArrayMap.validate(ArrayMap.java:540)
                                                                              at android.os.Parcel.readArrayMapInternal(Parcel.java:2599)
                                                                              at android.os.BaseBundle.unparcel(BaseBundle.java:221)
                                                                              at android.os.Bundle.getParcelable(Bundle.java:786)
                                                                              at me.kirimin.myapplication.BlankFragment.onCreate(BlankFragment.java:42)
                                                                              at android.support.v4.app.Fragment.performCreate(Fragment.java:2068)

更に、例にあるFragmentからargs.putString(ARG_PARAM3, param3);を抜いてargumentをModelとStringの2つだけにしてみると、今度はExceptionは発生せずにModelオブジェクトのメンバの一部とARG_PARAM2の中身がnullになってしまいます。

僕が見ていたコードでは複数のネストしたParcelable実装クラスがargumentに渡されていて、この挙動からはどのクラスの実装が間違っているのかがすぐに判断出来ず解析に時間が掛かってしまいました。
(いろいろなクラスが間違っていた)

まとめ

・Parcelableの間違いはrestoreが発生するまで気付かない恐れがある。
・Activity破棄時のテストはちゃんとやろう。
・Parcelable実装クラスを書き換える時は注意しよう。
・Parcel時の挙動もいろいろあるので注意しよう。