一、基本概念
SparseArray
的用法和key
為int
型別,value
為Object
型別的HashMap
相同,和HashMap
相比,先簡要介紹一下它的兩點優勢。
記憶體佔用
在 Java&Android 基礎知識梳理(8) - 容器類 我們已經學習過HashMap
的內部實現,它內部是採用陣列的形式儲存每個Entry
,並採用鏈地址法來解決Hash
衝突的問題。但是採用陣列會遇到擴容的問題,預設情況下當陣列內的元素達到loadFactor
的時候,會將其擴大為目前大小的兩倍,那麼就有可能造成空間的浪費。
SparseArray
雖然也是採用陣列的方式來儲存Key/Value
private int[] mKeys;
private Object[] mValues;
複製程式碼
但是與HashMap
使用普通陣列不同,它對存放Value
的mValues
陣列進行了優化,其建立方式為:
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
//預設情況下,建立的 initialCapacity 大小為 10。
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;
}
複製程式碼
其中ArrayUtils.newUnpaddedObjectArray(initialCapacity)
用於建立優化後的陣列,該方法實際上是一個Native
方法,它解決了當陣列中的元素沒有填滿時造成的空間浪費。
在 SparseArray 淺析 一文中介紹了SparseArray
對於陣列的優化方式,假設有一個9 x 7
的陣列,在一般情況下它的儲存模型可以表示如下:

可以看到這種模型下的陣列當中存在大量無用的0
值,記憶體利用率很低。而優化後的方案用兩個部分來表示陣列:
- 第一部分:存放的是陣列的行數、列數、當前陣列中有效元素的個數
- 第二部分:存放的是所有有效元素的行、列數、元素的值

mKeys
則是用普通陣列實現的,通過查詢Key
值所在的位置,再根據mValues
陣列的屬性找到對應元素的行、列值,從而得到對應的元素值。
避免自動裝箱
對於HashMap
來說,當我們採用put(1, Object)
這樣的形式來放入一個元素時,會進行自動裝箱,即建立一個Integer
物件放入到Entry
當中。
SparseArray
則不會存在這一問題,因為我們宣告的就是int[]
型別的mKeys
陣列。
二、原始碼解析
2.1 存放過程
public void put(int key, E value) {
//通過二分查詢法進行查詢插入元素所在位置。
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果大於0,那麼直接插入。
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);
}
//重新分配陣列,並插入新的 Key,Value。
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
複製程式碼
2.2 讀取過程
public E get(int key, E valueIfKeyNotFound) {
//通過二分查詢,在 Key 陣列中得到對應 Value 的下標。
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//取出下標對應的元素。
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
複製程式碼
2.3 刪除過程
public void delete(int key) {
//二分查詢所在位置。
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//將該位置的元素置為 DELETED,它是內部預先定義好的一個物件。
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
複製程式碼
可以看到,在刪除元素的時候,它是用一個空的Object
來標記該位置。在合適的時候(例如上面的put
方法),才通過下面的gc()
方法對mKeys
和mValues
陣列 重新排列。
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;
}
複製程式碼
2.4 二分查詢
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; // value found
}
}
//如果沒有找到,那麼返回的是低位指標的取反座標。
return ~lo; // value not present
}
複製程式碼
這裡用的是~
,由於lo>=0
,所以當無法查詢到對應元素的時候,返回值~lo
一定<0
。(~lo=-(lo+1)
)
這也是我們在2.1
中看到,為什麼在i>=0
時就可以直接替換的原因,因為只要i>=0
,就說明之前已經存在一個Key
相同的元素了。
而在返回值小於0
時,對它再一次取~
,就剛好可以得到 要插入的位置。
三、SparseArray 的效率問題
瞭解了SparseArray
的原理之後,我們可以分析出有以下幾方面有可能會影響SparseArray
插入的效率:
- 插入的效率。插入的效率其實主要跟
Key
值插入的先後順序有關,假如Key
值是按 遞減順序 插入的,那麼每次我們都是在mValues
的[0]
位置插入元素,這就要求把原來Values
和mKeys
陣列中[0, xxx]
位置元素複製到[1, xxx+1]
的位置,而如果是 遞增插入 的則不會存在該問題,直接擴大陣列陣列的範圍之後再插入即可。 - 查詢的效率。這點很明顯,因為採用了二分查詢,如果查詢的
Key
值位於折半處,那麼將會更快地找到對應的元素。
也就是說SparseArray
在插入和查詢上,相對於HashMap
並不存在明顯的優勢,甚至在某些情況下,效率還要更差一些。
Google
之所以推薦我們使用SparseArray
來替換HashMap
,是因為在移動端我們的資料集往往都是比較小的,而在這種情況下,這兩者效率的差別幾乎可以忽略。但是在記憶體利用率上,由於採用了優化的陣列結構,並且避免了自動裝箱,SparseArray
明顯更高,因此更推薦我們使用SparseArray
。
四、SparseArray 的衍生
SparseArray
還有幾個衍生的類,它們的基本思想都是一樣的,即:
- 用兩個陣列分別儲存
key
和value
,通過下標管理對映關係。 - 採用二分查詢法查詢現在
mKeys
陣列中對應找到所在元素的下標,再去mValues
陣列中取出元素。
我們在平時使用的時候,可以根據實際的應用場景選取相應的集合型別。
Key 型別不同
假如key
為long
型:
LongSparseArray
:key
為long
,value
為Object
Value 型別不同
假如key
為int
,而value
為下面三種基本資料型別之一,那麼可以採用以下三種集合來避免value
的自動裝箱來進一步優化。
SparseLongArray
:key
為int
,value
為long
SparseBooleanArray
:key
為int
,value
為boolean
SparseIntArray
:key
為int
,value
為int
Key 和 Value 型別都不同
假如key
和value
都不為基本資料型別,那麼可以採用:
ArrayMap
:key
為Object
,value
為Object