学而不思则罔,思而不学则殆
【Android】SparseIntArray源码解析
简介
SparseIntArray是Android提供的轻量级的Map<int,int>。SparseIntArray优化了int到int键值对的存储
内部对数据还采取了压缩的方式来表示稀疏数组的数据。
其中key值数组是按照升序排列的数组。
总结
设计目的是优化int到int映射的存储,使用int类型的数组mKeys存储映射的键,使用对应类型的数组mValues存储值
int类型的键在存储上是有顺序的,在查找值时,先使用二分查找,在mKeys中查找值在mValues中的下标,然后返回值
以上三种数据类型和SparseArray最大的区别在于SparseArray在删除元素的时候会将元素设置为DELETED,后续会有gc的过程。
相对于使用HashMap,这样的设计的优势和缺点:
优势:
避免int类型的键自动装箱
相较于HashMap使用Node,这样的设计使用更小的存储单元即可存储key到value的映射
缺点:
在进行元素查找时使用二分查找,元素较多(谷歌给出的数字是大于1000)时,查找效率较低
在进行元素的添加和删除时,可能会频繁进行元素的移动,运行效率可能会降低
数据结构
public class SparseIntArray implements Cloneable {
@UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
private int[] mKeys;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, int)
private int[] mValues;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
private int mSize;
...
}
整体采用两个数组,一个是Key值的数组和Values数组
源码分析
构造函数
默认大小是10
public SparseIntArray() {
this(10);
}
也可以自定义大小:
/**
* Creates a new SparseIntArray containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings. If you supply an initial capacity of 0, the
* sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
public SparseIntArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.INT;
} else {
mKeys = ArrayUtils.newUnpaddedIntArray(initialCapacity);
mValues = new int[mKeys.length];
}
mSize = 0;
}
创建一个新的不包含映射的SparseIntArray,它不需要任何额外的内存分配来存储指定数量的映射。如果您提供的初始容量为0,那么稀疏数组将用轻量级表示初始化,而不需要任何额外的数组分配。
测试
通过反射打印数组的状态:
private void showSparseIntArray() {
Class<SparseIntArray> sparseIntArrayClass = SparseIntArray.class;
Log.d("zhangyu", "\n-------------------------------------------------");
Log.d("zhangyu", "sparseIntArray 1:" + sparseIntArray.size() + " " + sparseIntArray);
try {
Field mSize = sparseIntArrayClass.getDeclaredField("mSize");
Field mKeys = sparseIntArrayClass.getDeclaredField("mKeys");
Field mValues = sparseIntArrayClass.getDeclaredField("mValues");
mSize.setAccessible(true);
Log.d("zhangyu", "Field mSize:" + mSize.get(sparseIntArray));
mKeys.setAccessible(true);
int[] keys = (int[]) mKeys.get(sparseIntArray);
mValues.setAccessible(true);
int[] values = (int[]) mValues.get(sparseIntArray);
//打印key值数组情况
Log.d("zhangyu", "Field mKeys:" + keys.length + " " + Arrays.toString(keys));
//打印values值数组情况
Log.d("zhangyu", "Field mValues:" + values.length + " " + Arrays.toString(values));
} catch (NoSuchFieldException | IllegalAccessException e) {
Log.d("zhangyu", "e:" + e);
e.printStackTrace();
}
Log.d("zhangyu", "sparseIntArray 2:" + sparseIntArray.size() + " " + sparseIntArray);
Log.d("zhangyu", "-------------------------------------------------\n");
}
初始状态
实例化一个SparseIntArray 对象,啥都不干,只是打印初始数组情况:
private SparseIntArray sparseIntArray = new SparseIntArray();
情况如下:
2020-09-27 22:32:41.620 5421-5421/com.example.sparseintarraydemo D/zhangyu: -------------------------------------------------
2020-09-27 22:32:41.620 5421-5421/com.example.sparseintarraydemo D/zhangyu: sparseIntArray 1:0 {}
2020-09-27 22:32:41.622 5421-5421/com.example.sparseintarraydemo D/zhangyu: Field mSize:0
2020-09-27 22:32:41.622 5421-5421/com.example.sparseintarraydemo D/zhangyu: Field mKeys:11 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
2020-09-27 22:32:41.622 5421-5421/com.example.sparseintarraydemo D/zhangyu: Field mValues:11 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
2020-09-27 22:32:41.622 5421-5421/com.example.sparseintarraydemo D/zhangyu: sparseIntArray 2:0 {}
2020-09-27 22:32:41.622 5421-5421/com.example.sparseintarraydemo D/zhangyu: -------------------------------------------------
size = 0没有问题,每个元素都为0也没有问题,
为啥数组初始长度是11?有点疑惑
查找元素
初始是添加四个元素,如下:
2020-09-28 07:52:06.884 6518-6518/com.example.sparseintarraydemo D/zhangyu: Field mSize:4
2020-09-28 07:52:06.884 6518-6518/com.example.sparseintarraydemo D/zhangyu: Field mKeys:11 [1, 100, 1000, 10000, 0, 0, 0, 0, 0, 0, 0]
2020-09-28 07:52:06.884 6518-6518/com.example.sparseintarraydemo D/zhangyu: Field mValues:11 [1, 100, 1000, 99, 0, 0, 0, 0, 0, 0, 0]
2020-09-28 07:52:06.884 6518-6518/com.example.sparseintarraydemo D/zhangyu: sparseIntArray 2:4 {1=1, 100=100, 1000=1000, 10000=99}
然后通过indexOfKey查找元素的下标:
/**
* Returns the index for which {@link #keyAt} would return the
* specified key, or a negative number if the specified
* key is not mapped.
*/
public int indexOfKey(int key) {
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
内部实现原理是通过二分查找的原理找到key值下标。时间复杂度
O
(
l
o
g
n
)
O(logn)
O(logn)
如果找到就返回下标,如果没有找到就返回一个负值,但是这个负值不是-1,而是有讲究的,返回值取反就是key真正的应该存在的位置的下标。
查找已经存在的key值(100):
2020-09-28 07:55:39.280 6518-6518/com.example.sparseintarraydemo D/zhangyu: indexOfKey:1
查找不存在的key值下标(50):
2020-09-28 07:58:33.880 6518-6518/com.example.sparseintarraydemo D/zhangyu: sparseIntArray:4 {1=1, 100=100, 1000=1000, 10000=99}
2020-09-28 07:58:33.880 6518-6518/com.example.sparseintarraydemo D/zhangyu: indexOfKey:-2
返回值是-2,-2取反等于多少呢?我们知道在计算机汇总都是补码
-2 原码: 1000 0010
-2 反码: 1111 1101
-2 补码: 1111 1110
-2 取反: 0000 0001 = 1
想了解更多原码,反码,补码请访问:原码,反码,补码概念
所以50的位置应该是1,也就是现在100的位置(如果有的话),这个值很重要,数组移动全都跟这个值相关。
查找不存在的key值下标(5000):
2020-09-28 08:04:04.331 6518-6518/com.example.sparseintarraydemo D/zhangyu: sparseIntArray:4 {1=1, 100=100, 1000=1000, 10000=99}
2020-09-28 08:04:04.331 6518-6518/com.example.sparseintarraydemo D/zhangyu: indexOfKey:-4
换算为:3
查找不存在的key值下标(50000):
2020-09-28 08:04:54.176 6518-6518/com.example.sparseintarraydemo D/zhangyu: sparseIntArray:4 {1=1, 100=100, 1000=1000, 10000=99}
2020-09-28 08:04:54.176 6518-6518/com.example.sparseintarraydemo D/zhangyu: indexOfKey:-5
换算为:4
在测试一个数组长度已满的情况:
D/zhangyu: Field mSize:11
D/zhangyu: Field mKeys:11 [1, 2, 3, 5, 55, 100, 555, 1000, 5555, 10000, 55555]
D/zhangyu: Field mValues:11 [1, 2, 3, 5, 55, 100, 555, 1000, 5555, 99, 55555]
D/zhangyu: sparseIntArray 2:11 {1=1, 2=2, 3=3, 5=5, 55=55, 100=100, 555=555, 1000=1000, 5555=5555, 10000=99, 55555=55555}
此时数组长度已满:
查找55556的位置下标:
2020-09-28 08:08:15.527 6518-6518/com.example.sparseintarraydemo D/zhangyu: sparseIntArray:11 {1=1, 2=2, 3=3, 5=5, 55=55, 100=100, 555=555, 1000=1000, 5555=5555, 10000=99, 55555=55555}
2020-09-28 08:08:15.527 6518-6518/com.example.sparseintarraydemo D/zhangyu: indexOfKey:-12
返回值为-12,说明没有找到,换算后为:11,如果要加入的话,位置下标为11.
添加元素
添加元素1:
2020-09-28 07:45:50.637 6321-6321/com.example.sparseintarraydemo D/zhangyu: Field mSize:1
2020-09-28 07:45:50.638 6321-6321/com.example.sparseintarraydemo D/zhangyu: Field mKeys:11 [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
2020-09-28 07:45:50.638 6321-6321/com.example.sparseintarraydemo D/zhangyu: Field mValues:11 [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
添加2:
2020-09-28 07:47:08.559 6321-6321/com.example.sparseintarraydemo D/zhangyu: sparseIntArray 1:2 {1=1, 2=2}
2020-09-28 07:47:08.559 6321-6321/com.example.sparseintarraydemo D/zhangyu: Field mSize:2
2020-09-28 07:47:08.559 6321-6321/com.example.sparseintarraydemo D/zhangyu: Field mKeys:11 [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0]
2020-09-28 07:47:08.560 6321-6321/com.example.sparseintarraydemo D/zhangyu: Field mValues:11 [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0]
把数组填满:
D/zhangyu: Field mSize:11
D/zhangyu: Field mKeys:11 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]
D/zhangyu: Field mValues:11 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]
D/zhangyu: sparseIntArray 2:11 {2=2, 4=4, 6=6, 8=8, 10=10, 12=12, 14=14, 16=16, 18=18, 20=20, 22=22}
测试数组扩容逻辑
再添加一个元素(1):
结果如下:
D/zhangyu: Field mSize:12
D/zhangyu: Field mKeys:23 [2, 4, 5, 6, 8, 10, 12, 14, 16, 18, 20, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
D/zhangyu: Field mValues:23 [2, 4, 5, 6, 8, 10, 12, 14, 16, 18, 20, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
D/zhangyu: sparseIntArray 2:12 {2=2, 4=4, 5=5, 6=6, 8=8, 10=10, 12=12, 14=14, 16=16, 18=18, 20=20, 22=22}
size = 12,但是数组长度从11变成了23,数组扩容了。
扩容
元素复制
添加元素
插入新增的元素
元素复制
后面的元素后移。
复制源码逻辑
现在再来追一下源码逻辑:
我用的是add添加元素。
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, int value) {
//1.查找新元素位置下标
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
//2.元素下标为正数,直接修改该位置下标的值
mValues[i] = value;
} else {
//3.取反
i = ~i;
//4.添加元素
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
- 通过前面【查找元素】测试,我们知道这里通过二分查找返回的数值i = -3 , 小于 < 0
- 取反 i = 2
- 插入元素,这才是重点
根据结果我们知道:数组显示经过了扩容,然后元素位置移动,最后在设置新加入的元素。
/**
* Primitive int version of {@link #insert(Object[], int, int, Object)}.
*/
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
//1.若果新增元素小于当前数组大小,直接元素移动位置
if (currentSize + 1 <= array.length) {
//1.1 新增元素后面的同一向后平移一个位置
System.arraycopy(array, index, array, index + 1, currentSize - index);
//1.2 插入新的元素
array[index] = element;
return array;
}
//2. 需要扩容,生成新的数组
int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
//3. 复制元素
System.arraycopy(array, 0, newArray, 0, index);
//4. 插入新的元素
newArray[index] = element;
//5. 复制元素
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
扩容数据长度逻辑
/**
* Given the current size of an array, returns an ideal size to which the array should grow.
* This is typically double the given size, but should not be relied upon to do so in the
* future.
*/
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
小于4的时候,直接为8,其他情况下是现有大小*2.
删除元素
/**
* Removes the mapping at the given index.
*/
public void removeAt(int index) {
//移动元素
System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
//大小减一
mSize--;
}
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
//根据二分查找,找出key所对应的数组下标
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//删除元素,位置移动
if (i >= 0) {
removeAt(i);
}
}
其中查找元素下标的时间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)
删除元素的时间复杂度为
O
(
n
)
O(n)
O(n)
删除结果:
D/zhangyu: -------------------------------------------------
D/zhangyu: add:6
D/zhangyu: indexOfKey:2
D/zhangyu: 删除前:{2=2, 4=4, 6=6, 8=8, 10=10, 12=12, 14=14, 16=16, 18=18, 20=20, 22=22}
D/zhangyu: 删除后:{2=2, 4=4, 8=8, 10=10, 12=12, 14=14, 16=16, 18=18, 20=20, 22=22}
D/zhangyu: -------------------------------------------------
D/zhangyu: sparseIntArray 1:10 {2=2, 4=4, 8=8, 10=10, 12=12, 14=14, 16=16, 18=18, 20=20, 22=22}
D/zhangyu: Field mSize:10
D/zhangyu: Field mKeys:11 [2, 4, 8, 10, 12, 14, 16, 18, 20, 22, 22]
D/zhangyu: Field mValues:11 [2, 4, 8, 10, 12, 14, 16, 18, 20, 22, 22]
D/zhangyu: sparseIntArray 2:10 {2=2, 4=4, 8=8, 10=10, 12=12, 14=14, 16=16, 18=18, 20=20, 22=22}
D/zhangyu: -------------------------------------------------
图形化删除元素逻辑
删除6,那么就需要把6后面的所有元素都前移一位。
同理对应的values也需要前移一位,才能两个数组意义对应。