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のインスタンスでありブートストラップローダ、つまりクラスローダのルートである。
-
- Bundle.java
public Bundle() { mMap = new HashMap(); mClassLoader = getClass().getClassLoader(); }
通常であれば問題になることはないはずだが、今回のようにプロセスが互いに分離されたアプリケーションとサービスのプロセス下のDalvikが別個にロードしたクラス(今回は別プロセスのサービスがロードしたクラス)であり、同じ型のはずだが違う型と見なされてロードに失敗する訳だ。
android.os.BadParcelableException: ClassNotFoundException when unmarshalling
なんだかんだとJavaを使い始めて10年以上が経つのだが、こうやって未だにクラスローダに悩まされる。
解決策として一番簡単なのは、AIDL等外部と通信を行うパラメタにBundleを含む場合はそのクラスローダをコンテキストのもので置換えることである。
-
- HogeActivity.java
Bundle bundle = new Bundle(this.getClassLoader());
本当はBundleを、例えばRemotableBundleとか拡張すれば良いのだが、ここでもBundleクラスはfinalなのだった。