前言
这周 QA 报了一个小 bug, 页面 A 传给页面 B 的数据顺序不对, 查了一下代码, 原来页面 A 中数据存储容器用的是 HashMap, 而 HasMap 存取是无序的, 所以传给 B 去读数据的时候, 自然顺序不对
解决
既然 HashMap 是无序的, 那我直接用 LinkedHashMap 来代替不就行了, 大多数人估计看到这个 bug 时, 开始都是这么想的于是我就顺手在 HashMap 前加了一个 Linked, 点了一下 run, 泯上一口茶, 静静等待着奇迹的发生
然而奇迹没有来临, 奇怪的事反倒是发生了, B 页面收到数据后, 居然报了一个类型强转错误, B 收到的是 HashMap, 而不是 LinkedHashMap, 怎么可能!!!! 我赶紧放下茶杯, review 了一下代码, 没错啊, A 页面传递的确实是 LinkedHashMap, 但是 B 拿到就是 HashMap, 真是活见鬼了
我立马 Google 了一下, 遇到这个错误的人还真不少, 评论区给出的一种解决方案就是用 Gson 将 LinkedHashMap 序列化成 String, 再进行传递由于 bug 催的紧, 我也没有去尝试这种方法了, 直接就放弃了传递 Map, 改用 ArrayList 了不过后来看源码, 又发现了另外一种方式, 稍后再说
原因
Bug 倒是解决了, 但是 Intent 无法传递 LinkedHashMap 的问题还在我脑海里萦绕, 我就稍微翻看了一下源码, 恍然大悟!
HashMap 实现了 Serializable 接口, 而 LinkedHashMap 是继承自 HashMap 的, 所以用 Intent 传递是没有问题的, 我们先来追一下 A 页面传递的地方:
intent.putExtra("map",new LinkedHashMap<>());
接着往里看:
- public Intent putExtra(String name, Serializable value) {
- if (mExtras == null) {
- mExtras = new Bundle();
- }
- mExtras.putSerializable(name, value);
- return this;
- }
intent 是直接构造了一个 Bundle, 将数据传递到 Bundle 里, Bundle.putSerializable() 里其实也是直接调用了父类 BaseBundle.putSerializable():
- void putSerializable(@Nullable String key, @Nullable Serializable value) {
- unparcel();
- mMap.put(key, value);
- }
这里直接将 value 放入了一个 ArrayMap 中, 并没有做什么特殊处理事情到这似乎没有了下文, 那么这个 LinkedHashMap 又是何时转为 HashMap 的呢? 有没有可能是在 startActivity() 中做的处理呢? 了解 activity 启动流程的工程师应该清楚, startActivity() 最后调的是:
ActivityManagerNative.getDefault().startActivity()
ActivityManagerNative 是个 Binder 对象, 其功能实现是在 ActivityManagerService 中, 而其在 app 进程中的代理对象则为 ActivityManagerProxy 所以上面的 startActivity() 最后调用的是 ActivityManagerProxy.startActivity(), 我们来看看这个方法的源码:
- public int startActivity(IApplicationThread caller, String callingPackage, Intent intent,
- String resolvedType, IBinder resultTo, String resultWho, int requestCode,
- int startFlags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException {
- Parcel data = Parcel.obtain();
- Parcel reply = Parcel.obtain();
- ......
- intent.writeToParcel(data, 0);
- ......
- int result = reply.readInt();
- reply.recycle();
- data.recycle();
- return result;
- }
注意到方法中调用了 intent.writeToParcel(data, 0), 难道这里做了什么特殊处理?
- public void writeToParcel(Parcel out, int flags) {
- out.writeString(mAction);
- Uri.writeToParcel(out, mData);
- out.writeString(mType);
- out.writeInt(mFlags);
- out.writeString(mPackage);
- ......
- out.writeBundle(mExtras);
- }
最后一行调用了 Parcel.writeBundle() 方法, 传参为 mExtras, 而之前的 LinkedHashMap 就放在这 mExtras 中
- public final void writeBundle(Bundle val) {
- if (val == null) {
- writeInt(-1);
- return;
- }
- val.writeToParcel(this, 0);
- }
这里最后调用了 Bundle.writeToParcel(), 最终会调用到其父类 BaseBundle 的 writeToParcelInner():
- void writeToParcelInner(Parcel parcel, int flags) {
- // Keep implementation in sync with writeToParcel() in
- // frameworks/native/libs/binder/PersistableBundle.cpp.
- final Parcel parcelledData;
- synchronized (this) {
- parcelledData = mParcelledData;
- }
- if (parcelledData != null) {
- ......
- } else {
- // Special case for empty bundles.
- if (mMap == null || mMap.size() <= 0) {
- parcel.writeInt(0);
- return;
- }
- ......
- parcel.writeArrayMapInternal(mMap);
- ......
- }
- }
可见最后 else 分支里, 会调用 Parcel.writeArrayMapInternal(mMap), 这个 mMap 即为 Bundle 中存储 K-V 的 ArrayMap, 看看这里有没有对 mMap 做特殊处理:
- void writeArrayMapInternal(ArrayMap<String, Object> val) {
- if (val == null) {
- writeInt(-1);
- return;
- }
- // Keep the format of this Parcel in sync with writeToParcelInner() in
- // frameworks/native/libs/binder/PersistableBundle.cpp.
- final int N = val.size();
- writeInt(N);
- if (DEBUG_ARRAY_MAP) {
- RuntimeException here = new RuntimeException("here");
- here.fillInStackTrace();
- Log.d(TAG, "Writing" + N + "ArrayMap entries", here);
- }
- int startPos;
- for (int i=0; i<N; i++) {
- if (DEBUG_ARRAY_MAP) startPos = dataPosition();
- writeString(val.keyAt(i));
- writeValue(val.valueAt(i));
- if (DEBUG_ARRAY_MAP) Log.d(TAG, "Write #" + i + " "
- + (dataPosition()-startPos) + "bytes: key=0x"
- + Integer.toHexString(val.keyAt(i) != null ? val.keyAt(i).hashCode() : 0)
- + " " + val.keyAt(i));
- }
- }
在最后的 for 循环中, 会遍历 mMap 中所有的 K-V 对, 先调用 writeString() 写入 Key, 再调用 writeValue() 来写入 Value 真相就在 writeValue() 里:
- public final void writeValue(Object v) {
- if (v == null) {
- writeInt(VAL_NULL);
- } else if (v instanceof String) {
- writeInt(VAL_STRING);
- writeString((String) v);
- } else if (v instanceof Integer) {
- writeInt(VAL_INTEGER);
- writeInt((Integer) v);
- } else if (v instanceof Map) {
- writeInt(VAL_MAP);
- writeMap((Map) v);
- }
- ......
- ......
- }
这里会判断 value 的具体类型, 如果是 Map 类型, 会先写入一个 VAL_MAP 的类型常量, 紧接着调用 writeMap() 写入 valuewriteMap() 最后走到了 writeMapInternal():
- void writeMapInternal(Map <String, Object> val) {
- if (val == null) {
- writeInt( - 1);
- return;
- }
- Set <Map.Entry < String,
- Object>> entries = val.entrySet();
- writeInt(entries.size());
- for (Map.Entry <String, Object> e: entries) {
- writeValue(e.getKey());
- writeValue(e.getValue());
- }
- }
可见, 这里并没有直接将 LinkedHashMap 序列化, 而是遍历其中所有 K-V, 依次写入每个 Key 和 Value, 所以 LinkedHashMap 到这时就已经失去意义了那么 B 页面在读取这个 LinkedHashMap 的时候, 是什么情况呢? 从 Intent 中读取数据时, 最终会走到 getSerializable():
- Serializable getSerializable(@Nullable String key) {
- unparcel();
- Object o = mMap.get(key);
- if (o == null) {
- return null;
- }
- try {
- return (Serializable) o;
- } catch (ClassCastException e) {
- typeWarning(key, o, "Serializable", e);
- return null;
- }
- }
这里乍一看就是直接从 mMap 中通过 key 取到 value, 其实重要的逻辑全都在第一句 unparcel() 中:
- synchronized void unparcel() {
- synchronized (this) {
- final Parcel parcelledData = mParcelledData;
- if (parcelledData == null) {
- if (DEBUG) Log.d(TAG, "unparcel"
- + Integer.toHexString(System.identityHashCode(this))
- + ": no parcelled data");
- return;
- }
- if (LOG_DEFUSABLE && sShouldDefuse && (mFlags & FLAG_DEFUSABLE) == 0) {
- Slog.wtf(TAG, "Attempting to unparcel a Bundle while in transit; this may"
- + "clobber all data inside!", new Throwable());
- }
- if (isEmptyParcel()) {
- if (DEBUG) Log.d(TAG, "unparcel"
- + Integer.toHexString(System.identityHashCode(this)) + ": empty");
- if (mMap == null) {
- mMap = new ArrayMap<>(1);
- } else {
- mMap.erase();
- }
- mParcelledData = null;
- return;
- }
- int N = parcelledData.readInt();
- if (DEBUG) Log.d(TAG, "unparcel" + Integer.toHexString(System.identityHashCode(this))
- + ": reading" + N + "maps");
- if (N <0) {
- return;
- }
- ArrayMap<String, Object> map = mMap;
- if (map == null) {
- map = new ArrayMap<>(N);
- } else {
- map.erase();
- map.ensureCapacity(N);
- }
- try {
- parcelledData.readArrayMapInternal(map, N, mClassLoader);
- } catch (BadParcelableException e) {
- if (sShouldDefuse) {
- Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
- map.erase();
- } else {
- throw e;
- }
- } finally {
- mMap = map;
- parcelledData.recycle();
- mParcelledData = null;
- }
- if (DEBUG) Log.d(TAG, "unparcel" + Integer.toHexString(System.identityHashCode(this))
- + "final map:" + mMap);
- }
这里主要是读取数据, 然后填充到 mMap 中, 其中关键点在于 parcelledData.readArrayMapInternal(map, N, mClassLoader):
- void readArrayMapInternal(ArrayMap outVal, int N,
- ClassLoader loader) {
- if (DEBUG_ARRAY_MAP) {
- RuntimeException here = new RuntimeException("here");
- here.fillInStackTrace();
- Log.d(TAG, "Reading" + N + "ArrayMap entries", here);
- }
- int startPos;
- while (N> 0) {
- if (DEBUG_ARRAY_MAP) startPos = dataPosition();
- String key = readString();
- Object value = readValue(loader);
- if (DEBUG_ARRAY_MAP) Log.d(TAG, "Read #" + (N-1) + " "
- + (dataPosition()-startPos) + "bytes: key=0x"
- + Integer.toHexString((key != null ? key.hashCode() : 0)) + " " + key);
- outVal.append(key, value);
- N--;
- }
- outVal.validate();
- }
这里其实对应于之前所说的 writeArrayMapInternal(), 先调用 readString 读出 Key 值, 再调用 readValue() 读取 value 值, 所以重点还是在于 readValue():
- public final Object readValue(ClassLoader loader) {
- int type = readInt();
- switch (type) {
- case VAL_NULL:
- return null;
- case VAL_STRING:
- return readString();
- case VAL_INTEGER:
- return readInt();
- case VAL_MAP:
- return readHashMap(loader);
- ......
- }
- }
这里对应之前的 writeValue(), 先读取之间写入的类型常量值, 如果是 VAL_MAP, 就调用 readHashMap():
- public final HashMap readHashMap(ClassLoader loader){
- int N = readInt();
- if (N < 0) {
- return null;
- }
- HashMap m = new HashMap(N);
- readMapInternal(m, N, loader);
- return m;
- }
真相大白了, readHashMap() 中直接 new 了一个 HashMap, 再依次读取之前写入的 K-V 值, 填充到 HashMap 中, 所以 B 页面拿到就是这个 HashMap, 而拿不到 LinkedHashMap 了
一题多解
虽然不能直接传 LinkedHashMap, 不过可以通过另一种方式来传递, 那就是传递一个实现了 Serializable 接口的类对象, 将 LinkedHashMap 作为一个成员变量放入该对象中, 再进行传递如:
- public class MapWrapper implements Serializable {
- private HashMap mMap;
- public void setMap(HashMap map){
- mMap=map;
- }
- public HashMap getMap() {
- return mMap;
- }
- }
那么为什么这样传递就行了呢? 其实也很简单, 因为在 writeValue() 时, 如果写入的是 Serializable 对象, 那么就会调用 writeSerializable():
- public final void writeSerializable(Serializable s) {
- if (s == null) {
- writeString(null);
- return;
- }
- String name = s.getClass().getName();
- writeString(name);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try {
- ObjectOutputStream oos = new ObjectOutputStream(baos);
- oos.writeObject(s);
- oos.close();
- writeByteArray(baos.toByteArray());
- } catch (IOException ioe) {
- throw new RuntimeException("Parcelable encountered" +
- "IOException writing serializable object (name =" + name +
- ")", ioe);
- }
- }
可见这里直接将这个对象给序列化成字节数组了, 并不会因为里面包含一个 Map 对象而再走入 writeMap(), 所以 LinkedHashMap 得以被保存了
结论:
一句话, 遇到问题就多看源码!
来源: https://zhuanlan.zhihu.com/p/28119448