提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
因为泛型擦除的原因基本数据类型不能作为泛型参数,如果需要就得使用它们的包装类就带来了装箱拆箱的开销,为了解决这个问题才有了 SparseArray 相关的几个类,SparseArray 比 HashMap 要慢一些是时间换空间的实现。
提示:基于 Android SDK 28
一、SparseArray 是什么 ?
SparseArray 是 Android 平台用来存储 key 为 int ,value 为 Object 的数据结构,它的内部实现是两个数组,一个 int 数组保存有序的 key 一个 Object 数组保存 value 并且 key 与 value 所在数组中的索引位置一致。另外为了提高性能对删除做了优化,当删除一个数据时并不是立即从 value 中删除而是先标记为删除,当对应的位置再次存储数据时可以直接复用这个空间避免每次删除添加元素时都移动元素
二、源码分析
1.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;
}
}
}
通过二分查找拿到指定 key 在 key 数组中的位置,如果 i 大于 0 说明指定 key 存在,如果指定位置的 value 没有标记为删除则标记为删除并且把 mGarbage 标记为 true 表示可以进行垃圾回收
2.put
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
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];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid;
}
}
return ~lo;
}
binarySearch 方法的作用是如果指定的 key 存在则返回指定 key 的位置否则返回 key 应该插入位置的取反值。如果 i 大于 0 说明指定的位置已经存储了值直接覆盖 value,否则对 i 取反得到应该插入的位置,如果这个位置是删除标记则直接覆盖 key 和 value ,否则判断如果数组有已经满了并且数组已经满了则调用 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 方法的作用是移动有效数据把标记为删除的数据覆盖,使 “稀疏数组” 变成连续数组并且重新记录 mSize 的值并且把 mGarbage 置为 false,进行了 gc 操作之后因为数据变化再次使用二分查找拿到 key 应该插入的位置,以为 key 可能不存在所以直接取反再把 key value 插入到数组中
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 = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
insert 做了真正的数据存储的工作,判断是否需要扩容把元素存储到指定位置,扩容规则也大致是当前容量的 2 倍
3.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++;
}
有一种情况,要插入的 key 比 key 数组中所有的元素都大,append 对这种场景做了优化直接调用 GrowingArrayUtils.append 插入元素而不需要进行二分查找并且减少了移动数组的操作,所以应优先使用 append 添加数据
4.get
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 没有标记为删除则返回对应位置的 value 否则返回指定的默认值
5.其他
- size 返回有效元素的个数
- clear 清除所有元素 mSize 置为 0 mGarbage 置为 false
- indexOfKey 返回指定 key 在数组中的索引找不到则返回负值
- indexOfValue 返回指定 value 在数组中的索引找不到则返回 -1(使用 == 比较元素的值)
- indexOfValueByValue 返回指定 value 在数组中的索引找不到则返回 -1(使用 equals 比较元素的值)
- keyAt 获取指定位置的 key
- valueAt 获取指定位置的 value
- setValueAt 设置指定位置的 value 的值
- remove 与 delete 一致
- removeAt 将指定位置的值标记为删除
- removeAtRange 将指定区域的值标记为删除
三、最后
SparseArray 还有几个其他变体
- SparseIntArray 是 key 为 int 类型 value 为 int 类型容器
- SparseLongArray 是 key 为 int 类型 value 为 long 类型容器
- SparseBooleanArray 是 key 为 int 类型 value 为 boolean 类型容器
- LongSparseArray 是 key 为 long 类型 value 为 Object 类型的容器
SparseArray 的使用场景
- 数据量不大(千以内)
- 空间比时间重要
- 需要使用 key 为 int 类型的容器
参考
面试必备:SparseArray源码解析 这一次,彻底搞懂SparseArray实现原理 面试还在问 SparseArray?记住 3 句话让你临时把佛脚抱好! Android数据容器之SparseArray 内存优化:充满矛盾的SparseArray
|