preface

QA reported A small bug this week, the order of data from page A to page B is not correct. After checking the code, it turns out that the data storage container in page A uses HashMap, while HasMap access is out of order, so the natural order is not correct when the data is passed to B to read.

To solve

Since HashMap is unordered, I’ll just use LinkedHashMap instead, which is what most people think when they see this bug. So I added a Linked link to the HashMap, clicked Run, drank a cup of tea and quietly waited for the miracle to happen.

B received HashMap instead of LinkedHashMap, how can !!!! I quickly put down my teacup and reviewed the code. Yes, page A is A LinkedHashMap, but page B is A HashMap.

I did a quick Google search and there are quite a few people who have encountered this error. One solution suggested in the comments section is to serialize the LinkedHashMap to a String using Gson and pass it… Because of the bug rush, I didn’t try this method, and gave up passing maps in favor of an ArrayList. But then I looked at the source code and found another way, which I will talk about later.

why

The Bug was fixed, but the Intent’s failure to deliver LinkedHashMap was still on my mind, so I took a quick look at the source code and realized!

HashMap implements Serializable interface, while LinkedHashMap is an extension of HashMap.

intent.putExtra("map",new LinkedHashMap<>());Copy the code

Then look inside:

public Intent putExtra(String name, Serializable value) {
    if (mExtras == null) {
        mExtras = new Bundle();
    }
    mExtras.putSerializable(name, value);
    return this;
}Copy the code

Intent is directly constructed a Bundle, the data is passed to the Bundle, Bundle. PutSerializable () is called directly the superclass BaseBundle putSerializable () :

void putSerializable(@Nullable String key, @Nullable Serializable value) {
    unparcel();
    mMap.put(key, value);
}Copy the code

The value is put directly into an ArrayMap, and nothing special is done.

When did this LinkedHashMap turn into a HashMap? Is it possible that the processing is done in startActivity ()?

Engineers who know how to start an activity should know that startActivity () ends with:

 ActivityManagerNative.getDefault().startActivity()Copy the code

ActivityManagerNative is a Binder object whose functionality is implemented in ActivityManagerService and whose proxy object in the APP process is ActivityManagerProxy. So the above startActivity () the last call is ActivityManagerProxy startActivity (), let’s take a look at the source code of this method:

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;
}Copy the code

Intent.writetoparcel (data, 0); 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); }Copy the code

The last line calls the Parcel. WriteBundle () method, passing it as mExtras, where the previous LinkedHashMap was placed.

    public final void writeBundle(Bundle val) {
    if (val == null) {
        writeInt(-1);
        return;
    }

    val.writeToParcel(this, 0);
}Copy the code

This calls bundle.writetopArcel (), which eventually calls writeToParcelInner (), its BaseBundle parent:

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); . }}Copy the code

Visible, in the last else branch will call Parcel. WriteArrayMapInternal (mMap), the mMap to Bundle stored in K – V ArrayMap, see if there is do special with 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)); }}Copy the code

In the final for loop, all k-V pairs in mMap are iterated, first calling writeString () to write the Key and then writeValue () to write the Value. The truth is in 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); }... . }Copy the code

If the value type is Map, a VAL_MAP constant is written, followed by a call to writeMap () to write value. WriteMap () finally goes to 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()); }}Copy the code

As you can see, instead of serializing the LinkedHashMap directly, all k-Vs are iterated, writing each Key and Value in turn, so the LinkedHashMap is meaningless at this point.

So what happens when page B reads this LinkedHashMap? When reading data from an Intent, you end up with 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);
        returnnull; }}Copy the code

Unparcel () : unparcel () unparcel () : 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);
    }Copy the code

Here mainly to read data, then fill in the mMap, the key point lies in 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();
}Copy the code

WriteArrayMapInternal () calls readString to read the Key value and readValue () to read the value.

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); . }}Copy the code

This corresponds to the previous writeValue (), which first reads the type constant value written between, or readHashMap () in the case of VAL_MAP:

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;
}Copy the code

ReadHashMap () creates a new HashMap, and then reads the k-v values that were previously written to fill the HashMap, so the page B gets the HashMap instead of the LinkedHashMap.

More than a problem solution

While you cannot pass a LinkedHashMap directly, you can pass a class object implementing the Serializable interface into which you pass the LinkedHashMap as a member variable. Such as:

public class MapWrapper implements Serializable {

  private HashMap mMap;

  public void setMap(HashMap map){
      mMap=map;
  }

  public HashMap getMap() {
      returnmMap; }}Copy the code

So why does it work that way? It’s also quite simple, because when writeValue () is written to a Serializable object, then writeSerializable () is called:

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); }}Copy the code

As you can see, this object is serialized directly into a byte array, instead of entering writeMap () because it contains a Map object, so the LinkedHashMap is saved.

Conclusion:

In a word, encounter problems on the source code!