为了节省内存,Android中引入了SparseArray和ArrayMap特有的数据结构。本文分析SparseArray和ArrayMap的原理。
SparseArray
SparseArray和ArrayMap一样,都是为了更高效的保存key为int型的<key,value>数据,用了同样的数据结构,但是为了提高效率,SparseArray也做了自己的优化。接下来就分析一下SparseArray的存储,添加和删除元素。
继承结构
SparseArray并没有像ArrayMap一样实现Map接口,仅仅实现了Cloneable接口。
存储结构
SparseArray的存储结构和ArraySet以及ArrayMap一脉相承,都使用int数组存储key值,使用Object数组存储value对象。不同点在于mKeys数组中存储的是添加元素的key值本身,没有进行hash值得计算。
public class SparseArray<E> implements Cloneable {
private static final Object DELETED = new Object();
/**
SparseArray中是否存在垃圾,为true表示有对象已被删除,可以启动gc,gc完成之后会把mGarbage置为false
*/
private boolean mGarbage = false;
private int[] mKeys;//存储int型的key值(注意存的是key本身,没有进行hash计算)
private Object[] mValues;
/**
当前集合中键值对的数量
*/
private int mSize;
/**
* Creates a new SparseArray containing no mappings.
*/
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;
}
...
}
put
//This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
// 高位+低位之各除以 2,写成右移,即通过位运算替代除法以提高运算效率
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
}
}
//若没找到,则lo是value应该插入的位置,是一个正数。对这个正数取反,即返回负数回去
return ~lo; // value not present
}
public void put(int key, E value) {
// 1.先进行二分查找
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 2. 如果找到了,则 i 必大于等于 0
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方法首先使用二分查找在mKeys中查找key,如果找到,则直接更新对应下标的value。如果未找到,binarySearch方法返回待插入的下标的取反,故i = ~i。如果待插入的位置的元素已经被标记为DELETED,则直接更新并返回。如果存在垃圾(mGarbage为true),且需要扩大数组的容量(mSize >= mKeys.length),则先执行gc函数,由于执行gc函数之后元素会发生移动,故重新计算待插入位置,最后执行元素的插入。插入函数分为插入key和插入value两步。GrowingArrayUtils.insert的源码如下:
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
//不需要扩容
if (currentSize + 1 <= array.length) {
//将array数组内从 index 移到 index + 1,共移了 currentSize - index 个,即从index开始后移一位,那么就留出 index 的位置来插入新的值。
System.arraycopy(array, index, array, index + 1, currentSize - index);
//在index处插入新的值
array[index] = element;
return array;
}
//需要扩容,构建新的数组,新的数组大小由growSize() 计算得到
int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
//这里再分 3 段赋值。
//首先将原数组中 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;
}
public static int growSize(int currentSize) {
//如果当前size 小于等于4,则返回8, 否则返回当前size的两倍
return currentSize <= 4 ? 8 : currentSize * 2;
}
函数的逻辑很简单,首先断言了currentSize <= array.length;如果array在不需要扩大容量的情况下可以添加一个元素,则先将待插入位置index开始的元素整体后移一位,然后插入元素,否则先进行扩容,然后将元素拷贝到新的数组中。
delete
SparseArray的删除有两种:根据key删除对象,删除指定index位置的对象。
根据key删除对象
/**
* 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;
}
}
}
通过ContainerHelpers.binarySearch在mKeys中找到key的位置,如果key存在,则返回key在mKeys中的下标,否则返回试图将key插入到mKeys中的位置的取反。找到待删除元素的下标后,SparseArray并没有像ArraySet和ArrayMap一样去删除元素,只是将待删除元素标记为DELETED,然后将mGarbage设置为true。DELETED实际上就是一个对象,具体申明为: Object DELETED = new Object(),SparseArray有gc的过程,后面会分析这个gc的过程。
删除指定index位置的对象
/**
* Removes the mapping at the specified index.
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
*/
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
删除指定位置元素的逻辑比较简单,判断待删除位置的元素是否已经被标记为DELETED,如果没有被标记,则标记指定位置的元素,并将mGarbage设置为true。
元素在被删除之后,都会将标志mGarbage设置为true,这是执行gc的必要条件。
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);
}
gc函数实际上就是将mValues数组中还未标记为DELETED的元素以及对应下标的mKeys数组中的元素移动到数组的前面,保证数组在0到mSize之间的元素都是未被标记为DELETED,经过gc之后,数据的位置可能会发生移动。
在元素被删除后,标志mGarbage设置为true,表示可以执行gc函数了。那么gc函数会在什么位置执行呢?
分析SparseArray源码可以发现,如果mGarbage设置为true,在以下函数调用中gc函数会执行:
put,append,size,keyAt,valueAt,setValueAt,indexOfKey,indexOfValue,indexOfValueByValue
将以上函数总结一下可以归纳为三类:
- 向SparseArray添加元素
- 修改SparseArray的mValues数组
- 获取SparseArray的属性
通过执行gc将未被标记为DELETED的元素前移,在进行元素查找时可以减少需要查找的元素的数量,减少查找的时间,在添加元素的时候也可以更加快速的找到待插入点。
总结
1.SparseArray内部主要通过 2 个数组来存储 key 和 value,分别是 int[] 和 Object[]。这也限定了其 key 只能为 int 类型,且 key 不能重复,否则会发生覆盖。
2.一切操作都是基于二分查找算法,将 key 以升序的方法 “紧凑” 的排列在一起,从而提高内存的利用率以及访问的效率。相比较 HashMap 而言,这是典型的时间换空间的策略。
3.删除操作并不是真的删除,而只是标记为 DELETE,以便下次能够直接复用。
4.SparseArray主要是为了优化int值到Object映射的存储,提高内存的使用效率。相较于HashMap,在存储上的优化如下:
1).使用int和Object类型的数组分别存储key和value,相较于HashMap使用Node,SparseArray存储key-value时不需要额外的结构体,更节省内存,
2).SparseArray使用int数组存储int类型的key,避免了int到Integer的自动装箱机制
3).虽然在存储int到Object映射时的内存使用效率更高,由于使用数组存储数组,在添加或者删除元素时需要进行二分查找,数据条数特别多的时候,效率会低于HashMap,谷歌给出的建议是数据量不要超过1000。
有优点就一定有缺点:
插入操作需要复制数组,增删效率降低
数据量巨大时,复制数组成本巨大,gc()成本也巨大
数据量巨大时,查询效率也会明显下降