SparseArray使用及源碼分析
使用方法
SparseArray源碼來自:android-28 android.util.SparseArray
首先看一下SparseArray的基本使用方法:
/**
* 創建對象
*/
SparseArray<String> sparseArray = new SparseArray<>();
/**
* 添加元素
*/
sparseArray.append(0, "str1");
sparseArray.put(1, "str2");
/**
* 刪除元素,兩種方式等同
*/
sparseArray.remove(1);
sparseArray.delete(1);
/**
* 修改元素,put或者append相同的key值即可
*/
sparseArray.put(1, "str3");
sparseArray.append(1, "str4");
/**
* 查找,遍歷
*/
//方式1
for (int i = 0; i < sparseArray.size(); i++) {
Log.i(TAG, sparseArray.valueAt(i));
}
//方式2
for (int i = 0; i < sparseArray.size(); i++) {
int key = sparseArray.keyAt(i);
Log.i(TAG, sparseArray.get(key));
}
SparseArray和HashMap有點相似,唯一不同的就是key和value的類型,HashMap的key值和value值爲泛型,但是SparseArray 的key值只能爲int 類型,value值爲Object類型。
原理分析
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
這裏先解釋一下這幾個變量:
1.DELETED是一個標誌字段,用於判斷是否刪除
2.mGarbage也是一個標誌字段,用於確定當前是否需要垃圾回收
3.mKeys數組用於存儲key
4.mValues數組用於存儲值
5.mSize表示當前SparseArray有幾個元素
接下來看幾個重要方法:
1.構造方法
public SparseArray() {
this(10);
}
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;
}
SparseArray構造方法中,創建了兩個數組mKeys、mValues分別存放int與Object,其默認長度爲10。
2.其他方法
2.1 append方法
/**
* Puts a key/value pair into the array, optimizing for the case where
* the key is greater than all existing keys in the array.
*/
public void append(int key, E value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) {
//當mSize不爲0並且不大於mKeys數組中的最大值時,因爲mKeys是一個升序數組,最大值即爲mKeys[mSize-1]
//直接執行put方法,否則繼續向下執行
put(key, value);
return;
}
//當垃圾回收標誌mGarbage爲true並且當前元素已經佔滿整個數組,執行gc進行空間壓縮
if (mGarbage && mSize >= mKeys.length) {
gc();
}
//當數組爲空,或者key值大於當前mKeys數組最大值的時候,在數組最後一個位置插入元素
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
//元素加一
mSize++;
}
2.2 put方法
public void put(int key, E value) {
// 二分查找,key在mKeys列表中對應的index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 如果找到,則直接賦值
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;// binarySearch方法中,找不到時,i取了其非,這裏再次取非,則非非則正
// 如果該位置的數據正好被刪除,則賦值
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
// 如果有數據被刪除了,則gc
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
// 插入數據,增長mKeys與mValues列表
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
因爲key爲int,不存在hash衝突;mKeys爲有序列表,通過二分查找,找到要插入的key對應的index ,相對於查找hash表應該算是費時間,但節省了內存,所以是時間換取空間;通過二分查找到的index,將Value插入到mValues數組的對應位置。
2.3 ContainerHelpers.binarySearch方法(二分查找)
// This is Arrays.binarySearch(), but doesn't do any argument validation.
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) {// 如果中間元素小於要查找元素,則midIndex賦值給 lo
lo = mid + 1;
} else if (midVal > value) {// 如果中間元素大於要查找元素,則midIndex賦值給 hi
hi = mid - 1;
} else {// 找到則返回
return mid; // value found
}
}
// 找不到,則lo 取非
return ~lo; // value not present
}
2.4 get方法
public E get(int key) {
return get(key, null);
}
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
// mKeys數組中採用二分查找,找到key對應的index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 沒有找到,則返回空
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {// 找到則返回對應的value
return (E) mValues[i];
}
}
每次調用get方法,則需執行一次mKeys數組的二分查找,因此mKeys數組越大則二分查找的時間就越長,因此SparseArray在大量數據,千以上時,效率較低。
2.5 gc方法
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize;
int o = 0;
int[] keys = mKeys;//保存新的key值的數組
Object[] values = mValues;//保存新的value值的數組
for (int i = 0; i < n; i++) {
Object val = values[i];
if (val != DELETED) {//如果該value值不爲DELETED,也就是不刪除
if (i != o) {
keys[o] = keys[i];//將 i 位置的元素向前移動到 o 處,這樣做最終會讓所有的非DELETED元素連續緊挨在數組前面
values[o] = val;
values[i] = null;//釋放空間
}
o++;//新數組元素加一
}
}
mGarbage = false;//回收完畢,置爲false
mSize = o;//回收之後數組的大小
// Log.e("SparseArray", "gc end with " + mSize);
}
主要就是通過之前設置的DELETED 標籤來判斷是否需要刪除,然後進行數組前移操作,將不需要刪除的元素排在一起,最後設置新的數組大小和設置mGarbage 爲false。
優缺點和應用場景
1.優點
1.1 避免了基本數據類型的裝箱操作
1.2 會定期通過gc函數來清理內存,內存利用率高
1.3 數據量小的情況下,隨機訪問的效率更高
2.缺點
2.1 數據量巨大時,複製數組成本巨大,gc()成本也巨大,查詢效率也會明顯下降
2.2 插入操作需要複製數組,增刪效率降低
3.應用場景
3.1 數量 <1000的時候
3.2 存取的value爲指定類型的,比如boolean(SparseBooleanArray)、int(SparseIntArray)、long(SparseLongArray)可以避免自動裝箱和拆箱問題