系列文章地址:
Android容器類-ArraySet原理解析(一)
Android容器類-ArrayMap原理解析(二)
Android容器類-SparseArray原理解析(三)
Android容器類-SparseIntArray原理解析(四)
SparseArray
和其他的Android容器類一樣,都是為了更加有效地利用記憶體,說直白點,就是為了節省記憶體。SparseArray
和ArrayMap
一樣,都是為了更高效的儲存int值到非原始型別的對映,用了同樣的資料結構,但是為了提高效率,SparseArray
也做了自己的優化。接下來就分析一下SparseArray
的儲存,新增和刪除元素。
繼承結構
上圖表明,SparseArray
並沒有像ArrayMap
一樣實現Map
介面,僅僅實現了Cloneable
介面。
儲存結構
儲存結構和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
和插入value
。GrowingArrayUtils.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
函式在ArraySet
和ArrayMap
的元素查詢中都出現過,作用是使用二分查詢,在mKeys
中找到key
的位置,如果key
存在,則返回key
在mKeys
中的下標,否則返回試圖將key
插入到mKeys
中的位置的取反。找到待刪除元素的下標後,SparseArray
並沒有像ArraySet
和ArrayMap
一樣去刪除元素,只是將待刪除元素標記為DELETED
,然後將mGarbage
設定為true
。DELETED
實際上就是一個物件,具體申明為: Object DELETED = new Object()
,SparseArray
有gc
的過程,後面會分析這個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%。