Android数据结构解析系列:
Android中的数据结构解析(一)ArrayList、LinkedList、Vector
Android中的数据结构解析(二)HashSet、LinkedHashSet、TreeSet
Android中的数据结构解析(三)HashMap、HashTable、TreeMap
HashMap是Java和Android开发中非常常用的数据结构,相信大多数人都对它非常熟悉。然而,作为一名Android开发,仅仅只知道使用HashMap是不够的。在很多情况下,HashMap对内存的消耗较大,从而影响移动设备的性能。因此,为了内存优化和性能提升,Android提供了用来替代HashMap的api:SparseArray和ArrayMap。
SparseArray
先来看一下SparseArray的介绍:
/**
* 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.
*/
可以看到,SparseArray只能存储key为int类型的数据。所以,可以使用SparseArray<Object>来替代HashMap<Integer, Object>。它在性能上优于HashMap的原因有两点:
1.避免了int转为Integer时的自动装箱
在上一节中讲过,HashMap是通过key值的hashCode方法来确定元素存放的位置的。所以,当key值是基本类型int时,会自动装箱成Integer对象。装箱过程中会创建对象,这个动作是很消耗内存的。而SparseArray避免了装箱的这个动作,从而提升了性能。
2.避免了额外的Entry对象
HashMap的底层实现是数组+链表的数据结构。HashMap的每一条数据都会用一个Entry对象进行记录:
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
除了key和value之外,还记录了hash值,和到下一个Entry对象的指针。而SparseArray只需要一个key数组和一个value数组存放数据(下面会讲到),不需要额外的Entry对象,从而节省了内存。
接下来看一下SparseArray的具体实现和常用方法:
构造方法:
private int[] mKeys;
private Object[] mValues;
private int mSize;
……
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;
}
key和value分别存放在mKeys和mValues这两个数组中。调用无参构造方法时,默认初始化数组的长度为10。
再来看一下SparseArray中增删改查方法的实现:
public void put(int key, E value) {
//二分查找 key是否存在
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
//如果key存在则直接替换value
mValues[i] = value;
} else {
//key不存在 对i取反(~)就是应该插入的位置
i = ~i;
//如果插入的位置刚好是被删除过的元素,则直接将删除掉的value替换为要插入的value
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果曾经删除过元素且没有进行过gc,进行一次gc操作
if (mGarbage && mSize >= mKeys.length) {
gc();
// gc后数组下标可能会改变 所以重新查找一遍
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//插入的操作
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
SparseArray存储的元素都是按元素的key值从小到大排列好的。put方法的第一行,先调用了ContainerHelpers.binarySearch(mKeys, mSize, key)查找key是否存在。看一下这个方法的源码:
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
}
就是一个简单的二分查找算法,如果找到key就返回key的位置,如果找不到就返回key应该插入位置的取反。二分查找是SparseArray的核心算法,可以快速查找到key的位置。
再回到put方法。在对key进行二分查找之后,如果key存在则直接替换掉相应的value,如果key不存在则在对应的位置插入key和value。
看到这里可能会有人迷糊了:中间那两个莫名其妙的if是干嘛的呢?好像删掉它们也不会有什么问题啊?这里就涉及到了SparseArray的一个巧妙的优化。我们知道,对数组进行插入和删除操作的代价是非常大的,例如在数组中间删除一个元素,那么这个元素后面的所有元素都要向前移动一个位置,如果删除操作多了,会极大的降低性能。
那么SparseArray的删除操作是怎么做的呢?来看一下delete方法:
private static final Object DELETED = new Object();
……
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
首先还是用二分查找找到要删除key在数组中的位置,查找到之后,并不立即删除这个元素,而是将相应的value标记为DELETED。这样如果之后有插入的key和这个删除掉的key相同时,直接替换掉value,这样就省去了一次删除和一次插入操作,提升了性能。在执行垃圾回收gc方法时,会一次性将所有标记为DELETED的元素全部删除,使原本要执行多次的删除操作减少到了一次:
private void gc() {
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;
}
看到这里,你应该不会再对put里那两个奇怪的if感到困惑了。再看一下get方法:
public E get(int key) {
return get(key, null);
}
@SuppressWarnings("unchecked")
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];
}
}
很简单没有什么难点了
要注意的一点是,由于SparseArray使用了二分查找,并且每次插入/删除等操作时都需要查找一次,所以在数据量大的时候,虽然节省了内存,但效率肯定是不如HashMap的。这时候的性能提升就很不明显了。
所以,使用SparseArray替换HashMap适合于以下两个条件:
1.key为int类型。(long类型也可以,使用LongSparseArray)
2.数据量不大。(千级左右)
ArrayMap
相对于SparseArray,ArrayMap不限制key的类型,任何的HashMap都可以用ArrayMap替换。ArrayMap的内部存储方式依然是两个数组:
int[] mHashes;
Object[] mArray;
不过不同的是,ArrayMap的key和value全部存储在mArray数组中。存储形式可以表示为:[key1, value1, key2, value2, key3, value3……]
那么mHashes数组中存储的自然就是每一个元素的hash值,查找元素时,先计算key转换过后的hash值,在mHashes数组中找到对应的hash值的位置,然后就可以在mArray数组中找到对应的key和value了。
ArrayMap在性能上优于HashMap的原因有两点:
1.和SparseArray一样,由于使用了两个数组存储数据,不再需要额外创建Entry对象,因此节省了内存空间。
2.扩容时,HashMap会重新new一个长度为2倍的容器返回。而ArrayMap则是调用System.arraycopy方法copy数据,减少了内存开销。
ArrayMap在mHashes数组中查找hash值时,同样用的是二分查找,所以在数据量较大时效率较低。因此ArrayMap一样适合于在数据量不大时使用。
总结
综上所述,在数据量在千级或千级以内时,使用SparseArray和ArrayMap可以减少内存的消耗,提升性能。其中,在key值为int或long型时,使用SparseArray(LongSparseArray);key值为其他对象时,使用ArrayMap。