Bundleをプロセス間通信で使う場合の注意

以前、AIDLを使ってサービスとアクティビティで通信を行う際に引数にBundleを渡すと原因不明の上記例外が発生したことがあった。
AIDLによるサービスとの通信でBundleにParcelableをセットするとBadParcelableException
AIDLによる通信でBundleにParcelableをセットするとBadParcelableException(その2)
不可解な結末

この時はエラーが消失してしまったため追求もそこで止めてしまっていたのだが、これはBundleクラスが内部に持つクラスローダー(ClassLoader)の問題が原因であることが判明した。

途中を端折っていきなり核心に入るが、Bundleクラス内部のデータはParcelクラスであり、こいつはの内部データをアンマーシャル(アンパーセル)する際に必要ならばクラスローダを使用する。(内部データの型によっては遅延評価を行い、デマンドでオブジェクトを再構成するためだ)

    • Bundle.java#unparcel
synchronized void unparcel() {
    if (mParcelledData == null) {
        return;
    }

    int N = mParcelledData.readInt();
    if (N < 0) {
        return;
    }
    if (mMap == null) {
        mMap = new HashMap();
    }
    mParcelledData.readMapInternal(mMap, N, mClassLoader);
    mParcelledData.recycle();
    mParcelledData = null;
}

このフィールドmParcelledDataはPacel型でありreadMapInternalはParcel#readParcelableを呼び出す。

    • Parcel.java#readParcelable
    try {
        Class c = loader == null ?
            Class.forName(name) : Class.forName(name, true, loader);
        Field f = c.getField("CREATOR");
        creator = (Parcelable.Creator)f.get(null);
    }
    :略
    :
    catch (ClassNotFoundException e) {
        Log.e("Parcel", "Class not found when unmarshalling: "
                            + name + ", e: " + e);
        throw new BadParcelableException(
                "ClassNotFoundException when unmarshalling: " + name);
    }

ここでクラスをロードできない場合に例外がスローされるのだ。

Bundleクラスはデフォルトでは自らの型から取得した(Class#getClassLoader())クラスローダを使用するが、こいつはjava.lang.BootClassLoaderのインスタンスでありブートストラップローダ、つまりクラスローダのルートである。

public Bundle() {
    mMap = new HashMap();
    mClassLoader = getClass().getClassLoader();
}

通常であれば問題になることはないはずだが、今回のようにプロセスが互いに分離されたアプリケーションとサービスのプロセス下のDalvikが別個にロードしたクラス(今回は別プロセスのサービスがロードしたクラス)であり、同じ型のはずだが違う型と見なされてロードに失敗する訳だ。

android.os.BadParcelableException: ClassNotFoundException when unmarshalling

なんだかんだとJavaを使い始めて10年以上が経つのだが、こうやって未だにクラスローダに悩まされる。


解決策として一番簡単なのは、AIDL等外部と通信を行うパラメタにBundleを含む場合はそのクラスローダをコンテキストのもので置換えることである。

Bundle bundle = new Bundle(this.getClassLoader());

本当はBundleを、例えばRemotableBundleとか拡張すれば良いのだが、ここでもBundleクラスはfinalなのだった。