本文借鉴自文章Android中你还在用HashMap<Integer,Object>吗?
在Android Studio中输入以下代码:
Map<Integer,Object> map = new HashMap<>();
系统会提示:
Use new SparseArray<Object>(...) instead for better performance
推荐我们使用SparseArray<Object>
来取代HashMap<Integer,Object>
,以获取更好的性能。
SparseArray简介
SparseArray
是java.util
包下的类,介绍如下:
SparseArrays map integers to Objects. Unlike a normal array of Objects, there can be gaps in the indices. It is intended to be more memory efficient than using a HashMap to map Integers to Objects, both because it avoids auto-boxing keys and its data structure doesn’t rely on an extra entry object for each mapping.
大致意思是:
SparseArrays是个整型到对象的映射,与一般的对象数组不同,它的索引值可以为空。它设计的目的是为了比HashMap<Integer,Object>拥有更高的内存性能。
主要优点有两个:1. 它避免了对key自动装箱的操作;
2. 它的映射关系并不依赖于额外的对象;
简单提一下自动装箱:Integer i = 6;
将int基本数据类型的值包装成Integer类的对象,底层实际上调用Integer的构造函数,将int类型的值作为参数传入。
可能有人会有疑惑:为什么HashMap中的key是Integer类型而不是int类型,我们平常写入的基本上都是int类型的,这样还要自动装箱不是浪费吗?
这要从HashMap的原理来看,HashMap是通过key的hashcode值初步定位,而基本数据类型由于只能存放在栈空间中,没有对象可言,因而也没有hashCode()等方法,所以需要自动装箱成Integer类型,调用Integer类的hashCode()方法来获取hashcode值,这样不可避免地造成了性能的浪费。
这就对SparseArray提出了考验,怎样能够避免自动装箱造成的浪费同时又能够读写映射关系呢?
SparseArray的使用
我们先来对比一下SparseArray和HashMap的使用,代码如下:
//定义
SparseArray<Object> sparseArray = new SparseArray<>() ;
Map<Integer,Object> map = new HashMap<>() ;
//添加数据
sparseArray.put(1,new Object());
map.put(1,new Object()) ;
//取数据
Object o1 = sparseArray.get(1) ;
Object o2 = map.get(1) ;
//删除数据
sparseArray.remove(1);
Object o3 = map.remove(1);
可以看出两者在使用上没有什么区别,只是SparseArray的remove方法没有返回值,完全可以类比HashMap的用法去使用SparseArray。
从源码分析SparseArray的内存性能
成员变量
//存储索引集合
private int[] mKeys;
//存储对象集合
private Object[] mValues;
//存储键值对总数
private int mSize;
//已删除值通用的标记对象
private static final Object DELETED = new Object();
//是否需要进行gc
private boolean mGarbage = false;
和HashMap的数据结构不同,HashMap是通过数组+链表的数据结构存储键值对,而SparseArray是创建两个数组存储。
由于采用int类型的数组,避免了自动装箱的操作,同时key和value之间不需要有额外的对象维护映射,后文可以看出是采用数组间一一对应的方式维护的,所以内存的使用效率很高。
构造函数
public SparseArray() {
this(10);
}
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
// 返回零长度的数组或者集合,而不是null
mKeys = ContainerHelpers.EMPTY_INTS;
mValues = ContainerHelpers.EMPTY_OBJECTS;
} else {
mKeys = new int[initialCapacity];
mValues = new Object[initialCapacity];
}
mSize = 0;
}
put
public void put(int key, E value) {
//此处调用了ContainerHelpers类的binarySearch方法,后面会介绍
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果i符合条件则替换i处的value值
if (i >= 0) {
mValues[i] = value;
} else {
//取反,用于标定插入的位置,后文会介绍
i = ~i;
//特殊情况,空间未满的情况下i所在处的value值为空,直接插入
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//空间已满,调用gc回收垃圾
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++;
}
}
ContainerHelpers
类提供的二分查找法是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
}
就是一个标准的二分查找,“>>>”右移符号为了避免溢出,如果未匹配上返回的是lo的取反值,这样返回值为非负则查找成功,为负则查找失败,put
方法中就可以通过返回值来做相应的操作。
put
方法中else
后面紧跟的取反操作可以将返回的负值再变回原值,而原值的lo代表当前元素按照升序排列应该插入的下标,经过取反操作后可以直接插入。
put
方法中最后的insert
方法会在数组已满的时候自动扩容,扩容代码如下:
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
//下面的是容量不够的情况
@SuppressWarnings("unchecked")
T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
System.arraycopy方法用于拷贝数组,如果操作的数组对象相同,则复制过程就是将数组中的要移动的部分复制到一个临时数组中,然后再从这个临时数组中将数据复制到原数组中,相当于整体移位。
扩容的算法是由growSize
方法控制的,源码如下:
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
get
get方法最终调用下面这个方法,其中valueIfKeyNotFound
的值为null
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];
}
}
这个方法简单,二分法查找,如果没找到返回空,找到了返回值。
remove
remove
方法最终调用的是delete
方法,代码如下:
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
这里的设计也考虑了性能优化,二分法找到需要删除的value后,并没有立即删除,而是用DELETE做了标记,并且将可清理状态设置为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;
}
gc的原理就是遍历数组,将非DELETE标记的资源往前移。
总结
- 使用上
SparseArray
与Map<Integer,Object>
没有多大区别,只是remove方法前者没有返回值。 SparseArray
比Map<Integer,Object>
消耗的内存少,避免了自动装箱和创建维持映射所需要的对象。SparseArray
采用二分法查找,时间复杂度为O(logn)
,HashMap
的key需要进行hash运算,在没有冲突的情况下是O(1)
,冲突的情况下是O(n)
,但是SparseArray
插入数据时涉及到对象的移动,而HashMap
不需要,所以速度上前者稍慢,选择使要根据情况而定。