
1 SparseArray 简介

SparseArray是Android特有的map类型集合容器,位于android.util包中,通过双数组实现,存储key的数组保持递增,因此可以通过二分查找来进行插入、遍历和删除等操作,如上图所示,与HashMap相比有以不同:
- 空间复杂度低于HashMap,主要有以下原因,1⃣️是key是基本数据类型避免了auto-boxing,2⃣️是不需要封装成数据结构Entry来存储
- 用数组实现,key递增插入数组,通过二分查找实现遍历,不同于HashMap的数组+链表/红黑树,因此时间复杂度低于HashMap
- 为了提升效率,SparseArra在删除元素的时候并未立即删除和挪动后面元素,而是用DELETED进行标示,只有计算size、添加元素需要扩容或者通过index操作时进行gc
- 部分接口未进行index效验,使用时防止出现IndexOutOfBoundsException,比如setValueAt(index, value)、removeAt(index),
2 SparseArray 源码分析
2.1 接口分析
/** 通过获取元素 */
public E get(int key)
public E get(int key, E valueIfKeyNotFound)
/** 删除指定index/key的元素 */
public void delete(int key)
public E removeReturnOld(int key)
public void remove(int key)
public void removeAt(int index)
public void removeAtRange(int index, int size)
/** 添加key-value */
public void put(int key, E value)
/** 替换index位置元素value值 */
public void setValueAt(int index, E value)
/** 存储元素数量 */
public int size()
/** 清空集合 */
public void clear()
/** 在已有元素尾部添加元素key-value,效率高 */
public void append(int key, E value)
/** 获取指定index位置key */
public int keyAt(int index)
/** 获取指定index位置value */
public E valueAt(int index)
/** 获取key的index位置 */
public int indexOfKey(int key)
/** 获取value的index位置 */
public int indexOfValue(E value)
2.2 源码分析
a. 成员变量
public class SparseArray<E> implements Cloneable {
// 标示,标示该位置可以被使用或者需要gc
private static final Object DELETED = new Object();
// 可以gc标示
private boolean mGarbage = false;
// 存储key的数组
private int[] mKeys;
// 存储value的数组
private Object[] mValues;
// 有效大小
private int mSize;
}
b. 构造方法
分配数组大小的ArrayUtils.newUnpaddedObjectArray大小根据文档是不小于size,具体可以参考https://cs.android.com/android/platform/superproject/+/master:art/runtime/mirror/array-alloc-inl.h
public SparseArray() {
// 默认大小为10
this(10);
}
public SparseArray(int initialCapacity) {
// 初始化大小为0时mKeys和mVaules数组初始化为null
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
// 分配数组大小
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
// 当前大小为0
mSize = 0;
}
c. get方法
public E get(int key) {
return get(key, null);
}
public E get(int key, E valueIfKeyNotFound) {
// 根据key二分查找,返回结果是正数表示找到了,负数表示未找到,但是-i是该key的插入位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// i < 0 未找到, DELETED表示该元素存在但被删除了返回默认值
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
d. remove方法
public void delete(int key) {
// 二分查找获取index位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
// 找到了index位置并未被删除设置DELETED标示,同时设置gc使能标示
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
public void removeAt(int index) {
// 删除直接index位置元素,可能出现IndexOutOfBoundsException异常
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
e. put方法
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// key-value已存在,替换value
if (i >= 0) {
mValues[i] = value;
} else {
// ~i就是插入位置
i = ~i;
// 插入位置元素已无效,直接替换key和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);
}
// 自增产在指定index位置插入元素,具体实现见下面
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
assert currentSize <= array.length;
// 当前数组可以存放size+1
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
// 从新创建数组,growSize()自增长二倍,最小值为8
@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;
}
e. append方法
// 效率高,不需要通过二分查找计算插入位置
public void append(int key, E value) {
// 先判断是否可以插入队尾
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
}
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
f. gc方法
什么时候gc?
- 操作方法依赖index位置时,gc前的index位置与传入的index存在不同,需要gc,比如keyAt(int index)
- 计算大小,比如size(),通过gc来获取真实size大小
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];
// 真实数据进行copy
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);
}
3 SparseArray 总结
- DELETED标示位配合gc可以提升效率
- System.arraycopy、ArrayUtils.newUnpaddedArray等JNI方法可以多多使用
- 根据不同的数据结构提供特有的方法来提升效率,比如append,当确定要拆入的key是递增的,插入效率就很高
- SparseBooleanArray,SparseLongArray等实现与SparseArray基本类似,优点是不仅仅key避免了auto-boxing,value也避免了auto-boxing,推荐使用,缺点是没有gc和DELETED标识位,造轮子的机会来了