SparseArray源码解读
google推荐使用SparseArray来代替HashMap,可见SparseArray应该有其优点,至于是什么优点,我们先来看看源码,就比较清楚了。
一、构造方法
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;
}
由构造方法可以看出,构造方法的作用就是:1、new一个int类型的数组mKeys,注意是int类型,而不是Integer类型,该数组是用来存放key的;2、new一个Object类型的数组mValues,该数组是用来存放value的;3、初始化变量mSize,该变量是键值对的个数,而不是数组的长度,初始值为0.
二、删除:public void delete(long key)
public void delete(long key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
static int binarySearch(long[] array, int size, long value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final long 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在数组mKeys的索引,如果找到key所对应的的索引,那么把对应的value置位DELETED,DELETED是一个Object对象,同时把mGarbage变量置位true,可见,只要进行了删除操作,变量mGarbage的值就是true。有两点需要注意:
(1)这里对ContainerHelpers.binarySearch进行一个简要的说明,该方法如果找到了key,那么返回key所对应的的位置索引,即是一个大于等于0且小于mSize的一个数,如果最终没有找到key,那么返回的是最后查找的索引的按位取反,源码中是~lo,我们只需记得返回一个负数即可,并且用的时候,需要对返回值取反。
(2)mSize的值并没有变化,也就是delete方法并不会使键值对的逻辑数量减少。
三、插入:public void put(long key, E value)
public void put(long 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++;
}
}
其实put方法有两层含义,如果key值已有,其实就是更新value,否则是进行插入。put方法的逻辑也是先用二分查找算法确定key的索引,如果key值已存在,那么mValues[i] = value即可;如果key值不存在,我们先对ContainerHelpers.binarySearch的值进行取反,得到二分查找最后的索引位置,如果该索引对应的value是DELETED,那么更新该位置的key和value即可,这样不用对数组进行扩充,也不用对数组的元素进行移动;如果如果该索引对应的value不是DELETED,那么就需要进行插入了,在插入之前,如果mGarbage为true并且mSize不小于mKeys.length,那么要进行gc操作,该gc操作不是java虚拟机的垃圾回收的gc,而是SparseArray定义的一个方法,源码如下:
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize;
int o = 0;
long[] 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);
}
有代码可知,gc方法是把value被标记为DELETED的元素给去掉了,并且有效数据前移,相当于去掉了内存碎片,如下图所示(仅供示意):
好了,我们只介绍put和delete方法,因为这两个方法是SparseArray内存管理的核心算法所在。通过上面的源码分析,我们可以总结得出:1、SparseArray使用的是两个数组来保存key和value,并且会在某些情况下采用元素拷贝挪移的方式清理内存碎片,提高已申请内存的使用效率,最大限度的节省内存开销;2、SparseArray的key都是基本数据类型,省去了装箱过程,这个也是一项优化。
SparseArray的相关变型
SparseIntArray:key是int类型,value是int类型
SparseBooleanArray:key是int类型,value是boolean类型
SparseLongArray:key是int类型,value是long类型
LongSparseArray:key是long类型,value是泛型,需要在实例化时指定。
关于SparseArray和HashMap的比较以及如何进行选择:
1、SparseArray比HashMap结构简洁,因此在内存使用方面SparseArray比HashMap有优势。
2、在插入、删除、查询效率方面,SparArray都涉及到了折半查找,效率为O(logN),不如HashMap。
3、SparseArray的key是int类型,省去了装箱过程,而HashMap的key是不支持基本数据类型的。
综上,个人给出建议如下:在小数据量时,HashMap和SparArray的时间效率相差不会很明显,这时候推荐采用SparseArray,可以节省内存;在数据量比较大时,数据的处理速度可能优先级比较高,特别是对于安卓客户端,用户的响应速度尤为重要,推荐使用HashMap。当然这些比较仅限于key的类型是int、boolean、long三种类型的,因为SparArray及其变型(如LongSparseArray、BooleanSparseArray)目前只支持这三种类型的key,至于其他类型,如String、Object等,只能选择HashMap。