一個ANDROID開發菜鳥的BUNDLE與MAP理解

深入瞭解Bundle和Map--轉自泡在網上的日子”

前言

因爲往Bundle對象中放入Map實際上沒有表面上看起來那麼容易。

這篇博客是在Eugenio @workingkills Marletti的幫助下完成的。

警告:這是一篇篇幅較長的博客

案例:往Bundle對象放入特殊的Map

假設有這樣一個案例:你需要將一個要傳遞的map附加到Intent對象。這個案例雖然不常見,但是,這種情況也是很有可能發生。

如果你在Intent對象中附加的是一個Map最常見的接口實現類HashMap,而不是包含附加信息的自定義類,你是幸運的,你可以用以下方法將map附加到Intent對象:

1
 intent.putExtra("map",myHashMap);

在你接收的Activity裏,你可以用以下方法毫無問題地取出之前在Intent中附加的Map:

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

但是,如果你在Intent對象附加另一種類型的Map,比如:一個TreeMap(或者其他的自定義Map接口實現類),你在Intent中取出之前附加的TreeMap時,你用如下方法:

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

然後就會出現一個類轉換異常:

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

因爲編譯器認爲你的Map(TreeMap)正試圖轉換成一個HashMap

稍後我會詳細地爲大家講解我爲什麼用 getSerializableExtra() 這個方法來取出附加到Intent中的Map。現在我可以先給大家一個通俗易懂的解釋:因爲所有默認的 Map 接口實現類都是Serializable,並且 putExtra()/getExtra() 方法接受的參數幾乎都是“鍵-值”對,而其中值的類型非常廣泛,Serializable就是其中之一,因此我們能夠使用 getSerializableExtra() 來取得我們傳遞的Map。

在我們進行下一步之前,讓我們去了解調用putExtra()/get*Extra()時涉及到的一些類或方法。

提示:文章很長,如果你只是想要一個解決方案的話,請跳到文章的最後面解決方案那裏。

Parcels:

大家都知道(也許少部分人不知道),在Android 系統中所有進程間通信是基於Binder機制。但是,希望大家明白的是允許數據在進程間傳遞是基於Parcel。

Parcel是Android進程間通信中, 高效的專用序列化機制。

與 Serializable 相反,Parcels 決不應該被用於儲存任何類型的持久性數據,因爲 Parcels 並不是爲“操作可更新數據”(可更新數據指的是,具有持久性的數據會由於它的長留存時間會不斷更新它的值)提供的,Parcels 更多的是傳遞 “短暫的一次性數據”,所以,不管什麼時候使用Bundle,你在底層處理的都是Parcel。比如附加數據到Intent對象,在Fragment中設 參數,等等。

Parcels 能處理很多類型,包括:本地類型,字符串類型,數組類型,Map類型,sparse arrays類型,以及parcelables和serializables對象。 除非你必須使用Serializable,一般情況下推薦使用Parcelables讀寫數據到Parcel.

相較於Serializable,Parcelable的優勢更多地體現在性能上,因爲Parcelable在內存開銷方面更小,而這個理由足以讓我們在大多數情況下毫不猶豫地選擇Parcelable而不是Serializable。

深入底層分析

讓我們來了解下是什麼原因使我們得到了ClassCastException異常。 從我們的代碼中可以看到,我們對Intent中putExtras()的調用實際上是傳入了一個String值和一個Serializable的對象,而不是傳入一個Map值。因爲Map接口實現類都是Serializable的,而不是Parcelable的。

第一步:找到第一個突破口

讓我們來看看在Intent.putExtra(String, Serializable)方法中做了什麼。

Intent.java

1
2
3
4
5
6
public Intent putExtra(String name, Serializable value) {
      // ...
      mExtras.putSerializable(name, value);
      return this;
    }
}

在這裏,mExtras是個Bundle,Intent指令所有附加信息到bundle,從而調用了Bundle的putSerializable()方法,讓我們來看看在Bundle中的putSerializable()方法中做了什麼:

Bundle.java

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

從上面代碼我們可以看出,Bundle中的putSerializable()方法中只是對父類的實現,調用了父類BaseBundle中的putSerializable()方法。

BaseBundle.java

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

首先,讓我們忽略其中的unparcel()這個方法。我們注意到mMap是一個ArrayMap類型的。這告訴我們,到了這步,我們往mMap中 設入的是一個Object類型的。也就是說,不管我們之前是什麼類型,在父類BaseBundle這裏都轉成了Obeject類型。

這裏開始出現問題了

68747470733a2f2f64323632696c623531686c7478302e636c6f756466726f6e742e6e65742f6d61782f3830302f312a476f6b53566b3377514968476f4351745649774e54412e676966.gif

第二步:分析寫入 map

有趣的是當把Bundle中的值寫入到一個Parcel中時,如果此時我們去檢查我們附加值的類型,我們發現仍然能得到正確的類型。

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

如我們所料,這裏打印出來的是TreeMap類型的。因此,在Bundle中寫成一個Parcel,與再次讀這期間一定發生了類型轉換。

如果我們觀察下是怎樣寫入Parcel的,我們看到,實際上是調BaseBundle中的writeToParcelInner()方法。

BaseBundle.java

1
2
3
4
5
6
7
8
9
10
11
void writeToParcelInner(Parcel parcel, int flags) {
  if (mParcelledData != null) {
    // ...
  else {
    // ...
    int startPos = parcel.dataPosition();
    parcel.writeArrayMapInternal(mMap);
    int endPos = parcel.dataPosition();
    // ...
  }
}

跳過所有不相干的代碼,我們看到在Parcel的writeArrayMapInternal()方法中做了大量的事(mMap 是一個 ArrayMap類型)

Parcel.java

1
2
3
4
5
6
7
8
9
10
11
  /* package */ 
    void writeArrayMapInternal(ArrayMap<String, Object> val) {
        // ...
        int startPos;
        for (int i=0; i<N; i++) {
        // ...
        writeString(val.keyAt(i));
        writeValue(val.valueAt(i));
        // ...
      }
    }

下一步讓我們更深入地分析

68747470733a2f2f64323632696c623531686c7478302e636c6f756466726f6e742e6e65742f6d61782f3830302f312a544f426c7432574f564c456c6e515479472d523759512e676966.gif

第三步:分析寫入Map值

那麼,在Parcel中writeValue() 方法又是怎樣呢?主要是一些if...else語句。

Parcel.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 (/* you get the idea, this goes on and on */) {
        // ...
    else {
        Class<?> clazz = v.getClass();
        if (clazz.isArray() &&
        clazz.getComponentType() == Object.class) {
            // Only pure Object[] are written here, Other arrays of non-primitive types are
            // handled by serialization as this does not record the component type.
            writeInt(VAL_OBJECTARRAY);
             writeArray((Object[]) v);
        else if (v instanceof Serializable) {
            // Must be last
            writeInt(VAL_SERIALIZABLE);
            writeSerializable((Serializable) v);
        else {
            throw new RuntimeException("Parcel: unable to marshal value "+ v);
        }
    }
}

雖然TreeMap是以Serializable的類型傳入到 bundle,但是在Parcel中writeValue()方法執行的是map這個分支的代碼---“v instanceof Map”,(“v instanceof Map”在“v instanceOf Serializable”之前)

到了這裏,問題更明顯了。

現在我想,他們是不是對 Map 進行了一些非常規的處理,使得 Map 將無可避免地被轉換爲 HashMap 類型。

第四步:分析將Map寫入到Parcel中

Parcel中的writeMap()方法並沒有做什麼事,只是將我們傳入的Map值強轉成Map類型,調用writeMapInternal()方法。

Parcel.java

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

JavaDoc文檔對這個方法的解釋非常清楚:即Map必須是String類型的。

儘管我們可能傳入一個key值不爲String的Map,類型擦除也使我們不會獲得運行時錯誤。(這是完全非法的)

事實上,看一下Parcel中的writeMapInternal()方法,這更打擊我們。

Parcel.java

1
2
3
4
5
6
7
8
9
10
  /* 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());
      }
    }

類型擦除使所有的這些代碼都不會出現運行時錯誤。

實際上,在我們遍歷Map調用writeValue()方法時,依賴的是原先的類型檢查。從我們之前分析writeValue() 這個方法能看出,writeValue()能處理非String類型的key值。

也許這裏的文檔和代碼在某些地方有些不一致(還沒有同步)。 但是,如果你在一個Bundle裏對TreeMap進行設值和取值,將不會出現問題。 當然也還是會出現TreeMap轉換成HashMap的異常。

黑洞啓示錄:

1 sOK3EennxwiAPNzz5s0C4Q.gif

在這裏已經非常清楚了,當Map寫入到一個Parcel時,Map丟失了它們的類型,所以當我們再次讀時是沒辦法來複原原來的信息。

第五步:分析讀Map

讓我們來看看Parcel中readValue()這個方法,這個方法和writeValue()相對應。

Parcel.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 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);
 
    // ...
      }
    }

parcel處理寫入數據的方式是:

  1. 寫入一個int來定義數據類型(一個VAL_*的常量)。

  2. 存儲數據本身(包括其他一些元數據,比如String這種沒有固定大小的類型的數據長度)。

  3. 遞歸調用非原始數據類型。

這裏我們可以看到,readValue()方法中,首先讀取一個int的數據,這個int數據是在writeValue()中將TreeMap設成的VAL_MAP的常量,然後去匹配後面的分支,調用readHashMap()方法來取回數據。

Parcel.java

1
2
3
4
5
6
7
8
9
10
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()這個方法只是將我們從Parcel中讀取的map重新進行打包。

這就是爲什麼我們總是從Bundle中獲得一個HashMap,同樣的,如果你創建了一個實現了Parcelable自定義類型Map,得到的也是一個HashMap。

很難說本身設計如此,還是是一個疏忽。 這確實是一個極端例子,因爲在一個Intent中傳一個Map是比較少見的,你也只有很小的理由來傳Serializable而不是Parcelable的。 但是文檔上沒有寫,這讓我覺得應該是個疏忽,而不是本身設計如此。

解決方案:

好了,分析底層代碼我們已經弄明白了我們的問題,現在我們定位到問題的關鍵位置。 我們需要明白的是在writeValue()方法中,TreeMap沒有進入“v instanceOf Map”這個分支

當我和 Eugenio談話時,我想到的第一個想法是將map包裹成一個Serializable的容器,這個想法是醜陋的但是有效的。 Eugenio迅速寫了個通用的wrapper類解決了這個問題。

MapWrapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 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();
  }
}

另一個可行的解決方案:

另一個解決方法是,在你將Map附加到Intent前,將Map轉成byte array的。然後調用getByteArrayExtra()方法。但是這種方法你必須處理序列化與反序列化的問題。

如果你想要其他的解決方案,你可以依據Eugenio提供的代碼要點來寫一個。

當你不能掌控Intent上面的代碼時:

也許,有這樣或那樣的原因,對於Bundle中的代碼你無法掌控,比如可能在第三方的library中。

這種情況,要想到, Map接口實現類有一個構造器方法,可以將map作爲參數傳入,比如 new TreeMap(Map),你可以把從Bundle中取回的HashMap,用構造器的方式轉成你想要的類型。

不過,要記得的是,這種用構造器的方式,map中的附加屬性將會丟失,只有鍵值對被保存了下來。

總結:

在Android開發中,你可能會被一些表面的事所欺騙,特別是一些小的,似乎是無關緊要的事。

當事情沒有像我們期盼中那樣發生時,不要死盯着JavaDoc文檔,因爲JavaDoc可能過時了,JavaDoc的作者也不知道你的特殊需求。這個時候去看看源碼,答案可能在AOSP代碼裏。

AOSP代碼是我們的巨大財富,這在移動開發領域幾乎是獨一無二的,因爲我們能夠準確的知道在底層都做了些什麼。

當你知道了底層代碼執行了什麼,你也就能夠成爲一個更好的開發人員。

記住:凡事要嘛就是成功,要嘛就是失敗

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章