SparseArray 实现原理
在使用HashMap的时候IDE会给出如下提示:
SparseArray 类注释如下:
SparseArrays map integers to Objects. Unlike a normal array of Objects,
there can be gaps in the indices. It is intended to be more memory efficient
than using a HashMap to map Integers to Objects, both because it avoids
auto-boxing keys and its data structure doesn't rely on an extra entry object
for each mapping.
SparseArray来代替HashMap会有更好性能,使用int[]数组存放key,避免了HashMap中基本数据类型需要装箱的操作,不使用额外的结构体(Entry),单个元素的存储成本下降。
1、类的初始化
初始化SparseArray只是简单的创建了两个数组,并将mSize赋值为0;
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;
}
2、put 方法:
public void put(int key, E value) {
//通过二分法去查找key,返回索引
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
//如果 i > 0 说明数组中已经有了key,直接覆盖原来的值
mValues[i] = value;
} else {
//取反得到key的插入位置
i = ~i;
// 如果索引小于当前已经存放的长度,并且这个位置上的值为DELETED(即被标记为删除的值)
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
// 到这一步说明直接赋值失败,检查当前是否被标记待回收且当前存放的长度已经大于或等于了数组长度
if (mGarbage && mSize >= mKeys.length) {
gc();
// 重新再获取一下索引,因为数组发生了变化
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
// 最终在 i 位置上插入键与值,并且size +1
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
插入的逻辑可以分为四点:
冲突直接覆盖:如果key 值存在则直接覆盖。
插入索引上为DELETED,直接给key 与value 赋值
索引上有值,但是应该触发gc() 然后再赋值
满容且无法gc() 扩容然后再拷贝
put 方法中使用了二分查找的算法,当找不到这个值的时候return ~lo,实际上到这一步的时候,理论上lo==mid==hi。所以这个位置是最适合插入数据的地方。但是为了让能让调用者既知道没有查到值,又知道索引位置,做了一个取反操作,返回一个负数。这样调用处可以首先通过正负来判断命中,之后又可以通过取反获取索引位置。
数据的插入方式
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;//断言
// 如果当前的长度加1还是小于数组长度
if (currentSize + 1 <= array.length) {
// 复制数组,没有进行扩容
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);
// 插入数据
newArray[index] = element;
// 复制后半部分
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
3、remove() 方法:
public void delete(int key) {
// 找到该 key 的索引
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 如果存在,将该索引上的 value 赋值为 DELETED
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
// 标记当前状态为待回收
mGarbage = true;
}
}
}
但是可以看到key仍然保存在数组中,并没有马上删除,目的应该是为了保持索引结构,同时不会频繁压缩数组,保证索引查询不会错位,那么key什么时候被删除呢?当SparseArray的gc()被调用时。
4、gc() 方法
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];
// 当前这个 value 不等于 DELETED
if (val != DELETED) {
if (i != o) {
// i != o
// 将索引 i 处的 key 赋值给 o 处的key
keys[o] = keys[i];
// 同时将值也赋值给 o 处
values[o] = val;
// 最后将 i 处的值置为空
values[i] = null;
}
// o 向后移动一位
o++;
}
}
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
o 只有在值等于DELETED的时候才不会向后移,也就是说,当i向后移动一位的时候,o还在值为DELETED的地方,而这时候因为i != o,就会触发第二个判断条件,将i位置的元素向前移动到o处。
优势:
避免了基本数据类型的装箱操作
不需要额外的结构体,单个元素的存储成本更低
数据量小的情况下,随机访问的效率更高
缺点
插入操作需要复制数组,增删效率降低
数据量巨大时,复制数组成本巨大,gc()成本也巨大
数据量巨大时,查询效率也会明显下降