一、前言
SparseArray 是 Android 在 Android SdK 为我们提供的一个基础的数据结构,其功能类似于 HashMap。与 HashMap 不同的是它的 Key 只能是 int 值,不能是其他的类型。
二、代码分析
1. demo 及其简析
首先也还是先通过 demo 来看一看 SparseArray 的基本使用方法,主要就是插入方法以及遍历方法。这也会后面的代码分析打下一个基础。
SparseArray
上面代码先是 new 了一个 SparseArray,注意声明时只能指定 value 的类型,而 key 是固定为 int 的。然后再往里面添加 key 以及 value。这里注意一下的是 key 为 0 的情况插入了 2 次。遍历时,是先通过顺序的下标取出 key ,再通过 keyAt 来 get 出 value。当然也可以一步到位通过 valueAt() 直接获取到 value。然后这个 demo 的执行结果如下。
sparseArraySample: i = 0;value = chongfude
sparseArraySample: i = 1;value = fsdfd
sparseArraySample: i = 2;value = fjdslfjdk
sparseArraySample: i = 3;value = 1
sparseArraySample: i = 4;value = true
sparseArraySample: i = 5;value = java.lang.Object@b67a0fa
sparseArraySample: i = 6;value = 42fsjfldk
sparseArraySample: i = 7;value = jfslfjdkfj
然后通过 Debug 来看一看在内存中,SparseArray 实际是如何存储的。如下图分别是 key 与 value 在内存中的形式。可以看出 keys 和 values 的大小都为 13,而且 keys 的值是按从小到大顺序排列的。
2.源码分析
下面是 SparseArray 的类图结构,可以看到其属性非常的少,也可以看出其分别用了数组 int[] 和 object[] 来存储 key 以及 value。
- SparseArray 初始化
SparseArray 的初始化也就是它的构造方法。
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;
}
其有 2 个构造方法,带参与不带参。当然,这个参数就是指定数组初始大小,也就是 SparseArray 的初始容量。而不带参数则默认指定数组大小为 10 个。
- 插入数据 put() 方法
public void put(int key, E value) {
// 1.先进行二分查找
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 2. 如果找到了,则 i 必大于等于 0
if (i >= 0) {
mValues[i] = value;
} else {
// 3. 没找到,则找一个正确的位置再插入
i = ~i;
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
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++;
}
}
这里调用了很多外部的方法以及内部的方法。首先是 ContainerHelpers#binarySearch() 的二分查找算法。
//This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
// 高位+低位之各除以 2,写成右移,即通过位运算替代除法以提高运算效率
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
}
}
//若没找到,则lo是value应该插入的位置,是一个正数。对这个正数去反,返回负数回去
return ~lo; // value not present
}
二分查找的分析属于基础内容,在注释中了。回到 put() 方法首先通过二分查找算法从当前 keys 中查找是否已经存在相同的 key 了,如果存在则会返回大于等于 0 的下标,然后接下来就会将原下标下的 values 中的旧value 替换成新的 value 值,即发生了覆盖。
那如果没有找到,那么将 i 取反就是要插入的位置了,这一结论正好来自 binarySearch() 的返回结果。可以看到其最后如果没有找到的话,就会返回 lo 的取反数。那么这里再把它取反过来那就是 lo 了。
这里如果 i 是在大小 mSizes 的范围内的,且其对应的 values[i] 又刚是被标记为删除的对象,那么就可以复用这个对象,否则就还是要依当前的 i 值进一步寻找要插入的位置,再插入相应的 value。
在插入之前,如果由于之前进行过 delete(),remoeAt() 以及 removeReturnOld() 中的某一个方法,那就可能要进行 gc() 操作。当然,这里不是指的内存的 gc()。
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;
}
通过代码很容易分析得出,这里的 gc ,实际就是压缩存储,简单点说就是让元素挨得紧一点。
而 gc() 完之后,下标 i 可能会发生变化,因此需要重新查找一次,以得到一个新的下标 i。
最后就是通过 GrowingArrayUtils.insert() 来进行 key 和 value 的插入。这个 insert() 根据数组类型重载了多个,这里只分析 int[] 类型的即可。
public static int[] insert(int[] array, int currentSize, int index, int element) {
//确认 当前集合长度 小于等于 array数组长度
assert currentSize <= array.length;
//不需要扩容
if (currentSize + 1 <= array.length) {
//将array数组内从 index 移到 index + 1,共移了 currentSize - index 个,即从index开始后移一位,那么就留出 index 的位置来插入新的值。
System.arraycopy(array, index, array, index + 1, currentSize - index);
//在index处插入新的值
array[index] = element;
return array;
}
//需要扩容,构建新的数组,新的数组大小由growSize() 计算得到
int[] newArray = new int[growSize(currentSize)];
//这里再分 3 段赋值。首先将原数组中 index 之前的数据复制到新数组中
System.arraycopy(array, 0, newArray, 0, index);
//然后在index处插入新的值
newArray[index] = element;
//最后将原数组中 index 及其之后的数据赋值到新数组中
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
上面的算法中,如果不需要扩容则直接进行移位以留出空位来插入新的值,如果需要扩容则先扩容,然后根据要插入的位置 index,分三段数据复制到新的数组中。这里再看看 growSize() 是如何进行扩容 size 的计算的。
public static int growSize(int currentSize) {
//如果当前size 小于等于4,则返回8, 否则返回当前size的两倍
return currentSize <= 4 ? 8 : currentSize * 2;
}
代码相对简单,当前 size 小于等于 4 则为 8 ,否则为 2 倍大小。
- get() 方法
public E get(int key) {
return get(key, null);
}
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 来返回对应的 value,前面在分析 put() 的时候已经分析过了二分查找。那么这里如果找到了,就会通过下标直接从 mValues[] 中返回。
- delete() 方法
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
delete() 也非常简单,通过二分查找算法定位到下标,然后将对应的 value 标记为 DELETE,并且标记需要进行 gc 了。这里需要注意的是被标记为 DELETE 的 value 不会在 gc 中被移除掉,而只会被覆盖掉,从而提高了插入的效率。
三、总结
文章对 SparseArray 进行了简要的分析,文章也只对主要的几个方法进行了分析,其他没有分析到的方法在这个基础上再进行分析相信也是很简单的。而总结下来几点是:
- 其内部主要通过 2 个数组来存储 key 和 value,分别是 int[] 和 Object[]。这也限定了其 key 只能为 int 类型,且 key 不能重复,否则会发生覆盖。
- 一切操作都是基于二分查找算法,将 key 以升序的方法 “紧凑” 的排列在一起,从而提高内存的利用率以及访问的效率。相比较 HashMap 而言,这是典型的时间换空间的策略。
- 删除操作并不是真的删除,而只是标记为 DELETE,以便下次能够直接复用。
最后,感谢你能读到并读完此文章。受限于作者水平有限,如果存在错误或者疑问都欢迎留言讨论。如果我的分享能够帮助到你,也请记得帮忙点个赞吧,鼓励我继续写下去,谢谢。