Gain insight into bundles and maps

Source: Internet
Author: User

reprinted from:http://www.devtf.cn/?p=120https://medium.com/the-wtf-files/the-mysterious-case-of-the-bundle-and-the-map-7b15279a794e

  • SOURCE Link: The mysterious case of the Bundle and the Map
Objective

Because putting a map into a bundle object isn't actually as easy as it looks on the surface.

This blog was completed with the help of Eugenio @workingkills Marletti.

Warning: This is a longer blog

Case: Placing a special map into the bundle object

Suppose there is a case where you need to attach a map to be passed to the intent object. This case is not common, but it is also likely to happen.

If you attach a map to the intent object that is the most common interface implementation class HashMap, instead of a custom class that contains additional information, you are lucky that you can attach a map to the intent object in the following ways:

 intent.putExtra("map",myHashMap);

In the activity you receive, you can use the following method to remove the previously attached map in intent:

 HashMap map = (HashMap) getIntent().getSerializableExtra("map");

However, if you attach another type of map to the intent object, such as a treemap (or other custom map interface implementation Class), you use the following method when you remove the treemap attached to intent:

 TreeMap map = (TreeMap) getIntent().getSerializableExtra("map");`

Then there is a class conversion exception:

 java.lang.ClassCastException: java.util.HashMap cannot be cast to java.util.TreeMap`

Because the compiler thinks your map (TREEMAP) is trying to convert to a HashMap

Later I will explain in detail why I use Getserializableextra () to remove the map attached to the intent. Now I can give you a plain explanation: because all the default MAP interface implementation classes are serializable, and the PutExtra ()/getextra () method accepts almost all key-value pairs, where the type of the values is very wide, Serializable is one of them, so we can use Getserializableextra () to get the map we pass.

Before we proceed to the next step, let's take a look at some of the classes or methods involved in calling Putextra ()/get*extra ().

Tip: The article is very long, if you just want a solution, skip to the end of the article solution there.

Parcels:

Everyone knows (perhaps a few people don't know) that all interprocess communication in Android is based on the binder mechanism. However, I hope you understand that allowing data to pass between processes is based on parcel.

Parcel is an efficient, dedicated serialization mechanism in inter-process communication between Android.

In contrast to Serializable, Parcels should never be used to store any type of persistent data, because Parcels is not for "Operation Updatable Data" (updatable data refers to the fact that persistent data is provided by the value of its long retention time, which is constantly being updated), Parcels is more about passing "ephemeral disposable data", so whenever you use bundles, you're dealing with parcel at the bottom. For example, append data to Intent object, set parameters in fragment, and so on.

Parcels can handle many types, including: local type, String type, array type, map type, sparse arrays type, and Parcelables and Serializables objects.
Unless you have to use serializable, it is generally recommended to use Parcelables to read and write data to parcel.

The advantage over serializable,parcelable is more in performance because parcelable is smaller in terms of memory overhead, which is enough for us to choose parcelable rather than serializable in most cases.

Deep analysis of the underlying

Let's find out what's causing us to get the classcastexception anomaly.
As we can see from our code, our call to Putextras () in intent is actually passing in a string value and a serializable object instead of passing in a map value. Because the map interface implementation class is serializable, not parcelable.

Step one: Find the first breakthrough

Let's take a look at what we did in the Intent.putextra (String, Serializable) method.

Intent.java

public Intent putExtra(String name, Serializable value) {      // ...      mExtras.putSerializable(name, value);      return this;    }}

Here, Mextras is a bundle,intent instruction all the additional information to the bundle, thus invoking the bundle's Putserializable () method, let's take a look at the putserializable in the bundle () What to do in the method:

Bundle.java

@Override    public void putSerializable(String key, Serializable value) {      super.putSerializable(key, value);    }

As we can see from the above code, the Putserializable () method in the bundle is just the implementation of the parent class, calling the Putserializable () method in the parent class Basebundle.

Basebundle.java

 void putSerializable(String key, Serializable value) {      unparcel();      mMap.put(key, value);    }

First, let's ignore the Unparcel () method. We note that Mmap is a arraymap<string, object> type. This tells us that by this step, we are setting an object type into the mmap. That is, regardless of what type we were before, the parent class Basebundle here is converted to the Obeject type.

There's a problem starting here.

Step Two: Parse write map

The interesting thing is that when we write the values in the bundle into a parcel, if we check the type of our added value at this point, we find that we can still get the right type.

    Intent intent = new Intent(this, ReceiverActivity.class);    intent.putExtra("map", treeMap);    Serializable map = intent.getSerializableExtra("map");    Log.i("MAP TYPE", map.getClass().getSimpleName());

As we expected, the TreeMap type is printed here. As a result, a parcel is written in the bundle, and a type conversion must have occurred during the re-reading.

If we observe how the parcel is written, we see that it is actually the Writetoparcelinner () method in the Basebundle.

Basebundle.java

    void writeToParcelInner(Parcel parcel, int flags) {  if (mParcelledData != null) {    // ...  } else {    // ...    int startPos = parcel.dataPosition();    parcel.writeArrayMapInternal(mMap);    int endPos = parcel.dataPosition();    // ...  }}

Skipping all the extraneous code, we see a lot of things done in Parcel's writearraymapinternal () method (MMap is a arraymap type)

Parcel.java

  /* package */ void writeArrayMapInternal(        ArrayMap<String, Object> val) {        // ...        int startPos;        for (int i=0; i<N; i++) {        // ...        writeString(val.keyAt(i));        writeValue(val.valueAt(i));        // ...      }    }
Next, let's go deeper into the analysis

Step three: Analyze write map values

So what about the WriteValue () method in parcel? Are mainly some if...else statements.

Parcel.java

   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); } else if (/* Get the idea, this goes on and on * *) {//...} else {class<?> cl            Azz = V.getclass (); if (Clazz.isarray () && clazz.getcomponenttype () = = Object.class) {//Only pure object[] is W Ritten here, other arrays of non-primitive types is//handled by serialization as this does not record the comp            Onent type.             Writeint (Val_objectarray);        Writearray ((object[]) v);          } else if (v instanceof Serializable) {//must is lastWriteint (val_serializable);        Writeserializable ((Serializable) v);        } else {throw new RuntimeException ("Parcel:unable to marshal value" + V); }          }        }

Although TreeMap is passed into the bundle as a serializable type, the WriteValue () method in parcel executes the code of the Map branch-"v instanceof Map" ("v instanceof map" in "V instanceof Serializable")

When we get here, the problem is more obvious.

Now I think they did some unconventional processing of the map so that the map would inevitably be converted to the HashMap type.

Fourth step: Parse to write map to parcel

The Writemap () method in parcel does nothing, just turns the map value we passed in to Map<string, object> type, and calls the Writemapinternal () method.

Parcel.java

   public final void writeMap(Map val) {        writeMapInternal((Map<String, Object>) val);    }

The Javadoc document explains this method very clearly: that is, the map must be of type string.

Although we may pass a map with a key value that is not a string, type erasure also causes us not to get a run-time error. (This is completely illegal)

In fact, look at the Writemapinternal () method in parcel, which strikes us more.

Parcel.java

  /* package */ void writeMapInternal(Map<String,Object> val) {      // ...      Set<Map.Entry<String,Object>> entries = val.entrySet();      writeInt(entries.size());      for (Map.Entry<String,Object> e : entries) {        writeValue(e.getKey());        writeValue(e.getValue());      }    }

Type erasure makes no run-time errors for all of these code.

In fact, when we traverse the map call WriteValue () method, we rely on the original type check. From our previous analysis of WriteValue () This method can be seen, writevalue () can handle non-string type of key value.

Perhaps the documentation and code here are somewhat inconsistent (not yet synchronized) in some places.
However, if you set values and values for Treemap<integer, object> in a bundle, there will be no problem.
Of course, there will also be TreeMap converted into hashmap anomalies.

Black hole Apocalypse:

It's very clear here that when map is written to a parcel, the map loses its type, so when we read it again, there is no way to restore the original information.

Fifth Step: Analysis reading Map

Let's take a look at the ReadValue () method in parcel, which corresponds to WriteValue ().

Parcel.java

 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);    // ...      }    }

The way parcel handles writing data is:

    1. Writes an int to define the data type (a Val_* constant).

    2. Stores the data itself (including some other metadata, such as a string that has no fixed-size type of data length).

    3. Recursive invocation of non-raw data types.

<

P> Here we can see that, in the ReadValue () method, first read the data of an int, the int data is the VAL_MAP constant in WriteValue () TreeMap, and then go to match the following branch, call Readhashmap () method to retrieve the data.

Parcel.java

  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;    }

Readmapinternal () This method simply re-packs the map we read from the parcel.

That's why we always get a hashmap from bundles, and again, if you create a map that implements Parcelable custom type, you get a hashmap.

It is hard to say whether the design is so, or an oversight.
This is indeed an extreme example, because it is relatively uncommon to pass a map in a intent, and you have only a small reason to preach serializable rather than parcelable.
But the document is not written, which makes me think it should be an oversight, not the design itself.

Solution:

Well, the analysis of the underlying code we've figured out our problem, and now we're positioned to the point where the problem is.
What we need to understand is that in the WriteValue () method, TreeMap does not enter the "v instanceof Map" Branch

When I was talking to Eugenio, my first thought was to wrap the map into a serializable container, an ugly but effective idea.
Eugenio quickly wrote a generic wrapper class to solve the problem.

Mapwrapper.java

  public class MapWrapper<T extends Map & Serializable> implements Serializable {  private final T map;  public MapWrapper(T map) {    this.map = map;  }  public T getMap() {    return map;  public static <T extends Map & Serializable> Intent         putMapExtra(Intent intent, String name, T map) {    return intent.putExtra(name, new MapWrapper<>(map));  }  public static <T extends Map & Serializable> T         getMapExtra(Intent intent, String name)         throws ClassCastException {    Serializable s = intent.getSerializableExtra(name);    return s == null ? null : ((MapWrapper<T>)s).getMap();  }}  }
Another workable solution:

Another workaround is to turn the map into a byte array before you attach the map to the intent. Then call the Getbytearrayextra () method. But this way you have to deal with serialization and deserialization issues.

If you want another solution, you can write one based on the code points provided by Eugenio.

When you can't control the code above intent:

Perhaps, for one reason or another, you have no control over the code in the bundle, for example in a third-party library.

This situation, to think,
The map interface implementation class has a constructor method that allows you to pass in a map as a parameter, such as New TreeMap (map), and you can convert the HashMap from the bundle to the type you want with the constructor.

Remember, however, that this constructor means that the attached property in the map will be lost and only the key-value pairs are saved.

Summarize:

In Android development, you may be deceived by some superficial things, especially small ones that seem insignificant.

Don't stare at Javadoc documents when things don't happen as we expect them to, because Javadoc may be out of date, and Javadoc's authors don't know your special needs. This time to see the source, the answer may be in the AOSP code.

AOSP code is a huge asset for us, which is almost unique in mobile development because we know exactly what is being done on the ground floor.

When you know what the underlying code is doing, you can become a better developer.

Gain insight into bundles and maps

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.