- 按照從構造方法->常用API(增、刪、改、查)的順序來閱讀源碼,并會講解閱讀方法中涉及的一些變量的意義。
1 概要
- SparseArray<E>是用于在Android平臺上替代HashMap的數據結構,更具體的說,
是用于替代key為int類型,value為Object類型的HashMap。 - 和ArrayMap類似,它的實現相比于HashMap更加節省空間,而且由于key指定為int類型,也可以節省int-Integer的裝箱拆箱操作帶來的性能消耗。
- 它僅僅實現了implements Cloneable接口,所以使用時不能用Map作為聲明類型來使用。
- 它也是線程不安全的,允許value為null。
從原理上說,
- 它的內部實現也是基于兩個數組。
- 一個int[]數組mKeys,用于保存每個item的key,key本身就是int類型,所以可以理解hashCode值就是key的值.
- 一個Object[]數組mValues,保存value。容量和key數組的一樣。
類似ArrayMap,
- 它擴容的更合適,擴容時只需要數組拷貝工作,不需要重建哈希表。
- 同樣它不適合大容量的數據存儲。存儲大量數據時,它的性能將退化至少50%。
- 比傳統的HashMap時間效率低
- 因為其會對key從小到大排序,使用二分法查詢key對應在數組中的下標。
- 在添加、刪除、查找數據的時候都是先使用二分查找法得到相應的index,然后通過index來進行添加、查找、刪除等操作。
所以其是按照key的大小排序存儲的。
- 另外,SparseArray為了提升性能,在刪除操作時做了一些優化:當刪除一個元素時,并不是立即從value數組中刪除它,并壓縮數組,而是將其在value數組中標記為已刪除。這樣當存儲相同的key的value時,可以重用這個空間。如果該空間沒有被重用,隨后將在合適的時機里執行gc(垃圾收集)操作,將數組壓縮,以免浪費空間。
適用場景:
- 數據量不大(千以內)
- 空間比時間重要
- 需要使用Map,且key為int類型。
示例代碼:
SparseArray<String> stringSparseArray = new SparseArray<>();
stringSparseArray.put(1,"a");
stringSparseArray.put(5,"e");
stringSparseArray.put(4,"d");
stringSparseArray.put(10,"h");
stringSparseArray.put(2,null);
Log.d(TAG, "onCreate() called with: stringSparseArray = [" + stringSparseArray + "]");
輸出:
//可以看出是按照key排序的
onCreate() called with: stringSparseArray = [{1=a, 2=null, 4=d, 5=e, 10=h}]
2 構造函數
//用于標記value數組,作為已經刪除的標記
private static final Object DELETED = new Object();
//是否需要GC
private boolean mGarbage = false;
//存儲key 的數組
private int[] mKeys;
//存儲value 的數組
private Object[] mValues;
//集合大小
private int mSize;
//默認構造函數,初始化容量為10
public SparseArray() {
this(10);
}
//用于標記value數組,作為已經刪除的標記
private static final Object DELETED = new Object();
//是否需要GC
private boolean mGarbage = false;
//存儲key 的數組
private int[] mKeys;
//存儲value 的數組
private Object[] mValues;
//集合大小
private int mSize;
//默認構造函數,初始化容量為10
public SparseArray() {
this(10);
}
//指定初始容量
public SparseArray(int initialCapacity) {
//初始容量為0的話,就賦值兩個輕量級的引用
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
//初始化對應長度的數組
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
//集合大小為0
mSize = 0;
}
關注一下幾個變量:
- 底層數據結構為int[]和Object[]類型數組。
- mGarbage: 是否需要GC
- DELETED: 用于標記value數組,作為已經刪除的標記
3 增 、改
3.1 單個增、改:
public void put(int key, E value) {
//利用二分查找,找到 待插入key 的 下標index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果返回的index是正數,說明之前這個key存在,直接覆蓋value即可
if (i >= 0) {
mValues[i] = value;
} else {
//若返回的index是負數,說明 key不存在.
//先對返回的i取反,得到應該插入的位置i
i = ~i;
//如果i沒有越界,且對應位置是已刪除的標記,則復用這個空間
if (i < mSize && mValues[i] == DELETED) {
//賦值后,返回
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果需要GC,且需要擴容
if (mGarbage && mSize >= mKeys.length) {
//先觸發GC
gc();
//gc后,下標i可能發生變化,所以再次用二分查找找到應該插入的位置i
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//插入key(可能需要擴容)
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
//插入value(可能需要擴容)
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
//集合大小遞增
mSize++;
}
}
//二分查找 基礎知識不再詳解
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
}
}
//若沒找到,則lo是value應該插入的位置,是一個正數。對這個正數去反,返回負數回去
return ~lo; // value not present
}
//垃圾回收函數,壓縮數組
private void gc() {
//保存GC前的集合大小
int n = mSize;
//既是下標index,又是GC后的集合大小
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
//遍歷values集合,以下算法 意義為 從values數組中,刪除所有值為DELETED的元素
for (int i = 0; i < n; i++) {
Object val = values[i];
//如果當前value 沒有被標記為已刪除
if (val != DELETED) {
//壓縮keys、values數組
if (i != o) {
keys[o] = keys[i];
values[o] = val;
//并將當前元素置空,防止內存泄漏
values[i] = null;
}
//遞增o
o++;
}
}
//修改 標識,不需要GC
mGarbage = false;
//更新集合大小
mSize = o;
}
GrowingArrayUtils.insert:
public static int[] insert(int[] array, int currentSize, int index, int element) {
//斷言 確認 當前集合長度 小于等于 array數組長度
assert currentSize <= array.length;
//如果不需要擴容
if (currentSize + 1 <= array.length) {
//將array數組內元素,從index開始 后移一位
System.arraycopy(array, index, array, index + 1, currentSize - index);
//在index處賦值
array[index] = element;
//返回
return array;
}
//需要擴容
// 構建新的數組
int[] newArray = new int[growSize(currentSize)];
//將原數組中index之前的數據復制到新數組中
System.arraycopy(array, 0, newArray, 0, index);
//在index處賦值
newArray[index] = element;
//將原數組中index及其之后的數據賦值到新數組中
System.arraycopy(array, index, newArray, index + 1, array.length - index);
//返回
return newArray;
}
//根據現在的size 返回合適的擴容后的容量
public static int growSize(int currentSize) {
//如果當前size 小于等于4,則返回8, 否則返回當前size的兩倍
return currentSize <= 4 ? 8 : currentSize * 2;
}
- 二分查找,若未找到返回下標時,與JDK里的實現不同,JDK是返回return -(low + 1); // key not found.,而這里是對 低位去反 返回。
- 這樣在函數調用處,根據返回值的正負,可以判斷是否找到index。對負index取反,即可得到應該插入的位置。
- 擴容時,當前容量小于等于4,則擴容后容量為8.否則為當前容量的兩倍。和ArrayList,ArrayMap不同(擴容一半),和Vector相同(擴容一倍)。
- 擴容操作依然是用數組的復制、覆蓋完成。類似ArrayList.
- 擴容操作依然是用數組的復制、覆蓋完成。類似ArrayList.
4 刪
5.1 按照key刪除
//按照key刪除
public void remove(int key) {
delete(key);
}
public void delete(int key) {
//二分查找得到要刪除的key所在index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果>=0,表示存在
if (i >= 0) {
//修改values數組對應位置為已刪除的標志DELETED
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
//并修改 mGarbage ,表示稍后需要GC
mGarbage = true;
}
}
}
5.2 按照index刪除
public void removeAt(int index) {
//根據index直接索引到對應位置 執行刪除操作
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
5.3 批量刪除
public void removeAtRange(int index, int size) {
//越界修正
final int end = Math.min(mSize, index + size);
//for循環 執行單個刪除操作
for (int i = index; i < end; i++) {
removeAt(i);
}
}
5 查
5.1 按照key查詢
//按照key查詢,如果key不存在,返回null
public E get(int key) {
return get(key, null);
}
//按照key查詢,如果key不存在,返回valueIfKeyNotFound
public E get(int key, E valueIfKeyNotFound) {
//二分查找到 key 所在的index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//不存在
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {//存在
return (E) mValues[i];
}
}
5.2 按照下標查詢
public int keyAt(int index) {
//按照下標查詢時,需要考慮是否先GC
if (mGarbage) {
gc();
}
return mKeys[index];
}
public E valueAt(int index) {
//按照下標查詢時,需要考慮是否先GC
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
5.3 查詢下標:
public int indexOfKey(int key) {
//查詢下標時,也需要考慮是否先GC
if (mGarbage) {
gc();
}
//二分查找返回 對應的下標 ,可能是負數
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
public int indexOfValue(E value) {
//查詢下標時,也需要考慮是否先GC
if (mGarbage) {
gc();
}
//不像key一樣使用的二分查找。是直接線性遍歷去比較,而且不像其他集合類使用equals比較,這里直接使用的 ==
//如果有多個key 對應同一個value,則這里只會返回一個更靠前的index
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
- 按照value查詢下標時,不像key一樣使用的二分查找。是直接線性遍歷去比較,而且不像其他集合類使用equals比較,這里直接使用的 ==
- 如果有多個key 對應同一個value,則這里只會返回一個更靠前的index
6 總結
- SparseArray的源碼相對來說比較簡單,經過之前幾個集合的源碼洗禮,很輕松就可以掌握大體流程和關鍵思想:時間換空間;
Android sdk中,還提供了三個類似思想的集合: - SparseBooleanArray,value為boolean
- SparseIntArray,value為int
- SparseLongArray,value為long
他們和SparseArray唯一的區別在于value的類型,SparseArray的value可以是任意類型。而它們是三個常使用的拆箱后的基本類型。