SparseArray 用来实现 int 类型与 Object 类型的映射,跟普通的 Map 不同,普通 Map 中有更多的空索引,对比 HashMap 来说,稀疏数组实现了更高效的内存使用,因为稀疏数组避免了 int 类型 key 的自动装箱,且稀疏数组每个 value 都不需要使用 Entry 对象来包装。所以在 Android 开发中,我们可以使用 SparseArray 来实现更高效的实现 Map
SparseArray 实现了 Cloneable 接口,说明时支持克隆操作的,下面慢慢分析增删改查以及克隆等操作
/**
* 删除操作时替换对应位置 value 的默认值
*/
private static final Object DELETED = new Object();
/**
* 是否需要回收
*/
private boolean mGarbage = false;
/**
* 存储 key 的数组
*/
private int[] mKeys;
/**
* 存储 value 的数组
*/
private Object[] mValues;
/**
* 当前存储的键值对数量
*/
private int mSize;
SparseArray 中声明了一个 int 类型的数组和一个 Object 类型的数组
/**
* 创建一个空 map 初始容量为 10
*/
public SparseArray() { this(10); }
/**
* 根据指定初始容量创建键值对为空的稀疏数组,并且不会申请额外内存;指定初始容量为 0 时会创建一个轻量级的不需要任何内存分配的稀疏数组
* capacity 容量
*/
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT; // 长度为 0 的 int 类型数组
mValues = EmptyArray.OBJECT; // 长度为 0 的 Object 类型数组
} else {
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;
}
SparseArray 有两个构造函数,默认时创建初始容量为 10 数组,另外一个时可以使用者指定出事容量的数量
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 使用二分法查找对应的 key 在数组中的下标
if (i >= 0) { // 索引大于等于 0 说明原数组中有对应 key
mValues[i] = value; // 则直接 Value 数组中的 value 值为最新的 value
} else { // 索引小于 0 说明原数组中不存在对应的 key
i = ~i; // 取反后得到当前 key 应该在的位置
if (i < mSize && mValues[i] == DELETED) { // 如果数组长度够,并且当前位置已被回收则直接对该位置赋值
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) { // 回收状态为 true 并且内容长度大于等于 key 数组长度
gc(); // 回收,整理数组
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); // 再次使用二分法查找位置
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); // 执行 key 插入到 key 数组对应位置
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); // 执行 value 插入到 value 数组对应位置
mSize++; // 键值对数量加 1
}
}
上面的 put 方法中用到了一个 ContainerHelpers 的 binarySearch 函数,我们先来看一下这个函数的操作,主要是使用二分法查找对应的位置
// ContainerHelpers
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; // 带符号右移,也就是做除以 2,这里是找到中间位置索引的操作
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 的时候,这里是将如果数组中存在 value 时应该在的位置取反后返回
}
接着我们看一下 gc() 方法的操作
//
private void gc() {
int n = mSize; // 键值对数量
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) { // 通过循环将 value 数组中的 DELETED 值移除,并且 DELETED 以后的键跟值都往前补
Object val = values[i];
if (val != DELETED) {
if (i != o) { // 循环第一次执行时 i 和 o 都是 0 ,这种情况不需要处理
keys[o] = keys[i];
values[o] = val;
values[i] = null; // 原位置置空
}
o++;
}
}
mGarbage = false; // 回收状态置为 false
mSize = o; // 将键值对的值更新为实际的键值对数量
}
/**
* GrowingArrayUtils 中定义了 泛型/int/long/boolean 等类型数组在指定位置插入数据的方法
*/
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) { // 不需要扩容
System.arraycopy(array, index, array, index + 1, currentSize - index); // 将对应位置后的内容右移
array[index] = element;
return array;
}
// 需要扩容,
int[] newArray = new int[growSize(currentSize)];
System.arraycopy(array, 0, newArray, 0, index); // 将对应位置前的内容插入
newArray[index] = element; // 将对应位置内容插入
System.arraycopy(array, index, newArray, index + 1, array.length - index); // 将对应位置后的内容插入
return newArray;
}
/**
* GrowingArrayUtils 中定义了 泛型/int/long/boolean 等类型数组在指定位置插入数据的方法,这个方法的作用为,在位置超出数组大小时,计算扩容后数组的新长度
* 旧数组长度小于 4 则设置为 8,否则都是在当前长度基础上扩容一被
*/
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
小结一下,插入操作的工作是,首先在原 key 数组中查找是否有对应的 key,如果找到则直接替换 value 数组中对应下标的值;如果 key 不存在之前的 key 数组,则需要根据是否回收状态进行无用数据回收,然后执行插入,插入过程中如果数组需要扩容还需要执行扩容操作。
由插入操作可以看出,keys 数组中的值为从小到大排列,是一个有序数组
上面分析了插入方法的主要逻辑,接下来继续看 查找/删除 等操作,如果明白了插入操作,下面的就都简单了
public E get(int key) {
return get(key, null);
}
/**
* 根据 key 查找 value ,如果 key 不存在则返回指定的默认值
*/
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];
}
}
可以看到 get() 方法比较简单,首先通过 二分法 找到当前 key 在 key 数组中的位置,如果位置不小于 0 且 value 数组中对应位置的值部位 DELETED,说明找到对应值,直接返回,否则就返回 null。get() 操作是有一个重载方法的,调用者可以传入一个默认值,在查不到对应 key 时则返回默认值。
/**
* 删除操作
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
public E removeReturnOld(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
final E old = (E) mValues[i];
mValues[i] = DELETED;
mGarbage = true;
return old;
}
}
return null;
}
删除操作就更简单了,首先通过二分法查找 key 所在位置,找到就将 value 中对应位置的值设置为 DELETED,在其他操作时通过 gc() 操作执行该位置的回收。removeReturnOld 方法则是会返回删除的 value 值。
同时,SparseArray 也提供了移除指定位置的键值对的方法
/**
* 删除指定位置的值
*/
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
/**
* 以 index 开始,删除之后 size 个值,包含 index 位置,不包含 index + size
*/
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
removeAt(i);
}
}
SparseArray 重写了 clone 方法,科隆时其 keys,values 数组都会克隆成新的数组
@Override
@SuppressWarnings("unchecked")
public SparseArray clone() {
SparseArray clone = null;
try {
clone = (SparseArray) super.clone();
clone.mKeys = mKeys.clone();
clone.mValues = mValues.clone();
} catch (CloneNotSupportedException cnse) {
/* ignore */
}
return clone;
}
首先执行 gc() 操作,然后返回正确的数量
public int size() {
if (mGarbage) {
gc();
}
return mSize;
}
/**
* 返回指定位置的 key
*/
public int keyAt(int index) {
if (mGarbage) {
gc();
}
return mKeys[index];
}
/**
* 返回指定位置的 value
*/
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
/**
* 将对应位置的值设置为指定 value
*/
public void setValueAt(int index, E value) {
if (mGarbage) {
gc();
}
mValues[index] = value;
}
/**
* 返回指定位置的 key
*/
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
/**
* 返回指定位置的 value
*/
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++) {
if (mValues[i] == value) {
return i;
}
}
return -1;
}
/**
* 返回指定 value 所在位置,只不过 value 相等的判断使用 equals 方法
*/
public int indexOfValueByValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++) {
if (value == null) {
if (mValues[i] == null) {
return i;
}
} else {
if (value.equals(mValues[i])) {
return i;
}
}
}
return -1;
}
/**
* 移除所有键值对
*/
public void clear() {
int n = mSize;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
values[i] = null;
}
mSize = 0;
mGarbage = false;
}
/**
* 插入键值对,优化插入的 key 大于所有现在已有 key 的情况,由于 key 数组是从大到小的有序数组,所以这种情况下不需要二分法查找位置,优化了性能
*/
public void append(int key, E value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) { // 如果不是大于现在已有的 key ,则按照正常方式插入
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) { // 执行回收 DELETED 的 value
gc();
}
mKeys = GrowingArrayUtils.append(mKeys, mSize, key); // 直接向后插入
mValues = GrowingArrayUtils.append(mValues, mSize, value); // 直接向后插入
mSize++;
}
/**
* 打印所有的 key value
*/
public String toString() {
if (size() <= 0) {
return "{}";
}
StringBuilder buffer = new StringBuilder(mSize * 28);
buffer.append('{');
for (int i=0; iif (i > 0) {
buffer.append(", ");
}
int key = keyAt(i);
buffer.append(key);
buffer.append('=');
Object value = valueAt(i);
if (value != this) {
buffer.append(value);
} else {
buffer.append("(this Map)");
}
}
buffer.append('}');
return buffer.toString();
}
SparseArray 的代码非常少,只有 450 行左右,并且特别易于理解。但 SparseArray 要比 HashMap 更加高效,在 Android 手机中,如果 key 为 int 类型的 Map 数据,最好使用 SparseArray 来实现。