[這篇文章是在歐金尼奧@workingkills Marletti的幫助下完成的。]
警告——這是一個很長的帖子。
假設(shè)你需要傳遞一個映射的值作為額外的意圖。這可能不是一個常見的情況,不可否認,但它也可能會發(fā)生。它的確發(fā)生在 Eugenio。
如果你正在使用一個哈希映射,這是映射中最常見的類型,你沒有創(chuàng)建一個包含“額外”信息的自定義類,那么你是幸運的。你可以寫入:
intent.putExtra("map", myHashMap);
在你接收的活動中,你會得到很好的映射,消除了額外的意圖:
HashMap map = (HashMap) getIntent().getSerializableExtra("map");
如果你需要以額外的意圖傳遞另外一種映射——比如說,一個樹形映射(或任何自定義實現(xiàn)),該怎么辦呢?好吧,當你找到它:
TreeMap map = (TreeMap) getIntent().getSerializableExtra("map");
然后,你可以得到:
java.lang.ClassCastException: java.util.HashMap cannot be cast to java.util.TreeMap
是的,一個很好的 ClassCastException異常,因為你的映射已經(jīng)變成…一個哈希映射。
我們將看到,為什么我們稍后會使用getSerializableExtra(),現(xiàn)在我們足以說,那是因為所有的默認映射的實現(xiàn)是可序列化的并且沒有范圍過窄的putExtra()/get*Extra()可以接受他們。
在我們繼續(xù)之前,讓我們來了解一下這個過程中所有的參與者。
[tl:dr; 如果你想要一個解決方案,請直接跳到最后的“解決方法”!]
你們中的很多人都知道(但也許有些人不知道),在 Android 框架下的所有 IPC 通信都是基于 Binders 的概念。并且希望你們很多人都知道,主要的機制是讓數(shù)據(jù)基于包在進程之間進行編組。
包是 Android 為 IPC 使用的一個優(yōu)化的、非通用的接口機制。與接口現(xiàn)象相反,你不應(yīng)該以任何一種持久性的形式使用包,因為它沒有提供用于處理不同版本的數(shù)據(jù)。每當你看到一個包,說明你正在引擎蓋下處理一個包。
添加額外意圖?包
在一個片段中設(shè)置參數(shù)?包
等等
包知道如何處理一大堆箱外類型,包括原生類型、字符串、數(shù)組、映射、稀疏陣列、打包和接口。打包是一種你必須可以讀寫數(shù)據(jù)到任意一個包的機制,除非你真的,真的組要使用接口。
打包相對于接口的優(yōu)勢主要是關(guān)于性能,在大多數(shù)情況下,這應(yīng)該是一個足夠更傾向于前者的理由,并且接口有一定的開銷。
所以,讓我們試著去了解什么讓我們得到一個 ClassCastException
。從我們所使用的代碼開始,我們可以看到,我們對 Intent#putExtras() 的調(diào)用解析需要一個字符串和接口。正如我們前面所說的,這是所預料到的,映射的實現(xiàn)是可序列化的,它們沒有被打包。此外,沒有一個 putExtras() 明確需要使用映射。
讓我們來看看在 Intent.putExtra(String,Serializable) 會發(fā)生什么:
public Intent putExtra(String name, Serializable value) {
// ...
mExtras.putSerializable(name, value);
return this;
}
在這里,mExtras顯然是一個包。那么好吧,意圖代表將所有的額外都打到一個包,就如同我們所預期的,并調(diào)用 Bundle#putSerializable()。讓我們看看這個方法:
@Override
public void putSerializable(String key, Serializable value) {
super.putSerializable(key, value);
}
事實證明,這恰恰僅代表了超級實現(xiàn),那就是:
void putSerializable(String key, Serializable value) {
unparcel();
mMap.put(key, value);
}
好,到最后我們嘗到了一些甜頭。
首先,讓我們忽略 unparcel()。我們可以看到,mMap 是一個 array<String,Object>。這告訴我們,我們正在失去任何一種曾經(jīng)擁有過的類型的信息,也就是說,在這一點上,不管我們將值放入包中使用的方法類型多么強大,一切都會在包含對象值的一個大的映射上結(jié)束 。
我們的蜘蛛意識開始發(fā)麻......
http://wiki.jikexueyuan.com/project/android-weekly/images/issue-145/1-GokSVk3wQIhGoCQtVIwNTA.gif" alt="映射與包的神秘關(guān)系" />
當我們真正開始寫包的內(nèi)容時,才是真正有趣的時候。在此之前,如果我們檢查額外的類型,我們?nèi)钥梢缘玫秸_的類型:
Intent intent = new Intent(this, ReceiverActivity.class);
intent.putExtra("map", treeMap);
Serializable map = intent.getSerializableExtra("map");
Log.i("MAP TYPE", map.getClass().getSimpleName());
這樣的輸出正如我們所預料,由 TreeMap 到 LogCat。所以這樣的轉(zhuǎn)變必須發(fā)生在包被寫入到所述包中并被再次讀取的時候。
如果我們看一下如何寫一個包時,我們可以看到 BaseBundle#writeToParcelInner 下的細節(jié)問題:
void writeToParcelInner(Parcel parcel, int flags) {
if (mParcelledData != null) {
// ...
} else {
// ...
int startPos = parcel.dataPosition();
parcel.writeArrayMapInternal(mMap);
int endPos = parcel.dataPosition();
// ...
}
跳過所有對我們無關(guān)緊要的代碼,我們可以看到,大部分工作都是由 Parcel#writeArrayMapInternal()(記住是 mMap 是一個數(shù)組映射)執(zhí)行的:
/* package */ void writeArrayMapInternal(
ArrayMap<String, Object> val) {
// ...
int startPos;
for (int i=0; i<N; i++) {
// ...
writeString(val.keyAt(i));
writeValue(val.valueAt(i));
// ...
}
}
基本上做的就是把在 BaseBundle 的映射里寫入的每一個鍵-值對根據(jù)值的大小形成連續(xù)的字符串(這里的值是字符串)。到目前為止后者似乎沒有考慮到值的類型。
讓我們更深一層!
http://wiki.jikexueyuan.com/project/android-weekly/images/issue-145/1-TOBlt2WOVLElnQTyG-R7YQ.gif" alt="映射與包的神秘關(guān)系" />
那么,你問,Parcel#writeValue() 看起來怎么樣?在這里,在它的 if-elseif-else 體現(xiàn):
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 作為一個借口放進包里,writeValue() 方法實際上也會把它放進映射分支的一個實例V中,這(原因很明顯)在 else…if(V 是接口的實例)分支之前。
在這一點上,感覺變得越來越強烈。
我現(xiàn)在想知道,對于映射他們是否使用了一些完全非法的捷徑,不知怎的就將他們變成了哈希映射?
http://wiki.jikexueyuan.com/project/android-weekly/images/issue-145/1-zA893bdM9QmLYPlRe7estg.gif" alt="映射與包的神秘關(guān)系" />
那么,事實上,就其本身而言,除了后續(xù)我們將強化映射的類型,writeMap() 自身做的事情并不多:
public final void writeMap(Map val) {
writeMapInternal((Map<String, Object>) val);
}
該方法的 JavaDoc 是很清楚的:
“映射的關(guān)鍵字必須是字符串對象?!?
類型擦除確保我們在這里不會有運行時間的錯誤,即使我們可能在傳遞一個關(guān)鍵字不是字符串類型的映射(再一次,這完全是較高水平的非法行為…)。
事實上,只要我們看一下 writeMapIntent(),我們就會想到:
/* 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());
}
}
再次,在這里類型擦除讓那些計算在運行時變得一文不值。事實是我們將之前對鍵和值進行類型檢查的 writeValue() 作為我們“解壓縮”映射,把一切都放進包里。正如我們所看見的,writeValue() 完全可以處理非字符串類型的鍵。
也許文檔和代碼在某些點上有點同步,但事實上,在包中放置和檢索一個 TreeMap<Integer,Object> 是相當容易的。
那么,理所當然,樹形映射成為一個哈希映射是一種例外。
黑洞與啟示
http://wiki.jikexueyuan.com/project/android-weekly/images/issue-145/1-zA893bdM9QmLYPlRe7estg.gif" alt="映射與包的神秘關(guān)系" />
好吧,這里的圖片已經(jīng)很清楚了。當映射被寫進一個包中時已經(jīng)完全失去他們的類型了,所以當他們進行回讀的時候沒有辦法對信息進行恢復。
作為最后一步,快速檢查我們的理論,讓我們?nèi)z查一下 readValue(),它是與 writeValue()相對應(yīng)的:
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);
// ...
}
}
當寫入數(shù)據(jù)時包工作的方式,每一項的內(nèi)容如下:
它定義了數(shù)據(jù)類型 int(VAL_*常量之一)
對數(shù)據(jù)本身進行轉(zhuǎn)儲(可以包括其他元數(shù)據(jù)如非固定大小的數(shù)據(jù)類型,例如字符串長度)
在這里,我們看到 readValue() 讀取數(shù)據(jù)的類型為 int,這使得我們的 TreeMap 通過 writeValue() 被設(shè)置為 VAL_MAP,然后根據(jù)相應(yīng)的選擇情況來調(diào)用 readHashMap() 來檢索數(shù)據(jù)本身:
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;
}
(the C#-style opening curly brace is actually in AOSP, it’s not my fault)
你幾乎可以想象,readMapInternal() 簡單的打包所有的映射條目,這些條目是從我們傳遞給映射的包中讀取的。
是的。這就是為什么你總是從一個包中得到一個哈希映射。如何你創(chuàng)建一個自定義的映射,通過接口進行實現(xiàn)結(jié)果也是如此。但絕對不是我們所希望的!
如果這是一個預期的效果或者只是一個疏忽那就很難說了。這是無可否認的一個邊緣情況,因為你有幾個真正的理由來將一個映射傳遞到一個Intent中,并且你應(yīng)該只是有很少的好理由去傳遞接口而不是包。但是缺乏文檔讓我覺得它實際上可能就只是一個疏忽而不是設(shè)計決策(從其他的設(shè)計決策中派生)。
好吧,我們要深一層的了解我們的問題,然而現(xiàn)在我們已經(jīng)確定了被我們打亂了的關(guān)鍵路徑。我們需要確保我們的 TreeMap 不被抓到由 writeValue() 映射檢查的實例 V 中。
當談?wù)摰?Eugenio 時我的大腦中想到的第一個解決方案是很單一的但卻很有效:將映射包裝到一個接口容器中。Eugenio 迅速投入到了這個普通的包裝中并確認它解決了這個問題。
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();
}
}
請注意,你在 gist 上找到的完整代碼是使用 Android 的 @NonNull 注釋強制執(zhí)行的。如果你想單純的在 Java 中使用這些代碼,你可以用 JetBrain 的 @NonNull 取代它,或者你也可以選擇脫離這些注釋。
另一個解決方法是把它作為一個 Intent extra 之前,自己先把映射提前序列化成一個字節(jié)數(shù)組,然后用 getByteArrayExtra() 對它進行檢索,但你必須手動處理序列化和反序列化。
如果你不怕麻煩想選擇其他的解決方案來代替,Eugenio 已經(jīng)為你提供了單獨 Gist 的代碼。
最后,也許出于某種原因,你無法控制 Bundle 創(chuàng)建的代碼——例如,因為它在某些第三方庫中。
在這種情況下,請記住,許多映射的實現(xiàn)需要有一個構(gòu)造函數(shù),該構(gòu)造函數(shù)是以映射作為輸入的,比如新建一個 TreeMap(Map)。如果有需要的話,你可以使用構(gòu)造函數(shù)將你檢索的哈希映射從 Bundle 中“變回”你之前使用的映射類型。
請記住,在這種情況下,在映射上的任何一個“extra”的屬性都將會丟失,并且只有鍵/值對會被保留下來。
作為一個 Android 開發(fā)人員,意味著你幾乎可以輕而易舉的用你的方式去完成任何事情,尤其是小的、微不足道的事情。
我們從中可以學到些什么?
當事情的發(fā)展不像我們所預期的那樣,
不要只盯著 JavaDoc 不動。
因為那樣只會浪費時間。
或者是因為 JavaDoc 的作者不了解有關(guān)于你的具體情況。
答案可能就在 AOSP 代碼中。
我們可以隨意的訪問AOSP代碼。這在動態(tài)領(lǐng)域中幾乎是獨一無二的。我們可以而且應(yīng)該知道這是為什么。
盡管有時候它看起來像 WTF-land,當你了解了你工作的內(nèi)部運行平臺,你就可以成為一名很好的開發(fā)人員了。
并且記?。簺]有打敗你的事情只會讓你變得更強,或者更瘋狂。
http://wiki.jikexueyuan.com/project/android-weekly/images/issue-145/0-40IPQ7jhknFGYKNR-.gif" alt="映射與包的神秘關(guān)系" />