SparseArray是一个Interger和Object的键值对,相当于HashMap<Integer,Object>
它具有以下特点:
1、它不同于Array<Object>
的是它的键值可以为不连续的数字,这点应该很好理解,数组的索引值是连续固定的,它可以任意且不连续,但是它不重复且是有序的。
2、它相比于HashMap<Integer,Object>
具有更好的内存效率,因为它不仅避免了键值的自动装箱,并且对于所有映射,它的数据结构不依赖于其他的实体对象,因为它内部维系的就是两个数组。它使用二分查找来索引对应的键值,通过键值就可以找到数组中对应的对象,但是它不适合包含大量的数据项,如果包含大量的数据项,它的效率将会比HashMap低,主要是因为它的插入删除操作是对数组进行插入和删除,在数组中进行插入和删除效率是比较低的,因为它要进行数据项的统一前后移动。
正是因为数组在插入和删除操作的缺陷,在SparseArray在设计的时候也进行了一些优化,在删除操作之后,它并不是立刻进行整个数组的压缩,而是将删除项标记为删除,这样在插入的时候就可以检查该项标记,这样就可以进行复用了,当需要重新进行内存分配的时候,再统一的进行内存回收,将标记的项回收掉。
下面我们来分析源码。
一、构造函数
private int[] mKeys;
private Object[] mValues;
public SparseArray() {
this(10);
}
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = ContainerHelpers.EMPTY_INTS;
mValues = ContainerHelpers.EMPTY_OBJECTS;
} else {
initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
mKeys = new int[initialCapacity];
mValues = new Object[initialCapacity];
}
mSize = 0;
}
从上面可以看到,它内部维系的就是两个数组,一个整形数组mKeys,代表键;一个对象数组mValues,代表值。
从上面可以看到,它的构造函数有两种,对于无参构造函数,它默认initialCapacity参数为10,所以我直接来看第二个构造函数:
1、如果initialCapacity==0
:
在ContainerHelpers里面:
static final int[] EMPTY_INTS = new int[0];
static final Object[] EMPTY_OBJECTS = new Object[0];
它为mKeys和mValues分别赋值为一个空数组。
2、initialCapacity!=0
:
首先使用idealIntArraySize计算数组的大小,然后分别为它们创建一个数组。
二、删除操作
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
从代码可以看到,它干了两件事:
1、将要删除的项用DELETED赋值
2、mGarbage=true,表示有内存需要回收
DELETE的定义如下,它就是一个对象:
private static final Object DELETED = new Object();
三、插入函数
public void put(int key, E value) {
//1、二分查找找到对应的index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果i>=0,说明对应的key有值,只需要进行数据的更新即可,即重新赋值
if (i >= 0) {
mValues[i] = value;
} else { // 如果 i<0
i = ~i;
//如果i在数组范围之内并且对应的值标记为删除,直接复用即可
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果数组空间不足,并且mGarbage为真
if (mGarbage && mSize >= mKeys.length) {
//进行回收操作
//它会进行数组的移动,就DELETE标记项删除
gc();
// 重新查找索引
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//如果空间不足
if (mSize >= mKeys.length) {
//重新分配内存大小
int n = ArrayUtils.idealIntArraySize(mSize + 1);
//重新分配内存
int[] nkeys = new int[n];
Object[] nvalues = new Object[n];
// 将数组的内存复制到新数组中
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
mKeys = nkeys;
mValues = nvalues;
}
if (mSize - i != 0) {
// 进行数组的统一移动,把索引为i的项移出来
System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
}
//完成插入操作
mKeys[i] = key;
mValues[i] = value;
mSize++;
}
}
我们来看看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;
// Log.e("SparseArray", "gc end with " + mSize);
}
它就是进行了数组的移动,将标记为DELETE的项删掉。
在删除对象之后,mGarbage赋值true;表示有可回收的内存,下面看看在哪些情况下会触发gc函数的调用,gc调用的前提是mGarbage为ture。
1、插入元素,空间不足,就是上面插入操作的那种情况。
2、获取容器大小的时候:
public int size() {
if (mGarbage) {
gc();
}
return mSize;
}
3、获取key
public int keyAt(int index) {
if (mGarbage) {
gc();
}
return mKeys[index];
}
4、获取value
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
5、设置value
public void setValueAt(int index, E value) {
if (mGarbage) {
gc();
}
mValues[index] = value;
}
6、得到指定key的索引
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
从这里我们也可以看出索引与key的区别。
7、得到指定值的索引
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
最后就剩下append函数了:
public void append(int key, E value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
}
int pos = mSize;
if (pos >= mKeys.length) {
int n = ArrayUtils.idealIntArraySize(pos + 1);
int[] nkeys = new int[n];
Object[] nvalues = new Object[n];
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
mKeys = nkeys;
mValues = nvalues;
}
mKeys[pos] = key;
mValues[pos] = value;
mSize = pos + 1;
}
跟上面的put函数一样,只是它是在尾部就行插入,不是在指定的索引,所以相比较于put函数,它不需要进行数组的移动。
另外,与SparseArray相似的还有下面这些类,它们内部维系的都是两个数组,下面我们来进行一个简单的比较:
SparseArray:
private int[] mKeys; //key为int类型的数组
private Object[] mValues; //value为Object类型的数组
LongSparseArray:
private long[] mKeys; // key为long类型的数组
private Object[] mValues; //vlaue为Object类型的数组
SparseBooleanArray:
private int[] mKeys; //key为int类型的数组
private boolean[] mValues; //value为boolean类型的数组
SparseIntArray:
private int[] mKeys; //key为int类型的数组
private int[] mValues; //vlaue为int类型的数组
SparseLongArray:
private int[] mKeys; //key为int类型的数组
private long[] mValues; //value为long类型的数组