1. 程式人生 > 實用技巧 >Android集合之SparseArray、ArrayMap詳解

Android集合之SparseArray、ArrayMap詳解

作為一個Anndroid開發人員來說,我們大多數情況下時使用的Java語言,自然在一些資料的處理時,使用到的集合框架也是Java的,比如HashMapHashSet等,但是你可否知道,Android因為自身特殊的需求,也為自己量身定製了“專屬”的集合類,查閱官方文件,android.util包下,一共捕獲如下幾個類:SparseArray系列(SparseArraySparseBooleanArraySparseIntArraySparseLongArrayLongSparseArray),以及ArrayMapArraySet,我相信即便沒學過,看到這些類名,基本也能猜到一些它們的區別和用法了,下面我們就來好好學一學它們,開始吧!

目錄

1.使用方法
2.感受設計之美
3.優缺點及應用場景

正文

使用方法

按照我的習慣,我覺得不管學什麼,首先需要的就是會用“它”,感受一下它的用法,其次才能再談理論上的東西,下面我們先來學一學怎麼使用。
首先我們看一下SparseArray的使用方法

        //宣告
        SparseArray<String> sparseArray= new SparseArray<>();
        //增加元素,append方式
        sparseArray.append(0, "myValue");
        //增加元素,put方式
        sparseArray.put(1, "myValue");
        //刪除元素,二者等同
        sparseArray.remove(1);
        sparseArray.delete(1);
        //修改元素,put或者append相同的key值即可
        sparseArray.put(1,"newValue");
        sparseArray.append(1,"newValue");
        //查詢,遍歷方式1
        for(int i=0;i<sparseArray.size();i++){
            Log.d(TAG,sparseArray.valueAt(i));
        }
        //查詢,遍歷方式2
        for(int i=0;i<sparseArray.size();i++){
            int key = sparseArray.keyAt(i);
            Log.d(TAG,sparseArray.get(key));
        }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

OK,很正常的使用方法,和hashmap等資料結構基本一樣。
唯一不同的就是key和value的型別,hashmap的key值和value值為泛型,但是SparseArray的key值只能為int 型別,value值為Object型別,看到這,你可能會覺得很奇怪,這不是在使用上受到了很大的約束嘛,這樣約束的意義何在呢?

先別急,我們看看剩下的SparseArray的雙胞胎兄弟姐妹們,LongSparseArraySparseArray相比,唯一的不同就是key值為long,所以,既然為long ,那麼相對SparseArray

來說,它可以儲存的資料元素就比SparseArray多。

順帶溫習一下,int的範圍是-2^31 到 231-1,而long是-263 到 2^63-1

然後輪到了SparseBooleanArraySparseIntArraySparseLongArray,這三兄弟相對SparseArray來說就是value值是確定的,SparseBooleanArray的value固定為boolean型別,SparseIntArray的value固定為int型別,SparseLongArray的value固定為long型別。

注意這裡的value中的值型別boolean、int、long都是小寫的,意味著是基本型別,而不是封裝型別

稍作總結一下,如下

SparseArray          <int, Object>
LongSparseArray      <long, Object>
SparseBooleanArray   <int, boolean>
SparseIntArray       <int, int>
SparseLongArray      <int, long>
  • 1
  • 2
  • 3
  • 4
  • 5

ok,然後我們再看看ArrayMapArraySet的使用

        ArrayMap<String,String> map=new ArrayMap<>();
        //增加
        map.put("xixi","haha");
        //刪除
        map.remove("xixi");
        //修改,put相同的key值即可
        map.put("xixi2","haha");
        map.put("xixi2","haha2");
        //查詢,通過key來遍歷
        for(String key:map.keySet()){
            Log.d(TAG,map.get(key));
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

OK,很正常的用法,和HashMap無異。ArraySet就不用我繼續說了吧,它們的關係就像HashMapHashSet一樣,它和HashSet都是不能儲存相同的元素。

額外說明一下,ArraySet使用要求sdk最小版本為23,也就是minSdkVersion值必須大於等於23

感受設計之美

由於SparseArray的三兄弟原理上和SparseArray一樣,所以我們先來看SparseArray的設計思想
首先,我們來到SparseArray的原始碼,其中定義瞭如下一些成員

    private static final Object DELETED = new Object();
    private boolean mGarbage = false;
	//需要說明一下,這裡的mKeys陣列是按照key值遞增儲存的,也就是升序,這個在查詢會講到為什麼要保證升序
    private int[] mKeys;
    private Object[] mValues;
    private int mSize;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

比較簡單,一共五個欄位,DELETED是一個標誌欄位,用於判斷是否刪除(這個後面分析到了,自然會就明白了),mGarbage也是一個標誌欄位,用於確定當前是否需要垃圾回收,熟悉的mKeys陣列用於儲存key,mValues陣列用於儲存值,最後一個表示當前SparseArray有幾個元素,好了,接下來看最重要的增加方法,先看append方法

    public void append(int key, E value) {
        if (mSize != 0 && key <= mKeys[mSize - 1]) {
	        //當mSize不為0並且不大於mKeys陣列中的最大值時,因為mKeys是一個升序陣列,最大值即為mKeys[mSize-1]
	        //直接執行put方法,否則繼續向下執行
            put(key, value);
            return;
        }
		//當垃圾回收標誌mGarbage為true並且當前元素已經佔滿整個陣列,執行gc進行空間壓縮
        if (mGarbage && mSize >= mKeys.length) {
            gc();
        }
		//當陣列為空,或者key值大於當前mKeys陣列最大值的時候,在陣列最後一個位置插入元素。
        mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
        mValues = GrowingArrayUtils.append(mValues, mSize, value);
        //元素加一
        mSize++;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

append方法主要處理兩種情況:一:當前陣列為空,二:新增一個key值大於當前所有key值最大值的時候。這兩種情況都有一個共同的特點就是隻需要在陣列末尾直接插入就好了,不需要去關心插入在哪裡,相當於處理兩種簡單的極端情形,所以我們在使用SparseArray的時候,也要有意識的將這兩種情形下的元素新增,使用append來新增,提高效率。
其實,顧名思義,append,追加的意思嘛,看來取名字還都是有講究的,不是亂取的,嘿嘿。
接下來看put方法

    public void put(int key, E value) {
	    //二分查詢,這裡有個巨經典的處理,你相信我,要是不經典,我把好吃的給你。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
		//查詢到
        if (i >= 0) {
            mValues[i] = value;
        } else {//沒有查詢到
            i = ~i;//獲得二分查詢結束後,lo的值
			//元素要新增的位置正好==DELETED,直接覆蓋它的值即可。
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
			//垃圾回收,但是空間壓縮後,mValues陣列和mKeys陣列元素有變化,需要重新計算插入的位置
            if (mGarbage && mSize >= mKeys.length) {
                gc();

                //重新計算插入的位置
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
			//在指定位置i出=處,插入元素
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }

  • 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

首先,看上去似乎是一個很普通的方法,沒有任何異樣,但是仔細思考,其實暗藏玄機,我們看到作者先執行了一個二分查詢,好,我們來到這個二分查詢,如下

    static int binarySearch(int[] array, int size, int value) {
        int lo = 0;
        int hi = size - 1;

        while (lo <= hi) {
            final int mid = (lo + hi) >>> 1;
            final int midVal = array[mid];

            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;  // 找到了
            }
        }
        return ~lo;  // 沒找到
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

很熟悉,對吧,基本上都會寫,但是請你注意,注意了,人家在沒找到的時候,返回了一個值,這個有什麼用,換做我們的話,一般怎麼處理,我想很多人和我一樣,返回一個-1,表示查詢失敗不就可以了,是的,我也覺得這樣寫沒毛病。
現在我們假設自己是作者,我們來想下,他為什麼返回一個lo的取反,這沒有道理嘛,有什麼用呢?

一時想不到,沒關係,我們再回頭看看put方法,第一步拿到二分查詢的結果 i 之後,判斷 i 大於0,也就是查詢到了,正常向下執行,然後else,也就是 i 值為負,就是沒查詢到,因為我們在二分查詢裡返回的是lo的取反,即便最後沒查詢到,lo也是個正數,正數取反為負數,達到了效果,這是妙用之一,這時你可能會想,我為啥不直接返回個-1,不也達到了效果嗎?好,返回-1確實達到了效果,但是人家的返回值在完成了用於判斷是否查詢成功這個使命之後,還有第二個使命,首先負數取反後,即可再次得到二分查詢結束時lo的值,這個lo的值,我現在告訴你,這個位置是不是有點特殊,那特殊在哪呢,沒錯,這個值就是新增元素的插入位置,接下來的你:

(先懵一會 -->> 仔細思考一下 -->> 拿個筆畫一畫 -->> 哎喲嘿,好像還真是這麼回事 -->> 恍然大悟 -->> 發出感嘆:妙啊)

好了,你的流程走完了,這時你應該懂了這個lo值處理的巧妙之處,不懂的就讓我再囉嗦一會

假設我們有個陣列 3 4 6 7 8。用二分查詢來查詢元素5
初始:lo=0 hi=4
第一次迴圈:mid=(lo+hi)/2=2 2位置對應6 6>5 查詢失敗,下一輪迴圈 lo=0 hi=1
第二次迴圈:mid=(lo+hi)/2=0 0位置對應3 3<5 查詢失敗,下一輪迴圈 lo=2 hi=1
lo>hi 迴圈終止
最終 lo=2 即5需要插入的下標位置
神奇不~

所以返回 ~lo的2個作用:一,用於判斷是否查詢成功;二,用於記錄待新增元素的插入位置 (這操作真的完美!!)
好了,我們回到put方法,在拿到待插入元素應該插入的位置之後,我們就可以做出一系列操作了,但是你可能也注意到了一個地方,拿到插入的位置之後,它首先判斷需要插入的位置對應mValues陣列的值是不是為DELETED,如果是的話,直接覆蓋,至於為什麼這樣做,這個也就是下面我們要看的了,如果累了,可以喝口茶,接著再看,如下,delete方法原始碼

    public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

SparseArray也有remove方法,不過remove方法是直接呼叫的delete方法,所以二者是一樣的效果,remove相當於是一個別名

看到這個delete方法,先讓我感嘆一下,真簡單吶,清爽,直接,乾脆。但是問題來了,就這幾個操作就實現了刪除?,逗我呢,這明明就沒有刪除嘛,okok,不急,我們還是耐心看下它到底做了啥,首先二分查詢獲取刪除key的下標,然後如果成功查詢,也就是 i>0 時,判斷如果對應key值的value如果不等於DELETED,那麼將值置為DELETED,然後設定mGarbage為true,也就是垃圾回收的標誌在這裡被設定為了true,ok,然後呢,還是一臉懵啊,這也沒有刪除元素啊,只是做了個賦值操作,好,既然它核心就兩步賦值操作,我們不難想到,之前一直見到的一個叫gc()的方法,我們來到這個方法

    private void gc() {
	    //奇怪的作者沒有刪掉註釋,程式碼強迫症的我好想給他把這句日誌的註釋刪掉,但是沒有許可權,嚶嚶嚶!
        // Log.e("SparseArray", "gc start with " + mSize);

        int n = mSize;//壓縮前陣列的容量
        int o = 0;//壓縮後陣列的容量,初始為0
        int[] keys = mKeys;//儲存新的key值的陣列
        Object[] values = mValues;//儲存新的value值的陣列

        for (int i = 0; i < n; i++) {
            Object val = values[i];

            if (val != DELETED) {//如果該value值不為DELETED,也就是沒有被打上“刪除”的標籤
                if (i != o) {//如果前面已經有元素打上“刪除”的標籤,那麼 i 才會不等於 o
	                //將 i 位置的元素向前移動到 o 處,這樣做最終會讓所有的非DELETED元素連續緊挨在陣列前面
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;//釋放空間
                }

                o++;//新陣列元素加一
            }
        }
		//回收完畢,置為false
        mGarbage = false;
        //回收之後陣列的大小
        mSize = o;
		//哼!,這裡的註釋也沒刪
        // Log.e("SparseArray", "gc end with " + mSize);
    }
  • 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

核心的壓縮方法思想也很簡單,主要就是通過之前設定的DELETED標籤來判斷是否需要刪除,然後進行陣列前移操作,將不需要刪除的元素排在一起,最後設定新的陣列大小和設定mGarbage為false。

這裡我當初咋一看的時候,有個問題沒想明白,就是對陣列進行了賦值操作,更改的也是方法裡宣告的keysvalues陣列,但是我沒有更改mKeys陣列和mValues陣列嘛,這個其實是基礎知識,陣列在賦值的時候,是傳遞的引用,對陣列來說,就是地址,就是兩個陣列指向了記憶體中同一塊地址,修改任意一個,都會影響另外一個,不信的話,你可以試試下面的例子,看看執行結果
public static void main(String[] args) {
int[] a=new int[] {5,4,8};
int[] b=a;
b[1]=10;
System.out.println(a[1]);
}

到這為止,我們明白了SparseArray的刪除元素的機理,概括說來,就是刪除元素的時候,咱們先不刪除,通過value賦值的方式,給它貼一個標籤,然後我們再gc的時候再根據這個標誌進行壓縮和空間釋放,那麼這樣做的意圖是什麼呢?我為啥不直接在delete方法裡直接刪除掉,多幹淨爽快?繞那麼大圈子,反正不是刪除?
別急,我們回過頭來看put方法

			//元素要新增的位置正好==DELETED,直接覆蓋它的值即可。
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

還記得這一段嗎,在新增元素的時候,我們發現如果對應的元素正好被標記了“刪除”,那麼我們直接覆蓋它即可,有沒有一種頓悟的感覺!也就是說,作者這樣做,和我們每次delete都刪除元素相比,可以直接省去刪除元素的操作,要知道這在效率上是一個很可觀的提高,但是並不是每次put元素都是這樣的情況,因為還有gc()方法來回收,那麼我們再仔細想想,和每次delete元素相比,設定“刪除”標誌位,然後空間不足的時候,呼叫gc方法來一次性壓縮空間,是不是效率上又有了一個提高。

僅僅只是一個刪除元素的方法,作者的處理就使用了足足兩個小技巧來提升效率,達到最優:一,刪除設定“標誌位”,來延遲刪除,實現資料位的複用,二,在空間不足時,使用gc()函式來一次性壓縮空間。從中可見作者的良苦用心,每一個設計都可以堪稱是精髓!。
我們再總結一下SparseArray中的優秀設計

  • 延遲刪除機制(當仁不讓的排第一)
  • 二分查詢的返回值處理
  • 利用gc函式一次性壓縮空間,提高效率

好了,到這為止,相信你對SparseArray的基本工作原理有了一個比較清晰的認識!

中場休息:喝口快樂水,活動一下,稍後再來

我們接著來看ArrayMap

在開始接下來的內容前,希望大家能對hashmap的設計原理有一個比較深入的瞭解,因為學習的過程中,單一的學習某個東西,可能沒有感受,即便是人家的優秀設計,也體會不到巧妙之處,但是一旦有了一個對比,學習起來就會有一種大局觀,這對學習是非常有利的。

簡單補充下hashmap的實現原理,採用陣列+連結串列的結構來實現,新增元素時,首先計算key的hash值,然後根據這個hash值,定位到下標,如果衝突的話,則在該下標節點處鏈上一個連結串列,以頭插法新增新元素為連結串列頭結點,如果連結串列長度超過指定長度,則轉換為紅黑樹。
hashmap解決衝突的辦法叫做鏈地址法,或者叫拉鍊法。

先來看看ArrayMap裡面重要的成員變數

	//是否置hashcode值為唯一,也就是固定值
    final boolean mIdentityHashCode;
    int[] mHashes;//儲存key的hash值
    Object[] mArray;//儲存key值和value值
    int mSize;//集合大小
    //ArrayMap物件轉換為MapCollections
    MapCollections<K, V> mCollections;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

當然我們最需要關心的就是mHashes[]mArray[]這兩個陣列.
接下來,看到增加元素的方法,append方法,如下

    public void append(K key, V value) {
        int index = mSize;
        //獲取key的hash值
        final int hash = key == null ? 0
                : (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
        if (index >= mHashes.length) {
            throw new IllegalStateException("Array is full");
        }
        //當前陣列不為空,hash值小於 mHashes[]陣列最大的元素時(mHashes陣列為遞增有序陣列)
        if (index > 0 && mHashes[index-1] > hash) {
            RuntimeException e = new RuntimeException("here");
            e.fillInStackTrace();
            Log.w(TAG, "New hash " + hash
                    + " is before end of array hash " + mHashes[index-1]
                    + " at index " + index + " key " + key, e);
            put(key, value);//交給put方法處理
            return;
        }
        //當前陣列為空,或者hash值大於 mHashes[]陣列最大的元素時
        mSize = index+1;//陣列元素數量+1
        mHashes[index] = hash;//在mHashes陣列index下標處放入key的hash值
        index <<= 1;//相當於乘2操作,為什麼要用移位操作呢,因為移位操作效率高
        mArray[index] = key;//在mArray陣列index*2下標處放入key值
        mArray[index+1] = value;//在mArray陣列index*2+1下標處放入value值
    }

  • 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

具體的分析,程式碼中已經給的比較明白了,通過這個append方法,我們可以看到ArrayMap裡的兩個核心陣列mHashes[]mArray[]是如何儲存資料的,即,mHashes按照升序(這裡看不出來升序,下面的查詢會分析到)儲存所有的key值計算出來的hash值,然後對於指定的key值計算出來的hash值儲存的位置index,對應到mArray陣列中,key就是index*2value就是index*2+1分析到這裡,其實查詢的方法我們也知道了,只需要計算keyhash值,得到index後,對應的keyvaluemArray陣列中查詢即可。
對應的儲存結構用一張圖來表示如下

接下來我們再順著看put方法,但是在看put方法之前,有沒有一種似曾相識的感覺,有沒有覺得append的這個邏輯套路和思想與SparseArray中的append方法的思想很像,二者都是類似的邏輯,先獲取key的hash值,然後和儲存hash值的陣列作對比,如果小於最大值,則交由put處理,其它情況陣列為空以及大於最大值,則append自己直接在陣列末端新增即可,後續的操作也是一模一樣,只不過根據不同的場景使用了不同的方法而已,可見很多東西是有共性的,如果我們自己需要設計這樣某個容器的新增方法時,也可以採納這種思想。

通過上面的對比分析,我們多多少少能猜到這裡的一些邏輯,我們現在來看看put方法

    @Override
    public V put(K key, V value) {
        final int hash;
        int index;
        if (key == null) {//key為空時,取hash值為定值0
            hash = 0;
            index = indexOfNull();
        } else {
	        //根據mIdentityHashCode判斷是否使用固定的hash值
            hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
            index = indexOf(key, hash);//通過hash值計算下標值,最終也是使用的二分查詢
        }
        if (index >= 0) {//如果找到了,說明之前已經put過這個key值了,這時直接覆蓋對應value值
            //mHashes陣列中的index值,對應的value值在mArray中index*2+1處
            index = (index<<1) + 1;
            final V old = (V)mArray[index];//記錄舊值
            mArray[index] = value;//覆蓋舊值,增加新的value值
            return old;
        }
		//如果index<0,也就是沒有根據key的hash值在mhashes陣列中找到對應的下標值
        index = ~index;//哇,經典復現!!!(具體SparseArray裡才講過)獲取key的hash值要插入的位置
        if (mSize >= mHashes.length) {//如果陣列容量已滿
	        //獲取擴容的大小,這個就是一個稍顯複雜的三目運算子,應該--問題不大!就不贅述了,嘿嘿
            final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
                    : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

            if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
			//接下來這三步,進行了allocArrays一個操作,我們暫且不管,放一放,待會再來收拾它
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(n);
			//將舊的陣列賦值給進行allocArrays操作之後的陣列
            if (mHashes.length > 0) {
                if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
            }
			//進行一個叫freeArrays的操作,我們和allocArrays一樣,待會再來收拾它
            freeArrays(ohashes, oarray, mSize);
        }
		//如果待插入的位置小於mSize,則需要將mHashes陣列index的位置空出來,相應的後面元素後移
		//同時mArray陣列中index*2和index*2+1這兩個位置也要空出來,相應的後面的元素後移
        if (index < mSize) {
            if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
                    + " to " + (index+1));
            System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
            System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
        }
		//呼!終於可以進行插入操作了
        mHashes[index] = hash;
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;
        mSize++;
        return null;
    }

  • 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
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

中間有一些沒見過的函式,沒事,我們先讓它囂張一會會,待會再來收拾,整個函式大體流程為:通過key計算hash值,再使用得到的hash值二分查詢(indexOf)在mhashes陣列中的index下標值,我們追蹤到indexfor方法裡面,最終會發現它也是呼叫的ContainerHelpers.binarySearch方法,熟悉不,沒錯,就是我們SparseArray中使用的二分查詢的方法,通過這個方法,我們在沒有查詢到元素時,只需要將返回值取反即可獲取待插入元素需要插入的位置,然後接下來陣列容量滿的時候進行的一大串操作先不管,然後會執行陣列移動的工作,為相應的元素插入騰出空間,最後插入,結束。

通過整個插入方法的流程,我們知道了ArrayMap裡面資料的儲存結構,以及其中的關係,我們接著往下。

接下來我們就來收拾剛才遇見的兩個神祕大魔頭函式,但是在收拾之前,我們先來看看ArrayMap裡與之相關的另外一些成員變數,它們的定義如下

	//多次出現的BASE_SIZE ,固定值為4,至於為什麼是4,分析完了,就知道了
    private static final int BASE_SIZE = 4;
    //快取陣列數量的最大值,也就是說最多隻能快取10個數組
    private static final int CACHE_SIZE = 10;
    
	//原始碼中對這幾個陣列作了英文註釋說明,這裡簡要介紹下
    //這四個成員變數完成的功能就是:快取小陣列以避免頻繁的用new建立新的陣列,避免消耗記憶體
    static Object[] mBaseCache;//用來快取容量為BASE_SIZE的mHashes陣列和mArray陣列
    static int mBaseCacheSize;//代表mBaseCache快取的陣列數量,控制快取數量
    static Object[] mTwiceBaseCache;//用來快取容量為BASE_SIZE*2的mHashes陣列和mArray陣列
    static int mTwiceBaseCacheSize;//代表mTwiceBaseCache快取的陣列數量,控制快取數量

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

mBaseCache陣列和mTwiceBaseCache陣列實現的功能基本一樣,只不過mBaseCache是用來快取容器儲存的元素數量為BASE_SIZE的陣列,mTwiceBaseCache是用來快取數量為BASE_SIZE*2的陣列,它們都是一個指向陣列物件的連結串列指標,每個陣列物件中,陣列的第一個元素指向下一個陣列,第二個元素指向對應的hash值陣列,餘下為空。

瞭解上面這些相關的成員變數之後,我們首先收拾freeArrays方法,他的程式碼如下

	private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
		//引數hashes陣列對應mHashes陣列   引數array陣列對應mArray陣列  size代表容器元素數量
        if (hashes.length == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {//防止多執行緒不同步
	            //如果沒有達到快取數量上限
                if (mTwiceBaseCacheSize < CACHE_SIZE) {
                    array[0] = mTwiceBaseCache;//將array的第一個元素指向快取陣列
                    array[1] = hashes;//將array的第二個元素指向hashes陣列
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;//將從下標2起始的位置,全部置空,釋放空間
                    }
                    //將快取陣列指向設定完畢的array陣列
                    //也就是將array陣列新增到快取陣列的連結串列頭
                    mTwiceBaseCache = array;
                    mTwiceBaseCacheSize++;//快取完畢,快取陣列的數量加一
                    if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
                            + " now have " + mTwiceBaseCacheSize + " entries");
                }
            }
        } else if (hashes.length == BASE_SIZE) {//完全一樣的邏輯,同上
            synchronized (ArrayMap.class) {
                if (mBaseCacheSize < CACHE_SIZE) {
                    array[0] = mBaseCache;
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mBaseCache = array;
                    mBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
                            + " now have " + mBaseCacheSize + " entries");
                }
            }
        }
    }

  • 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
  • 32
  • 33
  • 34
  • 35
  • 36

經過程式碼中的註釋分析,我相信大家心中有數了,但是可能還是有點懵,我們再稍微分析一下,整體結構就是分為兩種情況,一種是hashes陣列長度為BASE_SIZE,一種是hashes陣列長度為BASE_SIZE*2就兩種情況,而且這兩種情況的邏輯一模一樣,只不過換了些成員變數而已,對於大小為其它的情況,這個函式不作任何處理,所以我們這裡就挑BASE_SIZE*2這種情況來分析,經過程式碼中逐行的解釋,我們發現作者就是在構建一個單向連結串列,連結串列中的每個節點(這個節點就是處理過後的mArray陣列)有兩個物件(分別為下標0的值和下標1的值),一個指向下一個節點(相當於next指標),剩下一個就是指向存放hashes值的陣列,也就是構建了一個存放mHashes陣列的連結串列。
這個連結串列的結構如下:

那作者這樣做圖個啥呢,用一個連結串列把存放hashes值的陣列串起來幹嘛,有啥用呢,好的,我們帶著疑問再來收拾另外一個allocArrays方法(其實已經說了是快取,就先假裝不知道嘛),如下

    private void allocArrays(final int size) {
	    //mHashes陣列容量為0,直接丟擲異常
	    //EMPTY_IMMUTABLE_INTS這個值是mHashes陣列的初始值,是一個大小為0的int陣列
	    //直接寫mHashes.length==0不好嗎,真是一個奇怪的作者,莫非暗藏玄機?暫且留作疑問
        if (mHashes == EMPTY_IMMUTABLE_INTS) {
            throw new UnsupportedOperationException("ArrayMap is immutable");
        }
        //如果大小為BASE_SIZE*2=8,這時快取使用mTwiceBaseCache陣列來快取
        if (size == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {//防止多執行緒操作帶來的不同步
                if (mTwiceBaseCache != null) {
                    final Object[] array = mTwiceBaseCache;
                    //將mArray指向mTwiceBaseCache(相當於快取連結串列的頭指標)
                    //初始化mArray的大小(其實裡面0號位置和1號位置也有資料,只不過沒有意義)
                    mArray = array;
                    //將mTwiceBaseCache的指標指向頭節點陣列的0號元素,也就是指向第二個快取陣列
                    mTwiceBaseCache = (Object[])array[0];
                    //獲取頭節點陣列array的1號元素指向的hash值陣列,並賦給mHashes陣列
                    mHashes = (int[])array[1];
                    //將mTwiceBaseCache快取連結串列的頭節點0號元素和1號元素置空,釋放
                    array[0] = array[1] = null;
                    //快取陣列的數量減一
                    mTwiceBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                            + " now have " + mTwiceBaseCacheSize + " entries");
                    return;//結束
                }
            }
        } else if (size == BASE_SIZE) {//使用mBaseCache陣列來快取,同上
            synchronized (ArrayMap.class) {
                if (mBaseCache != null) {
                    final Object[] array = mBaseCache;
                    mArray = array;
                    mBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                            + " now have " + mBaseCacheSize + " entries");
                    return;//結束
                }
            }
        }
		//如果size既不等於BASE_SIZE,也不等於BASE_SIZE*2
		//那麼就new新的陣列來初始化mHashes和mArray,裡面資料均為空,相當於沒有使用快取
        mHashes = new int[size];
        mArray = new Object[size<<1];
    }

  • 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
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

出奇的相似有木有,都是分為兩種情況直接處理,一種BASE_SIZE*2,一種BASE_SIZE,而且從程式碼中可以看到這兩者處理的邏輯一模一樣,ok,好辦了,我們同樣的就挑BASE_SIZE*2這種情況來分析,首先將mTwiceBaseCache賦給mArray,這一步就是初始化mArray陣列,然後將mTwiceBaseCache指向下一個節點,然後將頭結點的1號元素,也就是快取的hashes陣列直接賦值給mhashes陣列,這一步就是初始化mhashes陣列,初始化完畢後,將使用過的節點置空,快取陣列的數量減一,完畢!

這兩個方法分析完畢之後,我們心中基本有了個大致的概念,freeArrays方法就是新增快取的,allocArrays就是取快取的,然後對於這裡的快取,主要是如下幾點需要澄清和注意

  • 只有當容器數量為BASE_SIZEBASE_SIZE*2這兩種情況下才快取
  • 這裡的快取可能跟平時所理解的快取不太一樣,這裡快取主要是實現記憶體空間的複用快取,主要是記憶體空間上的,而不是mHashes陣列和mArray陣列中資料的值的快取
  • allocArrays這個操作會清空mHashes陣列和mArray陣列中的值。所在在這個操作之前,需要儲存它們的值,然後在操作結束之後,用System.arraycopy方法再給他們賦值

接下來我們再回頭看到put方法裡的那段繞過的程式碼,現在再來看看這兩個函式到底做了什麼,如下

if (mSize >= mHashes.length) {//陣列容量滿的時候
	//計算擴容的大小
	final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
		: (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

	if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
	//儲存mHashes和mArray的資料
	final int[] ohashes = mHashes;
	final Object[] oarray = mArray;
	
	//這一步的工作就是,初始化mHashes和mArray陣列為擴容後的大小,如何初始化?
	//情形一:當且僅當n為BASE_SIZE或BASE_SIZE*2時,直接從對應的快取陣列中取,複用記憶體空間
	//從快取陣列中取,有什麼好處呢?就不用再new陣列,以免重新開闢空間,浪費記憶體
	//情形二:n不為BASE_SIZE或BASE_SIZE*2時,以new初始化mHashes陣列和mArray陣列
	allocArrays(n);
	//初始化完畢之後,再分別給對應的mHashes陣列和mArray陣列賦值
	if (mHashes.length > 0) {
		if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
			//給初始化後還未賦值的mHashes陣列賦值
			System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
			System.arraycopy(oarray, 0, mArray, 0, oarray.length);//同上
	}
	//新增快取
	//注意注意注意,這裡傳遞的size為mSize,也就是擴容之前的大小,並非擴容之後的大小n
	//引數一和引數二,也是擴容之前mHashes和mArray中的資料
	//如何新增快取?
	//情形一:mSize為BASE_SIZE或BASE_SIZE*2時
	//將mHashes陣列和mArray陣列連結到對應的快取連結串列中存起來
	//情形二:mSize不為BASE_SIZE或BASE_SIZE*2,什麼鳥事都不做
	freeArrays(ohashes, oarray, mSize);
}
  • 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

通過註釋,我們可以很清楚的看到具體的每一步做了什麼。

現在我們再來縷一下思路,關於擴容和快取這裡做個小總結:
當我們在put一個元素的時候

  • (1).如果當前元素已滿,那麼就需要擴容,擴容的大小怎麼確定,就是那個三目運算子,如果<4,則擴容至4,如果>4並且<8,則擴容至8,如果大於8,則按1.5倍增長。
  • (2)得到擴容後的大小之後:
    • 如果擴容後大小為BASE_SIZE或BASE_SIZE*2,那麼以直接從快取陣列中取的方式來建立陣列
    • 如果擴容後的大小不是上述兩種情況,那麼以new的方式來建立陣列,開闢新的空間,消耗記憶體
  • (3)給擴容後的mHashes陣列和mArray賦值
  • (4)新增擴容之前的陣列為快取(當然前提是擴容之前,size為BASE_SIZE或BASE_SIZE*2)

這個是快取在put函式中的使用,機智的你肯定還會猜想,在其它的地方應該也是有使用的,這個我們不急,待會就又見到他啦。

綜上,我們已經分析完了整個快取的思想,整個快取最終實現的效果總結成一句話就是:
避免了頻繁的建立陣列帶來的記憶體消耗(哎,為了一點記憶體,google設計人員也真的是煞費苦心啊)

現在我們對快取有了一個清晰的認識之後,我們再來思考之前原始碼中的註釋提出的一個問題:為什麼BASE_SIZE要設定為4,從整個快取的流程中我們可以看到,這個值主要就一個作用:當前陣列是否需要快取。那為什麼陣列長度為4的時候就要快取呢?其實你想一下,平常在Android開發中能用到這些容器的場景,是不是都有這些特點:使用頻率高,容納資料量小。所以如果我們不做快取處理,每次都為了一點點資料的儲存去開闢一個新的陣列空間,那麼必然會浪費掉很多記憶體。所以4和8這個值是一個相對意義上的“小”值,這個大小基本能最大程度涵蓋我們開發中大多數的應用場景,能最大限度上避免新開記憶體帶來的記憶體消耗,當然你可能會說,那剩下的那些儲存量很大的場景呢?這些場景咱們不快取也罷,畢竟遇到的少,沒有必要去做處理,咱們要考慮大局。

呼!終於收拾完了這兩個大魔頭,歇會,我們接著繼續。

弄清楚了插入方法和快取思想之後,其實剩下的方法都是些蝦兵蟹將之類的雜兵了,但是雜兵也要清,那就花點時間清一下唄。

查詢原理之前已經提到過了,我們現在就當做驗證,看看它的程式碼是否如預期,程式碼如下

    @Override
    public V get(Object key) {
        final int index = indexOfKey(key);//二分查詢獲取下標
        //mHashes陣列中index位置對應mArray中index*2+1的位置,使用位移操作以提高效率
        return index >= 0 ? (V)mArray[(index<<1)+1] : null;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這個方法看著非常乾淨利落,如果上面的都弄懂了,這個get方法的程式碼理解是完全沒有難度的。

接下來我們再看看稍顯複雜的刪除程式碼,remove方法如下

    @Override
    public V remove(Object key) {
        final int index = indexOfKey(key);//計算下標index
        if (index >= 0) {//如果找到了
            return removeAt(index);//跳轉到removeAt
        }
		//否則返回null
        return null;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我們接著來到removeAt方法,略長,但是不難,我們靜下心來一點點看

    public V removeAt(int index) {
        final Object old = mArray[(index << 1) + 1];//又是神祕操作,待會再看用來幹嘛的
        if (mSize <= 1) {//如果陣列為空或者只有一個元素,那麼直接將陣列置空即可
            // Now empty.
            if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
            freeArrays(mHashes, mArray, mSize);//又看到這個待會要收拾的方法了
            mHashes = EmptyArray.INT;//置空,INT為一個大小為0的int陣列
            //奇怪的作者為什麼要使用這個置空方法,是因為簡潔嘛?真想問問他
            mArray = EmptyArray.OBJECT;//置空,OBJECT為一個大小為0的Object陣列
            mSize = 0;//陣列大小置0
        } else {
	        //當陣列長度大於BASE_SIZE*2=8並且當前元素數量小於總容量的1/3時
            if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
	            // 嘿嘿,通過下面的英文註釋,可以看到下面的操作是在幹嘛了
                // Shrunk enough to reduce size of arrays.  We don't allow it to
                // shrink smaller than (BASE_SIZE*2) to avoid flapping between
                // that and BASE_SIZE.
                //翻譯過來就是,收縮足夠的空間來減少陣列大小,也就是說這樣是為了避免連續
                //刪除元素導致大量無用記憶體,這些記憶體需要及時釋放,以提高記憶體效率
                //(哎,再感嘆一次,為了一點點點點的記憶體,設計人員真的是煞費苦心啊)
                //但是註釋裡也說了,還要控制陣列不能收縮到小於8的值,以避免“抖動”
                //這個抖動我本來想具體解釋下的,但是我感覺這個東西完全可以意會,我就不言傳了
                //所以就留給你們自己感受這個“抖動”吧!哈哈
                
                //計算新的容量,如果大於8,那麼就收縮為當前元素數量的1.5倍,否則,就置為8
                final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2);
				//討厭的日誌,刪不掉刪不掉刪不掉!!!
                if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
				//儲存當前陣列的值
                final int[] ohashes = mHashes;
                final Object[] oarray = mArray;
                //又看到allocArrays這個方法了,嘿嘿,現在我們知道他是幹嘛的了
                allocArrays(n);//濃縮成一句話:複用記憶體以初始化mHashes陣列和mArray陣列
				//陣列元素減一
                mSize--;
                //如果刪除的下標index值大於0,則賦值以恢復mHashes和mArray陣列index之前的資料
                if (index > 0) {
	                //將之前儲存的陣列的值賦值給初始化之後的mHashes和mArray陣列,恢復資料
	                //但是注意到第五個引數index,表示這一步只是賦值了刪除元素index之前的資料
                    if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
                    System.arraycopy(ohashes, 0, mHashes, 0, index);
                    System.arraycopy(oarray, 0, mArray, 0, index << 1);
                }
                //如果index小於容器元素數量,則賦值index之後的資料
                if (index < mSize) {
                    if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize
                            + " to " + index);
                    //對mHashes陣列和mArray陣列作前移操作,前移index位置以後的元素
                    System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index);
                    //當然對mArray來說,就是前移index*2+2之後的資料元素
                    System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
                            (mSize - index) << 1);
                }
            } else {//當前陣列容量<8或者大於總容量的1/3時,不需要收縮陣列容量
                mSize--;//直接減小1
                if (index < mSize) {
                    if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize
                            + " to " + index);
                    //前移index之後的元素
                    System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index);
                    //前移index*2+2之後的元素
                    System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
                            (mSize - index) << 1);
                }
                //前移後,最後一個元素空出來了,及時置空,以釋放記憶體
                mArray[mSize << 1] = null;
                mArray[(mSize << 1) + 1] = null;
            }
        }
        //呼!分析完了
        return (V)old;
    }

  • 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
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

remove方法我在註釋中已經分析的非常詳盡了,主要就是一個地方注意的就是空間的收縮問題,根據原始碼我們可以明確的看到如果當前資料元素小於陣列長度的1/3,並且長度大於8時才收縮,為什麼是這個界限呢,因為一個數組裡容納的元素1/3都不到,可見效率是非常低的,所以為了節約記憶體要及時收縮,但是為什麼要保證大於8呢,本來是想留給你們自己意會的,我就還是再囉嗦一下,做一下解釋。

因為如果我不控制一個界限的話,那麼我在低資料量的操作時(<=8,也就是BASE_SIZE*2),我就有兩種選擇,要麼每次都從快取中去取,要麼完全不使用快取,每次都直接前移index之後的元素,首先我們肯定不能使用後者,這樣是完全違背作者設計快取的思想的,好,那我們現在採用第一種,因為沒有了>=8的條件,只需要滿足 < mHashes.leng/3即可,但是在陣列長度只有8的時候,8/3=2,也就是說我當前資料元素有1個的時候,1<2,所以要去快取取,但是我只需要再增加一個元素,也就是mSize=2時就不需要去快取取了,那我再刪除一個元素,又只有一個元素了,這時又要去快取取,那我不斷的增加刪除元素,是不是就會頻繁的去在這兩種情況之間切換,導致“抖動”。
所以我們的解決辦法就是新增一個下邊界,來避免這種情況的發生。(設計人員,您費心了)

至此,我們弄懂了所有的增刪查方法,綜上:可以看到allocArrays方法和freeArrays方法主要出現在需要調整陣列容量大小的地方,比如putremove然後再調整的時候,根據陣列的長度,來判斷是否選擇快取,所以我們不難想到這兩兄弟還會出現的第三個地方:沒錯,就是容量擴充的方法。如下:

    public void ensureCapacity(int minimumCapacity) {
        if (mHashes.length < minimumCapacity) {
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(minimumCapacity);
            if (mSize > 0) {
                System.arraycopy(ohashes, 0, mHashes, 0, mSize);
                System.arraycopy(oarray, 0, mArray, 0, mSize<<1);
            }
            freeArrays(ohashes, oarray, mSize);
        }
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

弄懂了之前的方法之後,這個方法的理解對我們來說簡直是小菜一碟呀,同樣的套路,同樣的操作!三步走:儲存當前值 -> 初始化大小 -> 移動元素 -> 快取之前的陣列 ,不對,好像是四步,嘿嘿!

好啦,我們ArrayMap的原理也弄懂了,做個小小的總結,總結下它的優秀設計

  • 儲存結構,兩個陣列儲存,一個存key的hash,一個存key和value(設計是最棒的)
  • 陣列快取設計(那2個大魔頭弄懂就行)
  • 刪除元素時的陣列容量及時收縮
  • 刪除元素時的下界控制,防止抖動

最後附上一下官方介紹自己親兒子ArrayMap的視訊連結
點我檢視Google介紹自己的親兒子ArrayMap

最後,不得不感嘆一下,優秀的思想真的有種讓人如允甘醇一般的暢快之感。

優缺點及應用場景

其實在分析原始碼的時候我們就或多或少可以感受到,它們的優點和缺點,這裡我就簡單明瞭,直接亮出它們的優缺點
SparseArray 優點:

  • 通過它的三兄弟可以避免存取元素時的裝箱和拆箱(關於裝箱和拆箱帶來的效率問題,可以檢視我的這篇文章,Java裝箱和拆箱詳解
  • 頻繁的插入刪除操作效率高(延遲刪除機制保證了效率)
  • 會定期通過gc函式來清理記憶體,記憶體利用率高
  • 放棄hash查詢,使用二分查詢,更輕量

SparseArray缺點:

  • 二分查詢的時間複雜度O(log n),大資料量的情況下,效率沒有HashMap高
  • key只能是int 或者long

SparseArray應用場景:

  • item數量為 <1000級別的
  • 存取的value為指定型別的,比如boolean、int、long,可以避免自動裝箱和拆箱問題。

ArrayMap優點:

  • 在資料量少時,記憶體利用率高,及時的空間壓縮機制
  • 迭代效率高,可以使用索引來迭代(keyAt()方法以及valueAt()方法),相比於HashMap迭代使用迭代器模式,效率要高很多

ArrayMap缺點:

  • 存取複雜度高,花費大
  • 二分查詢的O(log n )時間複雜度遠遠小於HashMap
  • ArrayMap沒有實現Serializable,不利於在Android中藉助Bundle傳輸。

ArrayMap應用場景:

  • item數量為 <1000 級別的,尤其是在查詢多,插入資料和刪除資料不頻繁的情況
  • Map中包含子Map物件

如果覺得這些優缺點一大堆,還是很迷,我就再精簡一下二者使用的取捨:

(1) 首先二者都是適用於資料量小的情況,但是SparseArray以及他的三兄弟們避免了自動裝箱和拆箱問題,也就是說在特定場景下,比如你儲存的value值全部是int型別,並且key也是int型別,那麼就採用SparseArray,其它情況就採用ArrayMap。
(2) 資料量多的時候當然還是使用HashMap啦

其實我們對比一下它們二者,有很多共性,,也就是它們的出發點都是不變的就是以時間換空間,比如它們都有即時空間壓縮機制,SparseArray採用的延遲刪除和gc機制來保證無用空間的及時壓縮,ArrayMap採用的刪除時通過邏輯判斷來處理空間的壓縮,其實說白了,它們的設計者都是力求一種極致充分的記憶體利用率,這也必然導致了所帶來的問題,例如插入刪除邏輯複雜等,不過這和我們使用起來一點都不違背,我們只需要在合適的場景進行取捨即可。

知道了它們的優缺點,我們才可以在實際應用場景中選取合適的容器來輔助我們的開發,提升我們程式的效能才是最關鍵的,畢竟人家Google設計人員煞費苦心為Android量身打造設計的容器,肯定是有使用的需求和場景的,所以我們在開發中要多對比,有取捨的使用容器。