AndroidではIntentのExtraやFragmentのArgumentなどとしてBundleというコンテナがよく使われます。
このBundleからParcelableを取り出すメソッドが、getParcelableです。API 33にて、key引数だけのバージョンはDeprecatedとなり、Class引数を取るバージョンが推奨されるようになっています(なお、API 33のClass引数あり版にはバグがあるため、実際に使えるのはAPI 34からです。ややこしいのでBundleCompatの使用を推奨します)
このgetParcelableに追加された引数はどのように使われるのか調べる機会があったので、まとめておきます。
結論
型チェックに使われている
以下、調査のためにコードを追っていく経緯です。
Deep-dive into Bundle
実装の違いを見てみましょう
@Nullable
public <T extends Parcelable> T getParcelable(@Nullable String key) {
unparcel();
Object o = getValue(key);
if (o == null) {
return null;
}
try {
return (T) o;
} catch (ClassCastException e) {
typeWarning(key, o, "Parcelable", e);
return null;
}
}
@Nullable
public <T> T getParcelable(@Nullable String key, @NonNull Class<T> clazz) {
return get(key, clazz);
}
引数一つの方は、unparcel()の後、getValue(key)で値を取り出し、指定の型にキャストしています。キャストできなければログを出力してnullを返却します。
一方、引数一つの方は、get(key, clazz)を呼び出しているだけですね。こちらの実装はBundleの親クラスであるBaseBundleに実装があります。
@Nullable
<T> T get(@Nullable String key, @NonNull Class<T> clazz) {
unparcel();
try {
return getValue(key, requireNonNull(clazz));
} catch (ClassCastException | BadTypeParcelableException e) {
typeWarning(key, clazz.getCanonicalName(), e);
return null;
}
}
引数一つのgetParcelable()とほぼ同じ内容で、こちらではgetValue(key, clazz)を呼び出しています。getValue()のバリエーションの違いですね。さらに追ってみます
@Nullable
final Object getValue(String key) {
return getValue(key, /* clazz */ null);
}
@Nullable
final <T> T getValue(String key, @Nullable Class<T> clazz) {
// Avoids allocating Class[0] array
return getValue(key, clazz, (Class<?>[]) null);
}
@Nullable
final <T> T getValue(String key, @Nullable Class<T> clazz, @Nullable Class<?>... itemTypes) {
int i = mMap.indexOfKey(key);
return (i >= 0) ? getValueAt(i, clazz, itemTypes) : null;
}
getValue(key)は、getValue(key, clazz)のclazzにnullを渡したものと同じ、ということで処理が合流しました。getValue(key, clazz)はさらに引数の多い、getValue(key, clazz, itemTypes)を第三引数nullで呼び出しています。
そして、keyをindexに変換して、getValueAt(i, clazz, itemTypes)を呼び出していますね。getValueAt()を調べます。
@Nullable
final <T> T getValueAt(int i, @Nullable Class<T> clazz, @Nullable Class<?>... itemTypes) {
Object object = mMap.valueAt(i);
if (object instanceof BiFunction<?, ?, ?>) {
synchronized (this) {
object = unwrapLazyValueFromMapLocked(i, clazz, itemTypes);
}
}
return (clazz != null) ? clazz.cast(object) : (T) object;
}
やっとそれらしい使用箇所が見えてきました。BundleではArrayMap<String, Object>
型のmMapにデータが格納されています。取り出した値が、BiFunctionでない場合は、キャストしているだけですね。clazzが指定されていれば、clazzを使って、そうでなければそのままキャストが行われています。キャスト方法に違いがありますが、実質的な動作として違いが出ることはなさそうです。
では、BiFunctionの場合ですが、そもそもBiFunctionとはなんぞや?ですが、Bundleのデータがシリアライズされている状態(Parcelに格納された状態で、mMapにデータが書き出されていない状態)で読み出しを行うと、unparcel()によって、mMapへのデータ読み出しが行われます。API 32まではこの時点ですべてのデータがデシリアライズされていましたが、API 33からは、ParcelableやSerializable、MapやArrayといった、さらにデシリアライズが必要なデータタイプについてはLazyValueという形で読み出されるようになっています。
LazyValueの定義は以下、BiFunctionのサブクラスです。
private static final class LazyValue
implements BiFunction<Class<?>, Class<?>[], Object>
ということが分かったところでunwrapLazyValueFromMapLocked()の中を見てみましょう
@Nullable
@GuardedBy("this")
private Object unwrapLazyValueFromMapLocked(int i, @Nullable Class<?> clazz,
@Nullable Class<?>... itemTypes) {
Object object = mMap.valueAt(i);
if (object instanceof BiFunction<?, ?, ?>) {
try {
object = ((BiFunction<Class<?>, Class<?>[], ?>) object).apply(clazz, itemTypes);
} catch (BadParcelableException e) {
if (sShouldDefuse) {
Log.w(TAG, "Failed to parse item " + mMap.keyAt(i) + ", returning null.", e);
return null;
} else {
throw e;
}
}
mMap.setValueAt(i, object);
mLazyValues--;
if (mOwnsLazyValues) {
Preconditions.checkState(mLazyValues >= 0,
"Lazy values ref count below 0");
// No more lazy values in mMap, so we can recycle the parcel early rather than
// waiting for the next GC run
Parcel parcel = mWeakParcelledData.get();
if (mLazyValues == 0 && parcel != null) {
recycleParcel(parcel);
mWeakParcelledData = null;
}
}
}
return object;
}
ポイントとなる値の読み出しは、BiFunction、つまりLazyValueのapplyです。
@Override
public Object apply(@Nullable Class<?> clazz, @Nullable Class<?>[] itemTypes) {
Parcel source = mSource;
if (source != null) {
synchronized (source) {
if (mSource != null) {
int restore = source.dataPosition();
try {
source.setDataPosition(mPosition);
mObject = source.readValue(mLoader, clazz, itemTypes);
} finally {
source.setDataPosition(restore);
}
mSource = null;
}
}
}
return mObject;
}
source、つまりParcelのreadValue()が実際の値の読み出しですね。
@Nullable
private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz,
@Nullable Class<?>... itemTypes) {
int type = readInt();
final T object;
if (isLengthPrefixed(type)) {
int length = readInt();
int start = dataPosition();
object = readValue(type, loader, clazz, itemTypes);
int actual = dataPosition() - start;
if (actual != length) {
Slog.wtfStack(TAG,
"Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type)
+ " consumed " + actual + " bytes, but " + length + " expected.");
}
} else {
object = readValue(type, loader, clazz, itemTypes);
}
return object;
}
readValueの4引数版に続きます。
@Nullable
private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz,
@Nullable Class<?>... itemTypes) {
final Object object;
switch (type) {
...
case VAL_PARCELABLE:
object = readParcelableInternal(loader, clazz);
break;
...
}
if (object != null && clazz != null && !clazz.isInstance(object)) {
throw new BadTypeParcelableException("Unparcelled object " + object
+ " is not an instance of required class " + clazz.getName()
+ " provided in the parameter");
}
return (T) object;
}
こちらは型ごとに処理が分かれているため、Parcelableについての部分を抜き出すとreadParcelableInternal()ですね。
@Nullable
private <T> T readParcelableInternal(@Nullable ClassLoader loader, @Nullable Class<T> clazz) {
Parcelable.Creator<?> creator = readParcelableCreatorInternal(loader, clazz);
if (creator == null) {
return null;
}
if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
Parcelable.ClassLoaderCreator<?> classLoaderCreator =
(Parcelable.ClassLoaderCreator<?>) creator;
return (T) classLoaderCreator.createFromParcel(this, loader);
}
return (T) creator.createFromParcel(this);
}
readParcelableCreatorInternal()を使って、Parcelable.Creatorを取得しています。
これは自前でParceableの実装をする場合に作るCREATORオブジェクトですね。(最近はアノテーションをつけるだけでParceableの実装が終わるので、知らない人も多くなっているかもしれませんが)
ここまで来ればParcelableの読み出しは知っている世界になっています。
最後に、readParcelableCreatorInternal()の中を見てみます。
@Nullable
private <T> Parcelable.Creator<T> readParcelableCreatorInternal(
@Nullable ClassLoader loader, @Nullable Class<T> clazz) {
String name = readString();
if (name == null) {
return null;
}
Pair<Parcelable.Creator<?>, Class<?>> creatorAndParcelableClass;
synchronized (sPairedCreators) {
HashMap<String, Pair<Parcelable.Creator<?>, Class<?>>> map =
sPairedCreators.get(loader);
if (map == null) {
sPairedCreators.put(loader, new HashMap<>());
mCreators.put(loader, new HashMap<>());
creatorAndParcelableClass = null;
} else {
creatorAndParcelableClass = map.get(name);
}
}
if (creatorAndParcelableClass != null) {
Parcelable.Creator<?> creator = creatorAndParcelableClass.first;
Class<?> parcelableClass = creatorAndParcelableClass.second;
if (clazz != null) {
if (!clazz.isAssignableFrom(parcelableClass)) {
throw new BadTypeParcelableException("Parcelable creator " + name + " is not "
+ "a subclass of required class " + clazz.getName()
+ " provided in the parameter");
}
}
return (Parcelable.Creator<T>) creator;
}
Parcelable.Creator<?> creator;
Class<?> parcelableClass;
try {
// If loader == null, explicitly emulate Class.forName(String) "caller
// classloader" behavior.
ClassLoader parcelableClassLoader =
(loader == null ? getClass().getClassLoader() : loader);
// Avoid initializing the Parcelable class until we know it implements
// Parcelable and has the necessary CREATOR field. http://b/1171613.
parcelableClass = Class.forName(name, false /* initialize */,
parcelableClassLoader);
if (!Parcelable.class.isAssignableFrom(parcelableClass)) {
throw new BadParcelableException("Parcelable protocol requires subclassing "
+ "from Parcelable on class " + name);
}
if (clazz != null) {
if (!clazz.isAssignableFrom(parcelableClass)) {
throw new BadTypeParcelableException("Parcelable creator " + name + " is not "
+ "a subclass of required class " + clazz.getName()
+ " provided in the parameter");
}
}
Field f = parcelableClass.getField("CREATOR");
if ((f.getModifiers() & Modifier.STATIC) == 0) {
throw new BadParcelableException("Parcelable protocol requires "
+ "the CREATOR object to be static on class " + name);
}
Class<?> creatorType = f.getType();
if (!Parcelable.Creator.class.isAssignableFrom(creatorType)) {
// Fail before calling Field.get(), not after, to avoid initializing
// parcelableClass unnecessarily.
throw new BadParcelableException("Parcelable protocol requires a "
+ "Parcelable.Creator object called "
+ "CREATOR on class " + name);
}
creator = (Parcelable.Creator<?>) f.get(null);
} catch (IllegalAccessException e) {
Log.e(TAG, "Illegal access when unmarshalling: " + name, e);
throw new BadParcelableException(
"IllegalAccessException when unmarshalling: " + name, e);
} catch (ClassNotFoundException e) {
Log.e(TAG, "Class not found when unmarshalling: " + name, e);
throw new BadParcelableException(
"ClassNotFoundException when unmarshalling: " + name, e);
} catch (NoSuchFieldException e) {
throw new BadParcelableException("Parcelable protocol requires a "
+ "Parcelable.Creator object called "
+ "CREATOR on class " + name, e);
}
if (creator == null) {
throw new BadParcelableException("Parcelable protocol requires a "
+ "non-null Parcelable.Creator object called "
+ "CREATOR on class " + name);
}
synchronized (sPairedCreators) {
sPairedCreators.get(loader).put(name, Pair.create(creator, parcelableClass));
mCreators.get(loader).put(name, creator);
}
return (Parcelable.Creator<T>) creator;
}
ということで、そろそろ目的を忘れてしまっているかもしれませんが。引数のclazzが何に使われているかですね。
キャッシュを利用したりリファレクションを使って取得したりということでコードが長くなっていますが、clazzが使われているのは以下
if (clazz != null) {
if (!clazz.isAssignableFrom(parcelableClass)) {
throw new BadTypeParcelableException("Parcelable creator " + name + " is not "
+ "a subclass of required class " + clazz.getName()
+ " provided in the parameter");
}
}
Parcelable自体はParcelにクラス名を書き込んでいるため、その情報に基づいてParceableへデシリアライズが行われますが、デシリアライズを実行する前に、取得しようとしているクラスへ代入可能なクラスなのかのチェックが行われています。動作の違いとしてはこれだけですね。
あくまで現在の実装でどうなっているかだけではありますが、推奨版のメソッドを使うと、デシリアライズ実行前に型チェックを行ってくれるという違いがあり、それ以外の違いはなさそうでした。
例えば、SaveStateHandleへIntentのExtraやFragmentのArgumentが展開されますが、その実装ではBudleから型チェックなしにObjectとして読み出して格納しているため、キャスト先のClassを渡さないと読み出せない、という実装に変わることはないかなと予想しています。
以上です。