SparseArray與ArrayMap源碼分析與總結

引言

  • SparseArray是在API level 1就已經添加的適用于Android的集合類,而ArrayMap實在API level 19才加入的集合類,雖說這兩者實在不同時期加入的,但是它們的目的只有一個,那就是在小數據量的情況下盡可能權衡內存占用以及使用效率,從而達到小數據量時能夠替換JDK中類似于HashMap之類的集合類模板。
  • 由于兩者源碼實際上存在很多相似之處,因此就放一起看了,源碼還是比較容易理解的。

SparseArray

  • SparseArray的數據構造可以從下面這幾行代碼得知,顯然是使用兩個數組分別存儲keyvalue的值,而這兩者又因元素索引存在對應關系而剛好形成元素間的相互映射,可以說是很簡單粗暴又有效了。

    public class SparseArray<E> implements Cloneable { 
        // ...
        private int[] mKeys;   
        private Object[] mValues;
        private int mSize;  
    } 
    
  • put方法:揭露核心的方法

    • 通過源碼可以看出,SparseArray使用的是二分查找法來查找key所在的位置索引,如果存在則替換掉對應索引上的value,如果不存在,則將在索引取反后的位置上添加元素,這點跟JDK中的Map是一樣的行為,存在則覆蓋。
    public void put(int key, E value) {
        // 二分查找算法 找到 key,也就是索引位置
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
        if (i >= 0) { // 存在
            mValues[i] = value; // value數組中找到key位置上的value
        } else { // 不存在
            i = ~i; 
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            } 
            if (mGarbage && mSize >= mKeys.length) {//可能value元素已經被刪除了
                gc();  // 那么chufa一次gc
                // 中心搜索一遍
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
    
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }
    
    • 這里的gc并非JVM中的那個GC,而是說當我們刪除了某個元素之后,被刪除元素所占用的那個位置上的數據就標記成了垃圾數據,然后就會通過gc來去除這個位置上的元素,而本質上,對于數組而言,就是挪動位置覆蓋掉這個位置咯。
    private void gc() { 
        int n = mSize;
        int o = 0;
        int[] keys = mKeys;
        Object[] values = mValues; 
        for (int i = 0; i < n; i++) {
            Object val = values[i]; 
            if (val != DELETED) { 
                if (i != o) {  // 相當于把key、value元素都向前挪動一次
                    keys[o] = keys[i];  
                    values[o] = val;
                    values[i] = null;
                } 
                o++;
            }
        } 
        mGarbage = false;
        mSize = o; 
    }
    
    • 上面代碼中的mGarbage會在刪除元素時被設置為true,也就是說標記這個位置上的元素為垃圾數據。
    public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }
    

SparseArray 綜述

  • 特點:使用int數組作為mapkey容器,Object數組作為value容器,使用索引對應的形式組成key-value這使得SparseArray可以不按照像數組索引那樣的順序來添加元素。可看成增強型的數組或者ArrayList

  • 查找:使用二分查找法查找key在數組中的位置,然后根據這個數組位置得到對應value數組中的value值。

  • 優劣:相對于HashMap,合理使用SparseArray可以節省大量創建Entry節點時產生的內存,不需要拆箱裝箱操作,提高性能,但是因為基于數組,插入和刪除操作需要挪動數組,已經使用了時間復雜度為O(logN)的二分查找算法,相對HashMap來說,非常消耗性能,當數據有幾百條時,性能會比HashMap低近50%,因此SparseArray適用于數據量很小的場景

  • 使用場景舉例

    • 通過View id來映射View對象實例。 一個非常現實的場景是,當我們使用RecyclerView時,我們可能需要創建若干個ViewHolder,特別是如果一個列表包含若干中布局類型的時候,而如果我們不適用類似于ButterKnifeDataBinding這類工具的話,那么我們需要對每個控件進行findViewById,這是件很糟心的事情,那么我們可以怎么簡化呢?欸,這種情況可以使用SparseArray來緩存一下我們的View(實際上ArrayMapHashMap等也可以),比方說以下示例代碼:
    public class CommViewHolder extends RecyclerView.ViewHolder {
    
        private SparseArray<View> mViewCache;
        private Object tag;
    
        public CommViewHolder(@NonNull View itemView) {
            super(itemView);
            mViewCache = new SparseArray<>();
        }
    
        public <T extends View> T getView(@IdRes int id) {
            T t = (T) mViewCache.get(id);
            if (t == null) {
                t = itemView.findViewById(id);
                mViewCache.put(id, t);
            }
            return t;
        }
    
        public void releaseCache(){
            mViewCache.clear();
            mViewCache = null;
        }
    
        public void setTag(Object tag) {
            this.tag = tag;
        }
    
        public Object getTag() {
            return tag;
        }
    }
    

    通過上面這個通用的ViewHolder,我們就可以應用于任意的布局類型,而不用每個都去寫一個對應的ViewHolder了,可以說很方便了。


ArrayMap

  • 通過閱讀ArrayMap的源碼可以發現,它和SparseArray簡直就是親生兄弟啊,不同點就是,ArrayMap具備完整的Map特性,因為實現了Map,并且具備哈希表的相關特性。
    public final class ArrayMap<K, V> implements Map<K, V> {
    
        final boolean mIdentityHashCode;
        int[] mHashes; // 存儲哈希值
        Object[] mArray; // 存儲元素
        int mSize;
        MapCollections<K, V> mCollections; 
    }
    
    
  • 我們先來看看查找索引index源碼,可以看到想要得到index,需要先使用二分查找法去mHashs數組中找出這個hash在數組中的位置,而這個位置就是index。這里就必然存在幾種情況,具體可以看下面的注釋。

    int indexOf(Object key, int hash) {
        final int N = mSize;
    
        // 使用二分查找算法搜索元素位置
        int index = binarySearchHashes(mHashes, N, hash);
        // 1. 不存在相關該hash 
        if (index < 0) {
            return index;
        }
        // 2. 存在該hash,且對應位置上有對應key
        if (key.equals(mArray[index<<1])) {
            return index;
        } 
        // 3. 存在該hash,但是對應位置上無對應key, 也就是說沖突了
        // 那么先搜索后半部分,之所以分成兩半來查找是為了縮小查詢范圍,提升搜索速度
        // 這實際實在賭博,賭目標值在數組后半段 ,最終能否提升速度就看是不是在數組后半段了
        int end;
        for (end = index + 1; end < N && mHashes[end] == hash; end++) {
            if (key.equals(mArray[end << 1])) return end;
        }
        // 再搜索前半部分
        for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
            if (key.equals(mArray[i << 1])) return i;
        } 
        // 對應key實在沒找到,說明確實是沖突了,那么返回個mHashes數組大小的取反值(負數)
        return ~end;
    }
    
  • 有了上面的分析過程,我們已經了解了ArrayMapindex是如何決定的了,那么通過put方法就可以比較直觀地看出ArrayMap的存儲過程了,首先會計算我們給定key的哈希值,然后通過這個哈希值去查找index,如果在這個index上已經元素,那么替換這個元素,如果不存在,那么將數據存入數組中;如果存在沖突,則校檢一下數組容量(看看需不需要擴容),然后存入數組

    public V put(K key, V value) {
        final int osize = mSize;
        final int hash;
        int index;
        // 1. 查找hash,計算index
        if (key == null) {
            hash = 0;
            index = indexOfNull();
        } else {
            hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
            index = indexOf(key, hash);
        }
        // 2. 根據index存值
        // 存在,則覆蓋掉舊元素,并返回舊元素
        if (index >= 0) {
            index = (index<<1) + 1;
            final V old = (V)mArray[index];
            mArray[index] = value;
            return old;
        }
        
        // 有沖突的情況 
        index = ~index; 
        // 數組可用空間不夠,那么擴容
        if (osize >= mHashes.length) { 
            final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                    : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); 
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(n); // 擴容
            // ...  
            if (mHashes.length > 0) { // 轉移元素到新數組
                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
            } 
            freeArrays(ohashes, oarray, osize); // 清理就數組
        }
    
        // 3. 存值
        if (index < osize) { 
            System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
            System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
        }  
        mHashes[index] = hash; 
        // key和value存在同一個數組上
        mArray[index<<1] = key;  
        mArray[(index<<1)+1] = value;
        mSize++;
        return null;
    }
    
    

    上面有個比較新穎的地方,就是它把keyvalue都存到了一個數組中去了,也就是mArray數組,key在前,value在后,同 index的關系如下圖:

mArray數組的存儲結構

ArrayMap 綜述

  • 特點

    • 實現了Map接口,并使用int[]數來存儲keyhash值,數組的索引用作index,而使用Object[]數組來存儲key<->value ,這還是比較新穎的 。
    • 使用二分查找查找hash值在key數組中的位置,然后根據這個位置得到value數組中對應位置的元素。
    • SparseArray類似,當數據有幾百條時,性能會比HashMap低50%,因此ArrayMap適用于數據量很小的場景
  • ArrayMap和HashMap的區別?

    • ArrayMap的存在是為了解決HashMap占用內存大的問題,它內部使用了一個int數組用來存儲元素的hashcode,使用了一個Object數組用來存儲元素,兩者根據索引對應形成key-value結構,這樣就不用像HashMap那樣需要額外的創建Entry對象來存儲,減少了內存占用。但是在數據量比較大時,ArrayMap的性能就會遠低于HashMap,因為 ArrayMap基于二分查找算法來查找元素的,并且數組的插入操作如果不是末尾的話需要挪動數組元素,效率較低。
    • 而HashMap內部基于數組+單向鏈表+紅黑樹實現,也是key-value結構, 正如剛才提到的,HashMap每put一個元素都需要創建一個Entry來存放元素,導致它的內存占用會比較大,但是在大數據量的時候,因為HashMap中當出現沖突時,沖突的數據量大于8,就會從單向鏈表轉換成紅黑樹,而紅黑樹的插入、刪除、查找的時間復雜度為O(logn),相對于ArrayMap的數組而言在插入和刪除操作上要快不少,所以數據量上百的情況下,使用HashMap會有更高的效率。
  • 如何解決沖突問題?ArrayMap中,假設存在沖突的話,并不會像HashMap那樣使用單向鏈表或紅黑樹來保留這些沖突的元素,而是全部keyvalue都存儲到一個數組當中,然后查找的話通過二分查找進行,這也就是當數據量大時不宜用ArrayMap的原因了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380