源码基于 API 25
参考文章:
1、面试必备:SparseArray源码解析
2、谈谈源码中的SparseArray
概括的说,SparseArray
是用于在 Android 平台上替代 HashMap 的数据结构。
更具体的说,是用于替代 key 为 int 类型,value 为 Object 类型的 HashMap。
和 ArrayMap 类似,它的实现相比于 HashMap 更加节省空间,而且由于 key 指定为 int 类型,也可以节省 int-Integer 的装箱拆箱操作带来的性能消耗。
它仅仅实现了 implements Cloneable 接口,所以使用时不能用 Map 作为声明类型来使用。
它也是线程不安全的,允许 value 为 null。
从原理上说,
它的内部实现也是基于两个数组。
一个int[]
数组mKeys
,用于保存每个 item 的 key,key 本身就是 int 类型,所以可以理解 hashCode 值就是 key 的值;一个Object[]
数组mValues
,保存 value 。容量和key数组的一样。
它扩容的更合适,扩容时只需要数组拷贝工作,不需要重建哈希表。
同样它不适合大容量的数据存储。存储大量数据时,它的性能将退化至少50%。
比传统的HashMap时间效率低。
因为其会对 key 从小到大排序,使用二分法查询 key 对应在数组中的下标。
在添加、删除、查找数据的时候都是先使用二分查找法得到相应的 index,然后通过 index 来进行添加、查找、删除等操作。
所以其是按照 key 的大小排序存储的。
另外,SparseArray 为了提升性能,在删除操作时做了一些优化:
当删除一个元素时,并不是立即从 value 数组中删除它,而是将其在 value 数组中标记为已删除。这样当存储相同的 key 的 value 时,可以直接覆盖这个空间。
如果该空间没有被重用,随后将在合适的时机里执行 gc(垃圾收集)操作,将数组压缩,以免浪费空间。
适用场景:
1、数据量不大(千以内)
2、空间比时间重要
3、需要使用Map,且key为int类型。
引用自:面试必备:SparseArray源码解析
private static final Object DELETED = new Object();
// 是否需要执行 gc() 的标记变量
private boolean mGarbage = false;
// key 数组
private int[] mKeys;
// value 数组
private Object[] mValues;
private int mSize;
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;
}
public void put(int key, E value) {
// 通过二分查找找出 key 在 mKeys 中对应的下标
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 如果 >=0,即 key 在 mKeys 中存在,且 i 即为对应下标,
// 直接覆盖 mValues[i] 中的值
if (i >= 0) {
mValues[i] = value;
} else {
// 否则 ~i 即为 key-value 要插入的位置
i = ~i;
// 如果i没有越界,且对应位置是已删除的标记,则复用这个空间,
// 复用后直接返回
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
// 如果需要进行回收,且已经存储的元素值已经达到了 mKeys 的长度,
// 则需要通过 gc() 方法清除被标记为 DELETED 的对应位置的内容
if (mGarbage && mSize >= mKeys.length) {
// gc() 方法会把标记为 DELETED 的 key-value 给清除,此时
// mKeys、mVulues 两个数组都会的有效值都会往前靠缩紧(即压缩)
gc();
// 因此 gc() 之后需要重新获取正确的
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
// 将 key-value 插入到对应的位置,
// 在插入的时候,可能会扩容
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
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;
mSiz
以及辅助方法:
// ContainerHelpers.java
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 {
// 直接返回了 key 所在的位置,mid 是属于 [0, size - 1] 的
return mid; // value found
}
}
// 基于二分查找,array 是排序好的,如果在 array 中没有 value 值,则会走到这里
// 当 value 大于 array[size - 1] 时,此时 lo == size,则 ~lo == -(size+1)
// 当 value 小于 array[0] 时,此时 lo == 0,则 ~lo == -1
// 无论如何 ~lo 都会为负,因为 ~lo 为负与 value 不存在 array 中是等价的,
// 且 lo 表示 value 应该插入的位置
return ~lo; // value not present
}
// GrowingArrayUtils.java
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
// currentSize + 1 <= array.length 表示还可以插入至少一个元素,因此不需要扩容
if (currentSize + 1 <= array.length) {
// System.arraycopy() 会根据 srcPos、dstPos 来判断是从前往后复制,还是从后往前复制
// 避免操作同一个数组时里面的值在操作的时候被覆盖
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
// 原数组已经满了,无法插入新元素了,因此需要扩容
// 扩容规则是如果当前容量大小 <= 4,则直接扩容为 8,否则变为原来的 2 倍
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 void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 在删除的时候,只会把对应位置的值标记为 DELETE,并不会更改 mSize,且不会马上触发 gc(),
// 这样的话可以针对某些情况提高效率,
// 比如先 delete(X) 然后 put(X, value)
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
public void remove(int key) {
delete(key);
}
// 根据索引 remove
public void removeAt(int index) {
// 只是把对应索引位置的元素标记为 DELETED
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
// 删除从 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);
}
}
需要注意的是,有关删除的方法,都没有更改 mSize,但是在调用 size() 的时候(如 mGarbage 为 true),一定会触发 gc(),从而得到正确的 mSize。
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];
}
}
// 查找 key 对应的索引
// 因此在必要的时候要先 gc,避免影响准确性
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
// 不像 key 一样使用的二分查找。是直接线性遍历去比较,
// 而且不像其他集合类使用 equals 比较,这里直接使用的 ==
// 如果有多个 key 对应同一个 value,则这里只会返回一个更靠前的 index
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
// 根据索引获取对应的 key
// 因此在必要的时候要先 gc,避免影响准确性
public int keyAt(int index) {
if (mGarbage) {
gc();
}
return mKeys[index];
}
public int size() {
// 必要时,先触发 gc,修正 mSize
if (mGarbage) {
gc();
}
return mSize;
}