SparseArray sparse 稀疏
介紹
SparseArray 用來實現 int 類型與 Object 類型的映射,跟普通的 Map 不同,普通 Map 中有更多的空索引,對比 HashMap 來說,稀疏數組實現了更高效的內存使用,因為稀疏數組避免了 int 類型 key 的自動裝箱,且稀疏數組每個 value 都不需要使用 Entry 對象來包裝。所以在 Android 開發中,我們可以使用 SparseArray 來實現更高效的實現 Map
SparseArray 實現了 Cloneable 接口,說明時支持克隆操作的,下面慢慢分析增刪改查以及克隆等操作
一、成員變量
/**
* 刪除操作時替換對應位置 value 的默認值
*/
private static final Object DELETED = new Object();
/**
* 是否需要回收
*/
private boolean mGarbage = false;
/**
* 存儲 key 的數組
*/
private int[] mKeys;
/**
* 存儲 value 的數組
*/
private Object[] mValues;
/**
* 當前存儲的鍵值對數量
*/
private int mSize;
SparseArray 中聲明了一個 int 類型的數組和一個 Object 類型的數組
二、構造函數
/**
* 創建一個空 map 初始容量為 10
*/
public SparseArray() { this(10); }
/**
* 根據指定初始容量創建鍵值對為空的稀疏數組,并且不會申請額外內存;指定初始容量為 0 時會創建一個輕量級的不需要任何內存分配的稀疏數組
* capacity 容量
*/
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT; // 長度為 0 的 int 類型數組
mValues = EmptyArray.OBJECT; // 長度為 0 的 Object 類型數組
} else {
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;
}
SparseArray 有兩個構造函數,默認時創建初始容量為 10 數組,另外一個時可以使用者指定出事容量的數量
三、添加/修改 操作
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 使用二分法查找對應的 key 在數組中的下標
if (i >= 0) { // 索引大于等于 0 說明原數組中有對應 key
mValues[i] = value; // 則直接 Value 數組中的 value 值為最新的 value
} else { // 索引小于 0 說明原數組中不存在對應的 key
i = ~i; // 取反后得到當前 key 應該在的位置
if (i < mSize && mValues[i] == DELETED) { // 如果數組長度夠,并且當前位置已被回收則直接對該位置賦值
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) { // 回收狀態為 true 并且內容長度大于等于 key 數組長度
gc(); // 回收,整理數組
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); // 再次使用二分法查找位置
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); // 執行 key 插入到 key 數組對應位置
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); // 執行 value 插入到 value 數組對應位置
mSize++; // 鍵值對數量加 1
}
}
上面的 put 方法中用到了一個 ContainerHelpers 的 binarySearch 函數,我們先來看一下這個函數的操作,主要是使用二分法查找對應的位置
// ContainerHelpers
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; // 帶符號右移,也就是做除以 2,這里是找到中間位置索引的操作
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 的時候,這里是將如果數組中存在 value 時應該在的位置取反后返回
}
接著我們看一下 gc() 方法的操作
//
private void gc() {
int n = mSize; // 鍵值對數量
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) { // 通過循環將 value 數組中的 DELETED 值移除,并且 DELETED 以后的鍵跟值都往前補
Object val = values[i];
if (val != DELETED) {
if (i != o) { // 循環第一次執行時 i 和 o 都是 0 ,這種情況不需要處理
keys[o] = keys[i];
values[o] = val;
values[i] = null; // 原位置置空
}
o++;
}
}
mGarbage = false; // 回收狀態置為 false
mSize = o; // 將鍵值對的值更新為實際的鍵值對數量
}
/**
* GrowingArrayUtils 中定義了 泛型/int/long/boolean 等類型數組在指定位置插入數據的方法
*/
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 = new int[growSize(currentSize)];
System.arraycopy(array, 0, newArray, 0, index); // 將對應位置前的內容插入
newArray[index] = element; // 將對應位置內容插入
System.arraycopy(array, index, newArray, index + 1, array.length - index); // 將對應位置后的內容插入
return newArray;
}
/**
* GrowingArrayUtils 中定義了 泛型/int/long/boolean 等類型數組在指定位置插入數據的方法,這個方法的作用為,在位置超出數組大小時,計算擴容后數組的新長度
* 舊數組長度小于 4 則設置為 8,否則都是在當前長度基礎上擴容一被
*/
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
小結一下,插入操作的工作是,首先在原 key 數組中查找是否有對應的 key,如果找到則直接替換 value 數組中對應下標的值;如果 key 不存在之前的 key 數組,則需要根據是否回收狀態進行無用數據回收,然后執行插入,插入過程中如果數組需要擴容還需要執行擴容操作。
由插入操作可以看出,keys 數組中的值為從小到大排列,是一個有序數組
上面分析了插入方法的主要邏輯,接下來繼續看 查找/刪除 等操作,如果明白了插入操作,下面的就都簡單了
四、查找方法 get(int key)
public E get(int key) {
return get(key, null);
}
/**
* 根據 key 查找 value ,如果 key 不存在則返回指定的默認值
*/
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 在 key 數組中的位置,如果位置不小于 0 且 value 數組中對應位置的值部位 DELETED,說明找到對應值,直接返回,否則就返回 null。get() 操作是有一個重載方法的,調用者可以傳入一個默認值,在查不到對應 key 時則返回默認值。
五、刪除方法 delete(int 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;
}
}
}
public E removeReturnOld(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
final E old = (E) mValues[i];
mValues[i] = DELETED;
mGarbage = true;
return old;
}
}
return null;
}
刪除操作就更簡單了,首先通過二分法查找 key 所在位置,找到就將 value 中對應位置的值設置為 DELETED,在其他操作時通過 gc() 操作執行該位置的回收。removeReturnOld 方法則是會返回刪除的 value 值。
同時,SparseArray 也提供了移除指定位置的鍵值對的方法
/**
* 刪除指定位置的值
*/
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
/**
* 以 index 開始,刪除之后 size 個值,包含 index 位置,不包含 index + size
*/
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
removeAt(i);
}
}
六、其他操作
克隆
SparseArray 重寫了 clone 方法,科隆時其 keys,values 數組都會克隆成新的數組
@Override
@SuppressWarnings("unchecked")
public SparseArray<E> clone() {
SparseArray<E> clone = null;
try {
clone = (SparseArray<E>) super.clone();
clone.mKeys = mKeys.clone();
clone.mValues = mValues.clone();
} catch (CloneNotSupportedException cnse) {
/* ignore */
}
return clone;
}
size() 返回鍵值對的數量
首先執行 gc() 操作,然后返回正確的數量
public int size() {
if (mGarbage) {
gc();
}
return mSize;
}
keyAt() valueAt() setValueAt() indexOfKey() indexOfValue()
/**
* 返回指定位置的 key
*/
public int keyAt(int index) {
if (mGarbage) {
gc();
}
return mKeys[index];
}
/**
* 返回指定位置的 value
*/
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
/**
* 將對應位置的值設置為指定 value
*/
public void setValueAt(int index, E value) {
if (mGarbage) {
gc();
}
mValues[index] = value;
}
/**
* 返回指定位置的 key
*/
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
/**
* 返回指定位置的 value
*/
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++) {
if (mValues[i] == value) {
return i;
}
}
return -1;
}
/**
* 返回指定 value 所在位置,只不過 value 相等的判斷使用 equals 方法
*/
public int indexOfValueByValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++) {
if (value == null) {
if (mValues[i] == null) {
return i;
}
} else {
if (value.equals(mValues[i])) {
return i;
}
}
}
return -1;
}
/**
* 移除所有鍵值對
*/
public void clear() {
int n = mSize;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
values[i] = null;
}
mSize = 0;
mGarbage = false;
}
/**
* 插入鍵值對,優化插入的 key 大于所有現在已有 key 的情況,由于 key 數組是從大到小的有序數組,所以這種情況下不需要二分法查找位置,優化了性能
*/
public void append(int key, E value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) { // 如果不是大于現在已有的 key ,則按照正常方式插入
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) { // 執行回收 DELETED 的 value
gc();
}
mKeys = GrowingArrayUtils.append(mKeys, mSize, key); // 直接向后插入
mValues = GrowingArrayUtils.append(mValues, mSize, value); // 直接向后插入
mSize++;
}
/**
* 打印所有的 key value
*/
public String toString() {
if (size() <= 0) {
return "{}";
}
StringBuilder buffer = new StringBuilder(mSize * 28);
buffer.append('{');
for (int i=0; i<mSize; i++) {
if (i > 0) {
buffer.append(", ");
}
int key = keyAt(i);
buffer.append(key);
buffer.append('=');
Object value = valueAt(i);
if (value != this) {
buffer.append(value);
} else {
buffer.append("(this Map)");
}
}
buffer.append('}');
return buffer.toString();
}
七、總結
SparseArray 的代碼非常少,只有 450 行左右,并且特別易于理解。但 SparseArray 要比 HashMap 更加高效,在 Android 手機中,如果 key 為 int 類型的 Map 數據,最好使用 SparseArray 來實現。