一、概述
最近看一些关于Android性能优化方面的书,有讲到了使用Android提供的SparseArray代替Java的HashMap来存储key-value键值对,一定程度上能够提升性能,但是SparseArray也有一定的局限性,比如key的类型固定为int,存储的元素个数过大时对性能有较大的影响等。平时使用过SparseArray,但是未进行深入研究,藉此机会来学习一下。
二、使用SparseArray示例
SparseArray采用泛型(SparseArray<E>
)来支持存储任意引用类型的元素,它的使用是比较简单的,因为它的key固定为int类型,所以只需指定value的类型即可。下面为了演示,采用String作为value的类型。实际使用时指定我们需要的类型即可,最终存储的key-value的类型分别为<int,E>
。
示例如下,列举了常用的几个方法(更多方法可以查看其api):
// 创建SparseArray,指定存储的值的类型为String
SparseArray<String> sparseArray = new SparseArray<>();
for (int i = 0; i < 20; i++) {
sparseArray.put(i, "element" + i); // put插入元素
}
// get获取元素值
System.out.println("element value:"+sparseArray.get(10));
// 删除元素
sparseArray.delete(10);
// 获取key对应的索引
int index = sparseArray.indexOfKey(15);
// valueAt根据索引获取元素值
System.out.println("element value:"+sparseArray.valueAt(index));
// 删除一个范围的值,从index起始,删除6个
sparseArray.removeAtRange(index, 6);
三、SparseArray源码分析
SparseArray是一种存储key-value键值对的容器,前面使用时我们就发现,只需指定value的类型即可,那是因为它内部对key的类型已经做了严格限定(只允许为int类型),因此只需指定value的类型即可,并且容器存储的value只允许为引用类型。
SparseArray将key和value分别使用数组存储,使用数组位置的索引作为key-value的映射纽带,通过二分查找key对应的索引,进而通过索引找到value的值。对元素的存储也有一套管理机制(gc调节),处理已经处于DELETED 状态的元素达到复用,使存储空间达到最优利用。
下面我们通过SparseArray的使用流程,来一步一步解剖它。
1. 创建SparseArray
创建SparseArray只需要一句话,如下:
SparseArray<String> sparseArray = new SparseArray<>();
实际上这是创建一个默认的SparseArray,它初始大小为10。查看源码可知,它是调用了另一个构造方法,初始化了一个大小为10的容器。
/**
* Creates a new SparseArray containing no mappings.
*/
public SparseArray() {
this(10);
}
/**
* Creates a new SparseArray containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings. If you supply an initial capacity of 0, the
* sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
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;
}
看到这里,出现了好几个全局变量:mKeys ,mValues ,mSize ,它们的定义如下:
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys; // 存储key的数组容器,类型为int
private Object[] mValues; // 存储value的数组容器,类型为Object,可存储任意引用类型
private int mSize; //实际存储的元素个数
可以发现key、value是单独存储的,对于上面的mKeys ,mValues ,mSize 都是比较好理解的,但是DELETED 常量是什么意思?见名知意,它确实表示一种删除的对象,当对mValues中的元进行删除时,实际上是赋值为DELETED 的,表示当前元素被删除了,对DELETED 的理解可以认为当前元素是无意义的或者是已经删除的。
那么mGarbage 又是什么?从名字上看,似乎与垃圾回收有关系,诚然它对SparseArray存储空间的动态管理起着重要的作用,在后面后详细分析它。
2. SparseArray存储元素
SparseArray使用put方法来存储元素,该方法定义如下:
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, E value) {// 插入 , key是有序存储的,从小到大
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 二分查找key是否存在,返回key的索引
if (i >= 0) { // key存在则直接覆盖value
mValues[i] = value;
} else { // key不存在
i = ~i; // i= -i-1 ; 如i=-15 , 则i = ~i后i=14
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); // 将key插入指定位置i
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); // 将value插入指定位置i
mSize++;
}
}
可以发现对key进行二分查找,获取key在mKeys中的索引,然后根据这个索引来存储value。因此,对mKeys来说是有序的(二分查找的保证),并且根据实现来看是从小到大的。
搜索key的索引可能不存在,此时二分查找返回的i是实际应该插入的位置index取负数-1(-index-1,即~i),此时进行 i = ~i;操作,即可得到应该插入的位置index,当然这个位置上的value有2中可能情况:
- 1.value可能是DELETED,此时直接进行覆盖就可以了
- 2.value的值是有意义的,此时需要腾出位置来以供插入
对于第二种情况,实现是由GrowingArrayUtils的insert方法来完成的,如下:
public static <T> T[] insert(T[] array, int currentSize, int index, T 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;
}
// 数组空间不够,重新开辟空间
T[] newArray = (T[]) Array.newInstance(array.getClass().getComponentType(),
growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index); // 拷贝0~index-1 到新数组
newArray[index] = element; // 插入到index位置
// 拷贝index 开始的原数组元素到新数组的index + 1起始,拷贝剩余的元素
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray; // 最后返回新创建的数组
}
特别说明:
arraycopy函数是System类的一个静态函数,用于将一个数组的内容拷贝到另一个数组。
函数原型
public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
- 从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。
- 源数组中位置在 srcPos 到 srcPos+length-1 之间的组件被分别复制到目标数组中的 destPos 到 destPos+length-1 位置。
参数:
- src - 源数组。
- srcPos - 源数组中的起始位置。
- dest - 目标数组。
- destPos - 目标数据中的起始位置。
- length - 要复制的数组元素的数量。
当数组长度不大于4时,指定增长为8,否则增长原来长度的两倍,增长规律通过如下函数控制:
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
使用ContainerHelpers的binarySearch方法进行二分查找,对于其中的二分算法如下,没有什么好说的:
// 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) {
final int mid = (lo + hi) >>> 1;
final int midVal = array[mid];
if (midVal < value) {
lo = mid + 1; // mid + 1 ~ hi
} else if (midVal > value) {
hi = mid - 1; // lo ~ mid - 1
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
2. SparseArray获取元素
SparseArray获取元素使用 get方法,定义如下:
/**
* Gets the Object mapped from the specified key, or <code>null</code>
* if no such mapping has been made.
*/
public E get(int key) {
return get(key, null);
}
/**
* Gets the Object mapped from the specified key, or the specified Object
* if no such mapping has been made.
*/
public E get(int key, E valueIfKeyNotFound) {
// 二分查找key是否存在,返回key的索引
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 不存在key或key对应的值为DELETED,则返回指定的默认值valueIfKeyNotFound
if (i < 0 || mValues[i] == DELETED) { return valueIfKeyNotFound;
} else {
return (E) mValues[i]; // 存在key则返回mValues中相同索引的value
}
}
可以发现,获取元素是非常简单的,两个方法,要么不指定默认值,不存在则返回null;要么指定默认值valueIfKeyNotFound,不存在则返回之。和存储元素类似,使用二分查找获得索引,然后得到value。
还有类似的valueAt、keyAt等就不一一分析了,原理大致相同。
3. SparseArray删除元素
SparseArray使用delete方法,通过key来删除一组key-value映射,该方法定义如下:
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 二分查找key是否存在,返回key的索引
if (i >= 0) { // 存在key
if (mValues[i] != DELETED) { // value有意义
mValues[i] = DELETED; // 赋值为DELETED
mGarbage = true; // 垃圾回收flag置为true
}
}
}
delete方法实现比较简单,主要有几个关键点:
- 1.使用二分查找得到索引,和之前一样
- 2.对要删除的value赋值为DELETED,即将此处的value置为无意义的
- 3.置mGarbage 为true,为内存空间的管理做准备
4. SparseArray内存管理
上面多次见过gc方法,那它是干什么的呢?对于一个容器而言,增删是很平常的事,因此如何管理内存是一个关键点。SparseArray对内存管理使用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++) { // 循环遍历,将数组后面有意义的值,移动到前面为DELETED的位置
Object val = values[i];
if (val != DELETED) {
if (i != o) { // 将i处的key-value映射前移到o处
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o; // 调整后的数组的元素的真实个数
// Log.e("SparseArray", "gc end with " + mSize);
}
这个方法比较关键,对我们掌握SparseArray原理至关重要。通常有两个条件同时满足时,才触发gc操作(某些方法只需要mGarbage为true,如size()方法):
- mGarbage
- mSize >= mKeys.length
mGarbage 标志着是否有元素存在垃圾状态,即被置为DELETED
mSize >= mKeys.length 表示此时存储的元素个数已满,看看是否需要调整
对于gc的原理是这样的:当满足gc条件后,执行gc,遍历所有mValues的元素,发现DELETED状态,则记录下来(o),在后面的遍历发现不为DELETED时,则将当前的元素key-value映射移动到之前记录的o处,这样使mValues数组的前面x项都被填充有意义的值,后面的空间都腾出来,使数组能够充分利用空间,而不是再开辟新空间来容纳元素。
举个栗子:
gc前的mValues假设是这样子的,当前mSize=7 :
element1, element2,element3,DELETED,element5,DELETED,element7
注:DELETED元素一般是删除了元素造成的,会使mGarbage=true
gc后的mValues变成了如下,当前mSize=5:
element1, element2,element3,element5,element7,null,null
gc后使得mValues的空间得以较好的利用,而不必从新开辟空间造成浪费。
5. SparseArray 使用index获取key与value
在理解上面的内容的基础上,看懂下面代码也比较容易了:
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseArray stores.
*
* <p>The keys corresponding to indices in ascending order are guaranteed to
* be in ascending order, e.g., <code>keyAt(0)</code> will return the
* smallest key and <code>keyAt(size()-1)</code> will return the largest
* key.</p>
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
*/
public int keyAt(int index) { // 未做index检查,可能导致异常产生
if (mGarbage) {
gc();
}
return mKeys[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the value from the <code>index</code>th key-value mapping that this
* SparseArray stores.
*
* <p>The values corresponding to indices in ascending order are guaranteed
* to be associated with keys in ascending order, e.g.,
* <code>valueAt(0)</code> will return the value associated with the
* smallest key and <code>valueAt(size()-1)</code> will return the value
* associated with the largest key.</p>
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
*/
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
从上面的注释可以得到:
- 在得到key或value时,可能会触发gc操作
- keyAt(0)获得最小的key,keyAt(size()-1)可获得最大的key
- valueAt(0)返回的是最小key对应的value,valueAt(size()-1)返回的是最大key对应的位置
- index的范围在0…size()-1其他值是不被支持的
6. SparseArray 删除指定索引、指定范围以及清空
删除指定索引处
/**
* 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; // 直接赋值为DELETED
mGarbage = true;
}
}
删除范围,利用removeAt来完成
/**
* Remove a range of mappings as a batch.
*
* @param index Index to begin at
* @param size Number of mappings to remove
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
*/
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
removeAt(i);
}
}
清空 容器
/**
* Removes all key-value mappings from this SparseArray.
*/
public void clear() {
int n = mSize;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
values[i] = null; // 清空时会将元素的值赋值为null
}
mSize = 0;
mGarbage = false;
}
注意:
- removeAt、removeAtRange删除元素会导致mGarbage 被置为true,但不会使mSize变化
- clear操作会将所有元素的值赋值为null,并且置mSize为0
四、总结
- 1.对于SparseArray而言,对其key-value映射方式(index,二分查找),插入元素的处理(直接覆盖还是需要拓展空间)、以及内存管理(gc)的理解是较为关键的。
- 2.对于SparseArray 而言,适用于存在数量不多的数据(通常推荐是1000以内),这样才能达到性能较优。毕竟当数量大了以后,每当有删除操作,接下来的操作(比如keyAt)都可能导致gc,会比较消耗内存,影响性能。
- 3.由于key被限定为int类型,因此在选择时需要考虑存入的元素的key的类型,这需要结合具体的情况综合考虑与运用。
- 4.SparseArray 为Android提供的容器,用来替代HashMap等Java类容器,因此使用范围有一定的平台局限性。
- 5.为了达到Android前后版本的兼容性,可以考虑使用v4包的SparseArrayCompat来代替SparseArray。