一.前言
SparseArray 和 ArrayMap 是 Android 系统 api 中用于存储键值对数据的集合,相比于 java 集合的 HashMap ,SparseArray 和 ArrayMap 在某些场景下能够以时间换空间策略,带来内存上效率的提升,因此更适合移动设备。
二.SparseArray
1.简介
1 | /** |
在源码中对 SparseArray 的介绍就如上面的注释,从中就可以知道以下几点:
(1).
SparseArray 存储 int -> Objects 映射关系(不是 Integer ),类似的类还有 SparseBooleanArray ,SparseIntArray , SparseLongArray ,LongSparseArray。对应的映射关系如下:
类 | 映射关系 |
---|---|
SparseArray | int -> Objects |
SparseBooleanArray | int -> boolean |
SparseIntArray | int -> int |
SparseLongArray | int -> long |
LongSparseArray | long -> Objects |
(2).
不像其他的数组结构下标是连续的,它能够允许某些下标不存在,因此称为 SparseArray (稀疏数组),因为避免了自动装拆箱,且不用创建其他的实体(HashMap 需要创建 Entry),因此在内存上有更高的效率。
(3).
与 HashMap 中使用 hash 值定位下标的方式不同,SparseArray 使用的是二分查找的方式,因此当有大量数据的时候,SparseArray 的速度将明显慢于 HashMap
(4).
在删除的元素的时候,SparseArray 不是立即对数组进行压缩(去除数组中为 空的元素),而是使用一个标志,对删除的位置进行标志,只有在数组扩容或者 键值对数量和某个元素需要恢复的时候,才会统一进行压缩。
2.源码
1 | public class SparseArray<E> implements Cloneable { |
1.put 方法
1 | public void put(int key, E value) { |
put 方法首先要就要查找对应的需要插入的位置,因为 SparseArray 是一个稀疏数组即可能有这样的情况 (0 ,1 ,3 ,5,6) 因此首先就要找到对应的插入位置。二分查找对应的算法如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static 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,而不是简单的 -1, 其实 ~lo 对应的就是需要插入的位置,比如 (0 ,1 ,3 ,5,6)查找 2,返回的就是 -2 ,取反可以将正数转为负数,如果是负数就说明查找失败,比如返回 -2 就说明查找失败,插入的位置为 2 .
接着回到 put 方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public void put(int key, E value) {
//使用二分查找
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//i>0
//说明查找成功,直接更新
if (i >= 0) {
mValues[i] = value;
//查找失败的话
} else {
//取反获取对应的插入的位置
i = ~i;
//如果下标 小于 元素数量并且之前被标志位删除的话
//重新赋值即可
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);
}
//对数组进行扩容 并复制
//GrowingArrayUtils.insert 使用的是 System.arraycopy
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
gc 压缩数组的源码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27private 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);
}
下一次进行 arrayCopy 的时候多余的 null 就不会进行复制,延迟删除机并一次性压缩就提高了效率。
2.append 方法
put 方法是在数组中插入元素,那么 append 对应的就是在数组末尾追加元素1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public void append(int key, E value) {
//如果 key 小于 存在的key
//就使用 put 进行插入
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
//判断是否需要 gc
if (mGarbage && mSize >= mKeys.length) {
gc();
}
//调用 GrowingArrayUtils.append 在数组末尾追加元素
//内部也是使用 System.arrayCopy
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
3.get 方法
get 方法 方法就相对简单了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public 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.
*/
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果查找失败或者元素已经被删除
//就返回 null 或者默认的值
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
与 HashMap 的对比
优点
- 频繁的插入删除操作效率高,因为延迟删除标志 DELETE
- 通过 gc 函数来清除,内存利用率高
- 不用自动拆箱
缺点
- 存取因二分查找的时间复杂度 O(log n),数据较多的情况下,效率没有 HashMap 高
- key 只能是 int 或者 long
因此 SparseArray 应用场景为 数据较少,并且
存取的 value 为指定类型的,比如 boolean、int、long,可以避免自动装箱和拆箱问题。
三.ArrayMap
1 | /** |
1.简介
在源码中对 ArrayMap 的介绍主要关注在第一段,后面的前面关于 SparseArray 的介绍基本一样,这里就不再赘述,只关注第一段。
ArrayMap 是一个简单的集合用于存储 键值对 。它比传统的键值对集合 HashMap 要在内存上更有效率,因为它维持的是一个数组的数据结构,一个整型数组用于保存每一项的 hash 值,一个 键值数组保存 键值对。因此这就使得 ArrayMap 不用创建额外的实体(HashMap 中需要创建 Entry ),在扩容的时候只需要复制就行了,不用重新 hash .
对应的数据结构如图。
2.源码
1 | private static final boolean DEBUG = false; |
构造函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public ArrayMap() {
this(0, false);
}
public ArrayMap(int capacity) {
this(capacity, false);
}
/** {@hide} */
public ArrayMap(int capacity, boolean identityHashCode) {
mIdentityHashCode = identityHashCode;
// If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS
// instance instead of the usual EmptyArray.INT. The reference
// is checked later to see if the array is allowed to grow.
if (capacity < 0) {
mHashes = EMPTY_IMMUTABLE_INTS;
mArray = EmptyArray.OBJECT;
} else if (capacity == 0) {
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
} else {
allocArrays(capacity);
}
mSize = 0;
}
public ArrayMap(ArrayMap<K, V> map) {
this();
if (map != null) {
putAll(map);
}
}
无论是哪个构造函数,最后都会调用 两个参数的 ArrayMap(int capacity, boolean identityHashCode)
- identityHashCode,如果 true 则 hash 值是默认的 hashCode System.identityHashCode(key) 不管 key 的hashCode 是否被重写 。否则就是 key.hashCode() 这个 hashCode 就有可能使重写的情况。
注意最后 如果设置的初始大小大于 0 ,则会调用 allocArrays 。allocArrays 这个方法就是从缓存的数组获取数组,避免重复的创建,既然有获取缓存当然有添加缓存,相应的添加缓存的方法就是 freeArrays 。
首先看缓存的数组是如何添加的
1 | static Object[] mBaseCache; |
注意到能够添加到缓存的条件只有两种情况 hash 数组的长度为 4 或者 8
添加的情况如图:
可以看出这是一个类似链表的结构,相应的获取缓存就是取出链表的头节点,并设置新的头节点
1 | //获取缓存的方法 |
put 方法
1 | public V put(K key, V value) { |
通过 hash 获取对应下标的过程和 之前的有点不同,因为 hashCode 的不唯一性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44int indexOf(Object key, int hash) {
final int N = mSize;
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
//先使用二分进行查找
int index = binarySearchHashes(mHashes, N, hash);
// If the hash code wasn't found, then we have no entry for this key.
if (index < 0) {
return index;
}
//如果查找成功,并且对应的 key 是相同的,即 hashCode 相同且 equals 返回 true
// If the key at the returned index matches, that's what we want.
if (key.equals(mArray[index<<1])) {
return index;
}
//前面的 equals 返回 false 则继续查找
//先从index 往 数组后面找
// Search for a matching key after the index.
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
if (key.equals(mArray[end << 1])) return end;
}
//再从 index 往数组前面找
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
if (key.equals(mArray[i << 1])) return i;
}
// Key not found -- return negative value indicating where a
// new entry for this key should go. We use the end of the
// hash chain to reduce the number of array entries that will
// need to be copied when inserting.
//查找失败则返回插入的位置 ,相同的 hashCode
//插入是插在后面。
return ~end;
}
对于get 方法就相对简单,这里就不做分析。
与 HashMap 的对比
优点
- 在数据量少时,内存利用率高,不用创建新的实体 Entry
- 迭代效率高,使用数组下标,相比于HashMap迭代使用迭代器,要快
缺点 - 存取因 二分查找的 O(log n ) 复杂度高,效率低
- ArrayMap 没有实现 Serializable,不利于在 Android 中借助 Bundle 传输。
因此 适用场景元素数量较少但是查询多,插入数据和删除数据不频繁的情况。