1.定义
- SparseArray是一个以基本类型int为键,Object为值的键值映射对。类似的还有SparseIntArray、SparseLongArray、SparseBooleanArray。
- 由于不需要自动装箱拆箱,并且不需要创建其他实体(例如HashMap中的Entry),因而它拥有更好的内存效率。
- 它使用二分查找进行查找、插入和删除,因而不适用于数据量大的情况,在查找效率方面,一般也是不如HashMap的,数据量大时更为突出。
2.应用场景
SparseArray适用于数据量小,并且存取的值为指定类型,例如boolean, long, int, 可以避免自动装箱和拆箱问题。
3.源码解析
private static final Object DELETED = new Object();
private boolean mGarbage = false;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
private int[] mKeys;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, E)
private Object[] mValues;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
private int mSize;
首先来看一下SparseArray的属性定义,DELETED这个对象先放一下,感觉是和删除相关的。然后mGarbage这个属性是一个bool值,garbage翻译为垃圾感觉也是和删除相关的,也放到分析删除的代码时再看。然后就是mKeys、mValues和mSize,通过属性名可以看出:
- mKeys为存储键的数组。
- mValues为存储值的数组。
- mSize应该是存入键值的数量。
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;
}
这里看到构造函数需要传入一个初始的大小,如果初始大小为0.,则创建一个int空数组和object空数组赋值。否则的话,这里newUnpaddedObjectArray调用了native方法VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen),这个方法会new一个不填充的数组,返回一个至少为minLen长度的数组,但有可能会更大。因此mKeys使用Object数组的length进行初始化,而不是使用initialCapacity。
public E get(int key) {
//默认值为null
return get(key, null);
}
@SuppressWarnings("unchecked")
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];
}
}
查找对应两个方法,一个为传默认值的,另一个为默认值返回null的。
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
//通过二分查找获取索引,若不存在为最接近的小值取反
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//i>=0 说明查到了
if (i >= 0) {
//若果对应的value不为DELETED
if (mValues[i] != DELETED) {
//将对应的value置为DELTED,mGarbage置为true
mValues[i] = DELETED;
mGarbage = true;
}
}
}
果然上面提到的DELETE和mGarbage是与删除相关的,删除只是做标记,不进行数组压缩,那么什么时候进行数组压缩呢?
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
//遍历
for (int i = 0; i < n; i++) {
Object val = values[i];
//若果值不为DELETED
if (val != DELETED) {
//并且新下标不等于旧下标
if (i != o) {
//将旧位置的key和value移到新位置
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
//新下标向后移一位
o++;
}
}
//标记置为false,标明已进行数组压缩
mGarbage = false;
//大小置为新数组键值对的数量
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
可以看到gc这个方法中对数组进行了压缩,移除了值为NULL的键值,那么什么时候会调用gc呢?
以上是所有调用gc方法的地方,可以将以上函数总结一下可以归纳为向数组中添加元素,修改mValues数组,获取SparseArray属性(键、值、size)。方法中都是通过判读mGarbage属性是否为true来判断是否需要gc。
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, E value) {
//二分查找获取索引
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//若果数组中已包含
if (i >= 0) {
mValues[i] = value;
} else {
//i取反获取数组索引
i = ~i;
//如果所查找的位置对应的值已标记为要回收
if (i < mSize && mValues[i] == DELETED) {
//覆盖要回收的值
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果数组需要被压缩并且键值对数量超过或者等于key数组的数量,这里比较这个大小,说明key数组已经满了,添加需要扩容, 因此回收之后可以不用进行数组扩容。
if (mGarbage && mSize >= mKeys.length) {
//回收
gc();
// 重新查找索引由于数组已变化
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
可以看到put方法整体流程:
查找索引 -> 若key存在则直接替换 -> 若key对应的value被标记为Delete,则替换key、value -> 若需要回收,则先回收重新计算索引 -> 添加键值
/**
* Primitive int version of {@link #insert(Object[], int, int, Object)}.
*/
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {
//若增加一个数据的大小不超过数组的长度时,直接将index之后的数据整体向后移一位
System.arraycopy(array, index, array, index + 1, currentSize - index);
//赋值
array[index] = element;
return array;
}
//若超过数组长度,则进行扩容
int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
//复制旧数组到新数组
System.arraycopy(array, 0, newArray, 0, index);
//index赋值
newArray[index] = element;
复制旧数组index后的数据到新数组index+1的位置,即将index之后数据整体向后移一位
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
public static int growSize(int currentSize) {
//若不超过4则返回8,否则返回原大小乘2
return currentSize <= 4 ? 8 : currentSize * 2;
}
这里通过查看key的insert方法,可以看出如果添加后的大小不超过数组大小,则直接进行添加,否则需要先扩容在添加,value的添加方法完全一致。
添加还有一个方法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]) {
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
}
//直接将数据放入mSize的位置,这里可以看到调用的GrowingArrayUtils的append方法
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
如果已知键大于数组中的所有键,则只需要将数据按顺序加到数组的最后位置即可。