SparseArray
SparseArray 是 Android 中特有的 key-value 存储结构,它类似于 HashMap 和 ArrayMap,不同的是 SparseArray 的 key 值 只能是 int 类型。除了 SparseArray 之外还有 SparseBooleanArray 、SparseIntArray 、SparseLongArray 和 SparseSetArray,顾名思义他们只是将存储的 value 具体化而已。下面我们只分析 SparseArray ,其他的实现几乎一致
本篇文章我们就来解析下 SparseArray ,文中 SparseArray 基于 Android 10
SparseArray 的源码其实并不多,大概就500行
二分查找
在源码解析之前,我们需要先弄懂二分查找。SparseArray 在插入、查找和删除等都是通过二分查找去实现的,调用了 ContainerHelpers.binarySearch 方法,ArrayMap 中也是通过这个方法查找的,我们就来看下 ContainerHelpers.binarySearch 怎么实现的吧
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];
// 中间值小于目标值,从(mid,hi]区间查找
if (midVal < value) {
lo = mid + 1;
// 中间值大于目标值,从[lo,mid)区间查找
} else if (midVal > value) {
hi = mid - 1;
// 这里就是找到了目标值了,然后mid索引
} else {
return mid; // value found
}
}
// 数组中没有目标值,返回~lo,可以通过负数判断是否存在目标值,如果需要将目标值存储,那么目标值所在的位置就是lo
return ~lo; // value not present
}
首先我们要知道二分查找是对于有序的数组,ContainerHelpers.binarySearch 传入的数组是正序排序的(从小到大)。这里就用示例图展示下二分查找
假设我们需要从数组[0,1,2,4,8,9,13,16]查找13所在的位置
步骤一: 首次查找时,我们将 lo 置为0,hi 置为7,那么mid就等于3
步骤二: 此时由于midVal=4<13,我们需要从(mid,hi]之间查找,将lo置为4,那么mids就等于5
步骤三: 此时由于midVal=9<13,我们需要从(mid,hi]之间查找,将lo置为6,那么mids就等于6
步骤四: 此时midVal=13,刚好等于我们的目标值13,所以我们就找到了13所在的位置是6
假如我们需要查找的元素是12呢?
我们还是用示例图分析下,步骤一二三都不变,步骤三 之后,我们的 lo = mids = 6,hi = 7,但是此时midVal = 13 > 12,所以我们需要从[lo,mid)之间查找,随后我们会将hi置为5,如下图所示
很显然 lo > hi,不满足条件 while (lo <= hi) 循环了,然后我们返回了 ~6。这个~6是什么概念?假如我们需要将12存进数组,是不是需要存放在9和13的中间,那么~(~6)不就是我们所要存储的位置吗?
想必到这里大家就知道了 ContainerHelpers.binarySearch 的作用了吧,它不光是可以查找已经存在元素的索引,还能将不存在的元素索引计算出来方便存储
构造函数
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;
}
可以看到 SparseArray 的默认长度是10,构造函数里面会实例 mKeys 和 mValues,看下 mKeys 和 mValues 是啥
private int[] mKeys;
private Object[] mValues;
mKeys 是 int 类型的数组,mValues 是 object 类型的数组,想必大家都知道了 mKeys 就是存放的 key 值,mValues 存放的是 value,并且 mKeys 是正序排序的。
存放数据
往 SparseArray 中存放数据时,调用 put 、 append 方法,类似这样
val sparseArray = SparseArray<String>()
sparseArray.put(1, "1")
sparseArray.append(2, "2")
append 方法其实内部也是调用了 put 方法这里就不分析 append 了,那么我们就来看下 put 方法是如何实现的吧
public void put(int key, E value) {
// 二分查找key所在的索引位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 数组中存在该key值,覆盖该key值
if (i >= 0) {
mValues[i] = value;
} else {
// 数组中不存在该key值,获取存放key值的位置
i = ~i;
// i < 数据容量并且mValues[i]是DELETED,将key和value存在i索引上
// 为什么要设置成DELETED?如果不设置成DELETED,那么往i位置添加元素,那么i位置之后的元素都要往后移,性能又是比较大的损耗
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
// mValues中存在DELETED并且mKeys存满了
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++;
}
}
-
通过二分查找获取
key是否存在mKeys中,如果存在那么将mValues对应索引的值置为value,相当于发生了替换。 -
假如
key不在mKeys中,将 i 取反获取key存放的索引位置。如果i < mSize并且mValues[i]又是被标记成了 DELETED(调用了 delete 或 remove 方法),就把key和value存放在 i 索引上 -
假如
mValues中存在 DELETED 并且 数据容量大于mKeys的长度时,调用gc方法移除 DELETED,调用gc之后mKeys和mValues的排序都发生了变化,所以要通过二分查找重新获取元素保存的位置,最后通过GrowingArrayUtils.insert将key和value分别插入mKeys和mValues中
我们再来看下 GrowingArrayUtils.insert 是如何插入元素的
// com.android.internal.util.GrowingArrayUtils.java
public static int[] insert(int[] array, int currentSize, int index, int element) {
// 断言数组还未越界
assert currentSize <= array.length;
// 假如可以插入元素,那么只需要将index及后面的元素往后挪一位,然后往index位置插入元素即可
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
// 将数组index位置的元素置为element
array[index] = element;
return array;
}
// 原数组无法插入元素(容量不够了),根据growSize的长度创建新的数组
int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
// 将原数组[0,index)位置的元素拷贝到新数组
System.arraycopy(array, 0, newArray, 0, index);
// 将新数组index位置的元素置为element
newArray[index] = element;
// 最后将原数组[index,array.length)的元素全部拷贝到新数组index+1之后的位置上
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
上面的注释写得很清楚了,相信大家都能看得懂,其实就是扩容与不扩容的问题。再来看下 growSize 的大小是如何计算的
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
当前 Size <= 4 时恒定大小为8,否则就是 Size 的两倍了。
查找数据
SparseArray 提供了 get 方法供我们获取数据, get 方法有两个重载函数,归根到底都是调用了两个参数的。
public E get(int key) {
return get(key, null);
}
// 默认值
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 查找不到该key值或者该位置上是DELETED时,返回默认值
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
// 返回该位置的元素
return (E) mValues[i];
}
}
两个参数的 get 方法可以设置默认值,当查找不到该 key 值返回默认值,一个参数的 get 方法默认值是 null
删除数据
SparseArray 中提供了 两种删除方式。
- 根据
key值删除,remove方法和delete方法 - 根据索引位置删除,
removeAt方法和removeAtRange方法
先来看一下 根据 key 值删除的实现
public void delete(int key) {
// 通过二分查找获取key值的位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 存在该key值
if (i >= 0) {
// 该位置上不是DELETED
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
// mGarbage置为true,为调用gc方法提供条件
mGarbage = true;
}
}
}
// 直接调用了delete方法
public void remove(int key) {
delete(key);
}
其实很简单,通过二分查找获取 key 值的位置,如果 key 值是存在的并且该位置不是 DELETED,那么将该位置置为 DELETED
再来看下根据索引位置删除
public void removeAt(int index) {
// index超过了存放数据的长度了,抛出数组越界
if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
// The array might be slightly bigger than mSize, in which case, indexing won't fail.
// Check if exception should be thrown outside of the critical path.
throw new ArrayIndexOutOfBoundsException(index);
}
if (mValues[index] != DELETED) {
// 标记为DELETED
mValues[index] = DELETED;
mGarbage = true;
}
}
// 范围内删除,可以删除多个数据
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
// 其实还是通过removeAt删除
removeAt(i);
}
}
这两种方式的实现逻辑其实是一样的,都是将该位置上的对象标记为DELETED
gc
这里的 gc 不是我们常说的 jvm gc,而是 SparseArray 中的 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];
// 判断DELETED
if (val != DELETED) {
if (i != o) {
// o位置上置为keys[i]
keys[o] = keys[i];
// o位置上置为values[i]
values[o] = val;
// i位置置为null
values[i] = null;
}
o++;
}
}
// 完成之后置为false,避免再次gc
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
这里用示例图演示下 gc 方法, SparseArray 的长度默认为10,并且 mValues 中存在两个DELETED
第一次出现DELETED的是3索引上,那么 mKeys 和 mValues 有如下变化
第二次出现DELETED的是7索引上,那么此时 mKeys 和 mValues 也会变化,如下所示
经过 gc 之后,mKeys 和 mValues 都发生了变化,mValues 会将非DELETED整体往前迁移并且将末尾的元素都置为空,mKeys 由于要对应上 mValues 的变化 对应位置上的 key 值也要向前迁移。
总结
上来就是本篇文章的内容了,主要针对 SparseArray 的存储、删除进行了详解,当然了还有一些其他方法大家可以自行查阅。这里对 SparseArray 做一个小结
-
SparseArray存放的key值只能是int类型,因为存放key值的数组有序排序并且是升序的 -
所有的操作都是基于二分查找,当查找到
key值时返回该位置的索引,否则返回该位置索引的逻辑非 -
删除操作中只是将该位置上的元素标记为DELETED,并没有移除该位置的元素,当
gc时才会将非 DELETED对象往前迁移