SparseArray詳解及源碼簡析

1、前言

SparseArray 是 Android 在 Android SdK 爲咱們提供的一個基礎的數據結構,其功能相似於 HashMap。與 HashMap 不一樣的是它的 Key 只能是 int 值,不能是其餘的類型。java

2、代碼分析

1. demo 及其簡析

首先也仍是先經過 demo 來看一看 SparseArray 的基本使用方法,主要就是插入方法以及遍歷方法。這也會後面的代碼分析打下一個基礎。算法

SparseArray<Object> sparseArray = new SparseArray<>();
        sparseArray.put(0,null);
        sparseArray.put(1,"fsdfd");
        sparseArray.put(2,new String("fjdslfjdk"));
        sparseArray.put(3,1);
        sparseArray.put(4,new Boolean(true));
        sparseArray.put(5,new Object());
        sparseArray.put(8,new String("42fsjfldk"));
        sparseArray.put(20,"jfslfjdkfj");
        sparseArray.put(0,"chongfude");

        int size = sparseArray.size();
        for (int i = 0;i < size;i++) {
            Log.d(TAG, "sparseArraySample: i = " + i + ";value = " + sparseArray.get(sparseArray.keyAt(i)) );
        }
複製代碼

上面代碼先是 new 了一個 SparseArray,注意聲明時只能指定 value 的類型,而 key 是固定爲 int 的。而後再往裏面添加 key 以及 value。這裏注意一下的是 key 爲 0 的狀況插入了 2 次。遍歷時,是先經過順序的下標取出 key ,再經過 keyAt 來 get 出 value。固然也能夠一步到位經過 valueAt() 直接獲取到 value。而後這個 demo 的執行結果以下。數組

sparseArraySample: i = 0;value = chongfude sparseArraySample: i = 1;value = fsdfd sparseArraySample: i = 2;value = fjdslfjdk sparseArraySample: i = 3;value = 1 sparseArraySample: i = 4;value = true sparseArraySample: i = 5;value = java.lang.Object@b67a0fa sparseArraySample: i = 6;value = 42fsjfldk sparseArraySample: i = 7;value = jfslfjdkfjbash

而後經過 Debug 來看一看在內存中,SparseArray 實際是如何存儲的。以下圖分別是 key 與 value 在內存中的形式。能夠看出 keys 和 values 的大小都爲 13,並且 keys 的值是按從小到大順序排列的。數據結構

keys

values

2.源碼分析

下面是 SparseArray 的類圖結構,能夠看到其屬性很是的少,也能夠看出其分別用了數組 int[] 和 object[] 來存儲 key 以及 value。 源碼分析

SparseArray.jpg

  • SparseArray 初始化 SparseArray 的初始化也就是它的構造方法。
public SparseArray() {
        this(10);
    }

   public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;
    }
複製代碼

其有 2 個構造方法,帶參與不帶參。固然,這個參數就是指定數組初始大小,也就是 SparseArray 的初始容量。而不帶參數則默認指定數組大小爲 10 個。ui

  • 插入數據 put() 方法
public void put(int key, E value) {
        // 1.先進行二分查找
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 2. 若是找到了,則 i 必大於等於 0
        if (i >= 0) {
            mValues[i] = value;
        } else {
        // 3. 沒找到,則找一個正確的位置再插入
            i = ~i;
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            if (mGarbage && mSize >= mKeys.length) {
                gc();
                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }
複製代碼

這裏調用了不少外部的方法以及內部的方法。首先是 ContainerHelpers#binarySearch() 的二分查找算法。this

//This is Arrays.binarySearch(), but doesn't do any argument validation. static int binarySearch(int[] array, int size, int value) { int lo = 0; int hi = size - 1; while (lo <= hi) { // 高位+低位之各除以 2,寫成右移,即經過位運算替代除法以提升運算效率 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; // value found } } //若沒找到,則lo是value應該插入的位置,是一個正數。對這個正數去反,返回負數回去 return ~lo; // value not present } 複製代碼

二分查找的分析屬於基礎內容,在註釋中了。回到 put() 方法首先經過二分查找算法從當前 keys 中查找是否已經存在相同的 key 了,若是存在則會返回大於等於 0 的下標,而後接下來就會將原下標下的 values 中的舊value 替換成新的 value 值,即發生了覆蓋。spa

那若是沒有找到,那麼將 i 取反就是要插入的位置了,這一結論正好來自 binarySearch() 的返回結果。能夠看到其最後若是沒有找到的話,就會返回 lo 的取反數。那麼這裏再把它取反過來那就是 lo 了。3d

這裏若是 i 是在大小 mSizes 的範圍內的,且其對應的 values[i] 又剛是被標記爲刪除的對象,那麼就能夠複用這個對象,不然就仍是要依當前的 i 值進一步尋找要插入的位置,再插入相應的 value。

在插入以前,若是因爲以前進行過 delete(),remoeAt() 以及 removeReturnOld() 中的某一個方法,那就可能要進行 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) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
                o++;
            }
        }
        mGarbage = false;
        mSize = o;
    }
複製代碼

經過代碼很容易分析得出,這裏的 gc ,實際就是壓縮存儲,簡單點說就是讓元素捱得緊一點。

而 gc() 完以後,下標 i 可能會發生變化,所以須要從新查找一次,以獲得一個新的下標 i。

最後就是經過 GrowingArrayUtils.insert() 來進行 key 和 value 的插入。這個 insert() 根據數組類型重載了多個,這裏只分析 int[] 類型的便可。

public static int[] insert(int[] array, int currentSize, int index, int element) {
        //確認 當前集合長度 小於等於 array數組長度
        assert currentSize <= array.length;
        //不須要擴容
        if (currentSize + 1 <= array.length) {
            //將array數組內從 index 移到 index + 1,共移了 currentSize - index 個,即從index開始後移一位,那麼就留出 index 的位置來插入新的值。
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            //在index處插入新的值
            array[index] = element;
            return array;
        }
        //須要擴容,構建新的數組,新的數組大小由growSize() 計算獲得
        int[] newArray = new int[growSize(currentSize)];
        //這裏再分 3 段賦值。首先將原數組中 index 以前的數據複製到新數組中
        System.arraycopy(array, 0, newArray, 0, index);
        //而後在index處插入新的值
        newArray[index] = element;
        //最後將原數組中 index 及其以後的數據賦值到新數組中
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }
複製代碼

上面的算法中,若是不須要擴容則直接進行移位以留出空位來插入新的值,若是須要擴容則先擴容,而後根據要插入的位置 index,分三段數據複製到新的數組中。這裏再看看 growSize() 是如何進行擴容 size 的計算的。

public static int growSize(int currentSize) {
        //若是當前size 小於等於4,則返回8, 不然返回當前size的兩倍
        return currentSize <= 4 ? 8 : currentSize * 2;
    }
複製代碼

代碼相對簡單,當前 size 小於等於 4 則爲 8 ,不然爲 2 倍大小。

  • get() 方法
public E get(int key) {
        return get(key, null);
    }
    public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }
複製代碼

get() 方法就是經過 key 來返回對應的 value,前面在分析 put() 的時候已經分析過了二分查找。那麼這裏若是找到了,就會經過下標直接從 mValues[] 中返回。

  • 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;
            }
        }
    }
複製代碼

delete() 也很是簡單,經過二分查找算法定位到下標,而後將對應的 value 標記爲 DELETE,而且標記須要進行 gc 了。這裏須要注意的是被標記爲 DELETE 的 value 不會在 gc 中被移除掉,而只會被覆蓋掉,從而提升了插入的效率。

3、總結

文章對 SparseArray 進行了簡要的分析,文章也只對主要的幾個方法進行了分析,其餘沒有分析到的方法在這個基礎上再進行分析相信也是很簡單的。而總結下來幾點是:

  • 其內部主要經過 2 個數組來存儲 key 和 value,分別是 int[] 和 Object[]。這也限定了其 key 只能爲 int 類型,且 key 不能重複,不然會發生覆蓋。
  • 一切操做都是基於二分查找算法,將 key 以升序的方法 「緊湊」 的排列在一塊兒,從而提升內存的利用率以及訪問的效率。相比較 HashMap 而言,這是典型的時間換空間的策略。
  • 刪除操做並非真的刪除,而只是標記爲 DELETE,以便下次可以直接複用。

最後,感謝你能讀到並讀完此文章。受限於做者水平有限,若是存在錯誤或者疑問都歡迎留言討論。若是個人分享可以幫助到你,也請記得幫忙點個贊吧,鼓勵我繼續寫下去,謝謝。

相關文章
相關標籤/搜索