一、基本概念
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
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:http://www.lxweimin.com/p/fd82d18994ce
- Android 面試文檔分享:http://www.lxweimin.com/p/8456fe6b27c4