鍍金池/ 教程/ Android/ 映射與包的神秘關(guān)系
原文鏈接
Issue #185
Issue #181
Issue #161
Issue #192
Issue #174
Issue #190
RecyclerView FastScroll – Part 2
僅作為Android 調(diào)試模式工具的Stetho
Issue #150
Issue #167
Issue #180
Issue #151
Issue #188
Issue #159
Issue #189
Issue #160
Issue #168
Issue #146
Issue #173
Issue #198
Issue #179
延期的共享元素轉(zhuǎn)換(3b)
Yahnac:RxJava Firebase&內(nèi)容提供
Issue #162
游戲性能:規(guī)劃限定條件
分析清單:測量和尋找哪些方面
Issue #148
Issue #166
Issue #158
Issue #178
Issue #193
Issue #145
Issue #170
Issue #169
Issue #196
Issue #186
Issue #172
Issue #171
附加Android工件和Gradle的檔案
Issue #147
自定義顏色范圍
根據(jù) Material 設(shè)計導航制圖工具樣式
Issue #187
Issue #184
Issue #175
在Android Lollipop上使用JobScheduler API
Android性能案例追蹤研究
使用安卓Wear API創(chuàng)建watchface—第2部分
在谷歌市場上創(chuàng)造更好的用戶體驗
映射與包的神秘關(guān)系
Issue #165
用Robolectric進行參數(shù)化測試
Issue #155
Issue #149
MVC / MVP中的M -模型
歡迎為 Android 和 iOS 嵌入 API
Issue #164
Android UI 自動化測試
Issue #182
Issue #191
Issue #183
Issue #163
Issue #157
響應(yīng)式編程(Reactive Programming)介紹
Issue #197
原文鏈接
Issue #153
Issue #152
Issue #176
原文地址
Android Material 支持庫:Electric Boogaloo的提示與技巧
Issue #156
Issue #154
Android的模糊視圖
Issue #194
Issue #177
Issue #195
針對Jenkins的谷歌商店安卓出版插件

映射與包的神秘關(guān)系

[這篇文章是在歐金尼奧@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() 明確需要使用映射。

步驟1:找到所述的第一個薄弱環(huán)節(jié)

讓我們來看看在 Intent.putExtra(String,Serializable) 會發(fā)生什么:

Intent.java

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

在這里,mExtras顯然是一個。那么好吧,意圖代表將所有的額外都打到一個包,就如同我們所預期的,并調(diào)用 Bundle#putSerializable()。讓我們看看這個方法:

Bundle.java

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

事實證明,這恰恰僅代表了超級實現(xiàn),那就是:

BaseBundle.java

    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)系" />

步驟2:編寫映射

當我們真正開始寫的內(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é)問題:

BaseBundle.java

    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í)行的

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

基本上做的就是把在 BaseBundle 的映射里寫入的每一個鍵-值對根據(jù)值的大小形成連續(xù)的字符串(這里的值是字符串)。到目前為止后者似乎沒有考慮到值的類型。

讓我們更深一層!

http://wiki.jikexueyuan.com/project/android-weekly/images/issue-145/1-TOBlt2WOVLElnQTyG-R7YQ.gif" alt="映射與包的神秘關(guān)系" />

步驟3:編寫映射值

那么,你問,Parcel#writeValue() 看起來怎么樣?在這里,在它的 if-elseif-else 體現(xiàn):

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 (/* 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)系" />

步驟4:將映射寫到包中

那么,事實上,就其本身而言,除了后續(xù)我們將強化映射的類型,writeMap() 自身做的事情并不多

Parcel.java

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

該方法的 JavaDoc 是很清楚的:

“映射的關(guān)鍵字必須是字符串對象?!?

類型擦除確保我們在這里不會有運行時間的錯誤,即使我們可能在傳遞一個關(guān)鍵字不是字符串類型的映射(再一次,這完全是較高水平的非法行為…)。

事實上,只要我們看一下 writeMapIntent(),我們就會想到:

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

再次,在這里類型擦除讓那些計算在運行時變得一文不值。事實是我們將之前對鍵和值進行類型檢查的 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)完全失去他們的類型了,所以當他們進行回讀的時候沒有辦法對信息進行恢復。

步驟5:對映射進行回讀

作為最后一步,快速檢查我們的理論,讓我們?nèi)z查一下 readValue(),它是與 writeValue()相對應(yīng)的:

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

當寫入數(shù)據(jù)時包工作的方式,每一項的內(nèi)容如下:

  1. 定義了數(shù)據(jù)類型 int(VAL_*常量之一)

  2. 數(shù)據(jù)本身進行轉(zhuǎn)儲(可以包括其他元數(shù)據(jù)如非固定大小的數(shù)據(jù)類型,例如字符串長度)

  3. 對數(shù)據(jù)類型進行遞歸嵌套(非原始)

在這里,我們看到 readValue() 讀取數(shù)據(jù)的類型為 int,這使得我們的 TreeMap 通過 writeValue() 被設(shè)置為 VAL_MAP,然后根據(jù)相應(yīng)的選擇情況來調(diào)用 readHashMap() 來檢索數(shù)據(jù)本身:

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

(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è)計決策中派生)。

解決方法(又名 tl;dr)

好吧,我們要深一層的了解我們的問題,然而現(xiàn)在我們已經(jīng)確定了被我們打亂了的關(guān)鍵路徑。我們需要確保我們的 TreeMap 不被抓到由 writeValue() 映射檢查的實例 V 中。

當談?wù)摰?Eugenio 時我的大腦中想到的第一個解決方案是很單一的但卻很有效:將映射包裝到一個接口容器中。Eugenio 迅速投入到了這個普通的包裝中并確認它解決了這個問題。

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

請注意,你在 gist 上找到的完整代碼是使用 Android 的 @NonNull 注釋強制執(zhí)行的。如果你想單純的在 Java 中使用這些代碼,你可以用 JetBrain 的 @NonNull 取代它,或者你也可以選擇脫離這些注釋。

另一個可能的解決方案

另一個解決方法是把它作為一個 Intent extra 之前,自己先把映射提前序列化成一個字節(jié)數(shù)組,然后用 getByteArrayExtra() 對它進行檢索,但你必須手動處理序列化和反序列化。

如果你不怕麻煩想選擇其他的解決方案來代替,Eugenio 已經(jīng)為你提供了單獨 Gist 的代碼。

當你無法控制不斷增多的 Intent 時

最后,也許出于某種原因,你無法控制 Bundle 創(chuàng)建的代碼——例如,因為它在某些第三方庫中。

在這種情況下,請記住,許多映射的實現(xiàn)需要有一個構(gòu)造函數(shù),該構(gòu)造函數(shù)是以映射作為輸入的,比如新建一個 TreeMap(Map)。如果有需要的話,你可以使用構(gòu)造函數(shù)將你檢索的哈希映射從 Bundle 中“變回”你之前使用的映射類型。

請記住,在這種情況下,在映射上的任何一個“extra”的屬性都將會丟失,并且只有鍵/值對會被保留下來。

結(jié)論

作為一個 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)系" />

上一篇:Issue #168下一篇:Issue #149