SparseArray原理分析

奇舞移動發表於2018-10-30

系列文章地址:
Android容器類-ArraySet原理解析(一)
Android容器類-ArrayMap原理解析(二)
Android容器類-SparseArray原理解析(三)
Android容器類-SparseIntArray原理解析(四)

SparseArray和其他的Android容器類一樣,都是為了更加有效地利用記憶體,說直白點,就是為了節省記憶體。SparseArrayArrayMap一樣,都是為了更高效的儲存int值到非原始型別的對映,用了同樣的資料結構,但是為了提高效率,SparseArray也做了自己的優化。接下來就分析一下SparseArray的儲存,新增和刪除元素。

繼承結構

SparseArray原理分析

上圖表明,SparseArray並沒有像ArrayMap一樣實現Map介面,僅僅實現了Cloneable介面。

儲存結構

SparseArray原理分析

儲存結構和ArraySet以及ArrayMap一脈相承,都使用int陣列儲存key值,使用Object陣列儲存物件。不同點在於mKeys陣列中儲存的是新增元素的key值本身,沒有進行hash值得計算。

put

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

    if (i >= 0) {
        mValues[i] = value;
    } else {
        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++;
    }
}
複製程式碼

put方法首先使用二分查詢在mKeys中查詢key,如果找到,則直接更新對應下標的value。如果未找到,binarySearch方法返回待插入的下標的取反,故i = ~i。如果待插入的位置的元素已經被標記為DELETED,則直接更新並返回。如果需要執行gc函式,且需要擴大陣列的容量(mSize >= mKeys.lengt),則先執行gc函式。由於執行gc函式之後元素會發生移動,故重新計算待插入位置,最後執行元素的插入。插入函式分為插入key和插入valueGrowingArrayUtils.insert的原始碼如下:

public static int[] insert(int[] array, int currentSize, int index, int element) {
    assert currentSize <= array.length;

    if (currentSize + 1 <= array.length) {
        System.arraycopy(array, index, array, index + 1, currentSize - index);
        array[index] = element;
        return array;
    }

    int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
    System.arraycopy(array, 0, newArray, 0, index);
    newArray[index] = element;
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
}
複製程式碼

函式的邏輯很簡單,首先斷言了currentSize <= array.length;如果array在不需要擴大容量的情況下可以新增一個元素,則先將待插入位置index開始的元素整體後移一位,然後插入元素,否則先擴容,然後將元素拷貝到新的陣列中。

刪除

為什麼刪除的時候我沒有使用一個具體的函式呢,是因為SparseArray的刪除有兩種:根據key刪除物件,刪除指定位置的物件。

根據key刪除物件

public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    if (i >= 0) {
        if (mValues[i] != DELETED) {
            mValues[i] = DELETED;
            mGarbage = true;
        }
    }
}
複製程式碼

ContainerHelpers.binarySearch函式在ArraySetArrayMap的元素查詢中都出現過,作用是使用二分查詢,在mKeys中找到key的位置,如果key存在,則返回keymKeys中的下標,否則返回試圖將key插入到mKeys中的位置的取反。找到待刪除元素的下標後,SparseArray並沒有像ArraySetArrayMap一樣去刪除元素,只是將待刪除元素標記為DELETED,然後將mGarbage設定為trueDELETED實際上就是一個物件,具體申明為: Object DELETED = new Object()SparseArraygc的過程,後面會分析這個gc的過程。

刪除執行位置的物件

public void removeAt(int index) {
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}
複製程式碼

刪除指定位置元素的邏輯比較簡單,判斷待刪除位置的元素是否已經被標記為DELETED,如果沒有被標記,則標記指定位置的元素,並將mGarbage設定為true

元素在被刪除之後,都會將標誌mGarbage設定為true,這是執行gc的必要條件。

gc

說到gc,給我的第一感覺應該是什麼高深的c/c++原始碼,其實不是,貼上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函式實際上就是將mValues陣列中還未標記為DELETED的元素以及對應下標的mKeys陣列中的元素移動到陣列的前面,保證陣列在0到mSize之間的元素都是未被標記為DELETED,經過gc之後,資料的位置可能會發生移動。

在元素被刪除後,標誌mGarbage設定為true,表示可以執行gc函式了。那麼gc函式會在什麼位置執行呢? 分析SparseArray原始碼可以發現,如果mGarbage設定為true,在以下函式呼叫中gc函式會執行:

put,append,size,keyAt,valueAt,setValueAt,indexOfKey,indexOfValue,indexOfValueByValue

將以上函式總結一下可以歸納為三類:

  • 向SparseArray新增元素
  • 修改SparseArray的mValues陣列
  • 獲取SparseArray的屬性

通過執行gc將未被標記為DELETED的元素前移,在進行元素查詢時可以減少需要查詢的元素的數量,減少查詢的時間,在新增元素的時候也可以更加快速的找到待插入點。

總結

SparseArray主要是為了優化int值到Object對映的儲存,提高記憶體的使用效率。相較於HashMap,在儲存上的優化如下:

  • 使用int和Object型別的陣列分別儲存key和value,相較於HashMap使用Node,SparseArray在儲存單個key-value時更節省記憶體
  • SparseArray使用int陣列儲存int型別的key,避免了int到Integer的自動裝箱機制

雖然在儲存int到Object對映時的記憶體使用效率更高,由於使用陣列儲存陣列,在新增或者刪除元素時需要進行二分查詢,元素較多(超過1000)時效率較低,谷歌給出的建議是資料量不要超過1000,這種情況下,相較於HashMap,效率降低不會超過50%。

關注微信公眾號,最新技術乾貨實時推送

SparseArray原理分析

相關文章