【实习周记】SparseArray源码分析
一.概述
SparseArray是Android中的容器,适用于Android这种对内存非常敏感的移动平台,用来存储键值对,适用于数据量不大,key必须为int类型的情况。
SparseArray内部通过两个数组实现。一个int类型的数组,用来存key。一个object类型的数组,用来存value。
二.主要方法的源码分析
1.重要字段
(1).private static final Object DELETED = new Object();//删除键值对后value数组对应位置的填充,代表该位置被删除
(2).private boolean mGarbage = false;//垃圾回收标志
(3).private int[] mKeys;// 用于存放key的数组
(4).private Object[] mValues;//用于存放value的数组
(5).private int mSize;//用于记录填充数据的数量
2.构造方法
(1).初始化mValues数组和mKeys数组,可以指定数组长度,默认长度为10
(2).设置mSize为0
3.put方法
public void put(int key, E value) {
//首先通过二分查找获取key的位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//若i>0说明key存在,之前添加过
if (i >= 0) {
//此时用新的值覆盖旧的值
mValues[i] = value;
} else {
//若i<0,说明key不存在,则对i加1并取相反数,作为新的键值对存储的位//置
i = ~i;
//这句话的意思时如果得到的i是之前被删除的键值对的位置
//则当前的键值对直接使用
//因为sparseArray有垃圾回收的机制,所以要进行此判断
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果垃圾回收标志为true,同时当前的键值对数量大于等于数组的长度
if (mGarbage && mSize >= mKeys.length) {
//进行垃圾回收
gc();
//防止垃圾回收后键值对的位置发生变化。再重新获取一次
//因为此时的键值对是新添加的,所以要进行~操作,使i变成正数
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//添加键值对
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
//长度加1
mSize++;
}
}
(1).二分查找
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
}
}
return ~lo; // value not present
}
当查找不到key时,会返回一个负值,
~lo = -(lo+1)
(2).垃圾回收
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];
if (val != DELETED) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
//关闭垃圾回收
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
sparseArray的垃圾回收算法与JVM的标记整理算法类似,把当前存在的键值对,移到最左面,从0位置开始。同时,对移动后的value数组原位置清空(null)。以此来减少存储的碎片化。
(3).键值对的添加
GrowingArrayUtils类提供多种类型的insert方法,内部实现相同。在此分析int类型
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);
//将新的数据添加到index位置
array[index] = element;
return array;
}
//若长度不满足,则需要扩容
int[] newArray = ArrayUtils.newUnpaddedIntArray(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;
}
(4).扩容
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
若小于等于4,则扩到8,若大于4,则扩大2倍
4.get方法
public E get(int key, E valueIfKeyNotFound) {
//获取key的位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//若不存在,则返回默认值
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
//若存在,返回相应的值
return (E) mValues[i];
}
}
5.delete方法
public void delete(int key) {
//获取key的位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//若key存在
if (i >= 0) {
//若对应value位置没有被删除
if (mValues[i] != DELETED) {
//删除,并开启垃圾回收
mValues[i] = DELETED;
mGarbage = true;
}
}
}
6.remove方法
内部调用delete方法
三.总结
1.优点
避免了key的自动装箱
其数据结构不依赖于额外的Entry对象来存储
2.缺点
当容器内在大量元素的时候,使用二分查找会带来很差的性能
在存在大量元素以及涉及大量增删的时候,由于会引起数组的频繁变化,使性能降低
3.插入的效率
插入的效率其实主要跟Key值插入的先后顺序有关,
如果Key值是按递减顺序插入的,那么每次我们都是在mValues的[0]位置插入元素,这就要求把原来Values和mKeys数组中的元素向后移动一个位置。
如果是递增插入 的则不会存在该问题,直接扩大数组的范围之后再插入即可。
4.延迟回收
延迟回收机制的好处在于首先删除方法效率更高,同时减少数组数据来回拷贝的次数,比如删除某个数据后被标记删除,接着又需要在相同位置插入数据,则不需要任何数组元素的来回移动操作。可见,对于SparseArray适合频繁删除和插入来回执行的场景,性能很好。