SparseArray
基础
Java的HashMap使用的是数组+链表的实现结构,无论有没有是否添加数据,都会分配一个数组(该数组用于存放链表的头元素)。这在某种程度上浪费了一部分的内存空间,而且HashMap的key,value必须是Object,所以对于基本数据类型来说,在存储到HashMap时必须经过一个自动装箱的过程,这又浪费了一部分性能。这算是HashMap的缺点。
对于SparseArray来说,它避免了HashMap的自动装拆箱的过程:因为它的key值是int类型的。它内部采用双数组实现:一个int[]类型数组,用于存储key值;一个Object[]数组,用于存储value。这里就有一个与ArrayList类似的问题:数组扩容——当初始容量不够,需要扩展一下key数组,value数组。
因此,在SparseArray的使用时,应该考虑初始容量的大小。大了,浪费内存;小了,数组扩容频繁,导致性能降低。
构造函数
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;
}
很简单,初始化两个数组,并将mSize设置为0。mSize指的是当前数组中具体存有的元素的个数,此处不能用数组的length——它只数组能存储的元素的个数,并不代表着数组中已存储了多少个元素。
get
如下:
public E get(int key) {
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];
}
}
ContainerHelpers.binarySearch()使用的是二分查找。为什么可以直接用二分查找,是因为mKeys数组中是已经排好序的,在put方法时可以看出。代码如下:
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
}
这个方法有一点要注意,如果没有找到指定value时的返回值:~lo(取反),而整个代码中可以看出:lo是不可能小于0的,所以~lo是小于0的。
因此,binarySearch()的返回值有两个作用:大于0,则代表所查找value在array中的下标;小于0,则代表array中没有该value,并且此时的返回值取反后为value应该插入到array中的下标。一个返回值两个作用:太帅了!!
知道这点后,就可以看出get的逻辑,其中==DELETED的逻辑在remove时说。其余略。
但,这里的无参get写的不太好,因为SparseArray本身的value可以是null。如果当前没有该key,并且调用了无参数的get方法,返回的也是null,容易造成模糊。
remove
如下:
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
/**
* @hide
* Removes the mapping from the specified key, if there was any, returning the old value.
*/
public E removeReturnOld(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
final E old = (E) mValues[i];
mValues[i] = DELETED;
mGarbage = true;
return old;
}
}
return null;
}
/**
* Alias for {@link #delete(int)}.
*/
public void remove(int key) {
delete(key);
}
逻辑很简单,并且remove与delete可以认为是同一个方法。removeReturnOld时,将旧有的值给返回了。
在remove时,并不是将当前key对应的value设置为null,而是设置成DELETED,它的定义如下:
private static final Object DELETED = new Object();
这样做的目的有两个:其一不用remove/delete时直接移动数组,因为内部采用的是双数组结构,对于数组来说查找方便,但插入删除很耗性能,所以并没有在每一次删除时都移动数组。这里使用DELETED相当于占位符,不用移数组,而且也知道该位置已经被remove/delete掉了,get时就能知道返回何值。其二,并没有直接设置为null,这样保证了SparseArray中可以存储null值。
gc
这是一个private方法,但这个方法很有意思,而且对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];
if (val != DELETED) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
}
该方法的功能很简单:移除掉那些被remove/delete过的key。为什么要这么做?为了节约内存。为什么不直接在remove/delete时删掉,原因见remove部分。
put
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
先查找key的位置,如果当前key已经存在,直接覆盖对应的value。如果没有,但要插入的位置上的元素为DELETED,就直接用新key,value取代原有的。否则就直接使用insert进行插入。
这中间有一个判断,当remove/delete(mGarbage只有在remove时才会被赋值为true)过,并且数组已经存储满了,那么就调用gc删除其中的deleted,然后再插入——这是为了在尽可能不影响性能的情况下节约内存。如果mGarbage为false,表示没有remove过,那么就不需要调用gc()。如果mSize<mKeys.length表明当前的数组并没有存满,现在不需要移动deleted。如果两个条件都满足,就表示当前数组中有脏数据,gc()之后就会空余出一部分空间,就不需要重新扩容了。
在put时也涉及到数组的扩容,即思路与ArrayList中完全一样。代码如下:
public static <T> T[] More ...insert(T[] array, int currentSize, int index, T element) {
assert currentSize <= array.length; //断言,当前的size肯定要小于数组的长度
if (currentSize + 1 <= array.length) {//数组容量足够,不需要扩容,只是将index后面的往后挪一位
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
@SuppressWarnings("unchecked")
//growSize代码为currentSize <= 4 ? 8 : currentSize * 2;
T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
growSize(currentSize));//生成一个新数组,容易为growSize。这里就是进行了扩容,逻辑完全与ArrayList相同
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
其余
后面还有一些方法,比较简单,捡其中两个说下:
clear():清空value数组,将其中的value全部设置为null。
append():类似于put,但比put的要求要多一个:使用append的前提时你知道你的key是比SparseArray中所有的key都大的。因为二分查找时要求Key时从小到大排列的。在满足这一点的前提下,使用append比put效率高。
总结
1,采用双数组结构可以模拟Map。扩展一下,采用多个数组,就可以模拟一对多的情况:一个数组值key,多个数组存value。当然,也可将多个value组建成珍上Object,直接使用SparseArray。
2,善用函数返回值。一个返回值可以表达多种信息。
3,将可能执行延误性能的操作尽量延迟到必须执行时才进行,因为有可能不执行。如上面的并不有在remove时直接移动数组,而是用DELETED做标识,到容量不够时才gc。