SparseArray源码分析
-
SparseArray(稀疏数组)是什么?
类似于map,可以储存key-value键值对,与HashMap不同因为其key只能是整型int而且内部存储结构是数组(最新HashMap存储结构为红黑树+链表+数组);为android的工具类,用于优化HashMap
这种情况。由于其内部使用数组来存储key和value,且数组内容可能大部分都并未使用,因此叫稀疏数组。 -
SparseArray的优势劣势?
与HashMap相对比,SparseArray的内存效率更高,因为它避免了自动装箱键,并且没有HashMap中额外的节点(链表节点,红黑树节点)内存开销。但是由于SparseArray将key放在一个数组结构中,且使用二分查找,因此其不适用于大数据量的储存。例如储存数百个数据,速度和HashMap相当,性能差异不明显,并且两者之间性能差异小于50%。相比于HashMap,SparseArray更节省内存,但是在大数据量的时候,插入和查找的效率都会比HashMap低。
-
SparseArray的应用场景?
适用于储存小容量数据,key必须为int型
下面分析源码:
类成员变量:
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
- DELETED:用于标记已删除的key-value
- mGarbage:用于标记是否需要回收被删除的数据
- mKeys:用于储存key
- mValues:用于储存values
- mSize:当前包含非空元素(value)的条目(key-value),未调用gc前,包括value为DELETED的情况,这里的gc并非为java中的System.gc(),而是SparseArray中的gc方法。
构造函数
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;
}
- 默认构造函数:调用带参构造函数,指定容量为10
- 带参(指定数组容量)构造函数:容量为空则赋值mKeys和mValues为长度为0的数组,否则创建最小长度为initialCapacity的Unpadded Object数组,再将key数组的大小指定为value数组的大小。
关于EmptyArray:
点我查看源码
//版本名称: Oreo API Level: 26
public final class EmptyArray {
private EmptyArray() {}
public static final boolean[] BOOLEAN = new boolean[0];
public static final byte[] BYTE = new byte[0];
public static final char[] CHAR = new char[0];
public static final double[] DOUBLE = new double[0];
public static final float[] FLOAT = new float[0];
public static final int[] INT = new int[0];
public static final long[] LONG = new long[0];
public static final Class>[] CLASS = new Class[0];
public static final Object[] OBJECT = new Object[0];
public static final String[] STRING = new String[0];
public static final Throwable[] THROWABLE = new Throwable[0];
public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
public static final java.lang.reflect.Type[] TYPE = new java.lang.reflect.Type[0];
public static final java.lang.reflect.TypeVariable[] TYPE_VARIABLE =
new java.lang.reflect.TypeVariable[0];
}
关于ArrayUtils.newUnpaddedObjectArray:
点我查看源码
// 版本名称: Oreo API Level: 26
public static Object[] newUnpaddedObjectArray(int minLen) {
return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen);
}
其中调用了VMRuntime.getRuntime().newUnpaddedArray来分配object数组内存,该方法字面意思为new一个un padded的数组,即new一个不填充的数组。返回一个至少为minLen长度的数组,但有可能会更大。增加的大小用于避免数组排列时填充的大小,填充量取决于组件类型和内存分配器实现。因为编译器会根据数组对齐,在申请obj内存时,往往会分配大于该obj的实际内存而保证自然对齐使CPU读写更加高效,但是这往往需要填充一些没有用到的空间,因此,为了不浪费这个空间,array变得更大了。
关于数据结构对齐可以看wiki描述:https://en.wikipedia.org/wiki/Data_structure_alignment
put
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); //二叉查找,可以看后面的方法解释
if (i >= 0) {
mValues[i] = value; //数组中存在该key,直接更新值
} else {
i = ~i; //取到要插入的数组下标
//如果要插入的下标小于mSize且被标志为已删除的话
//重新将key赋值,value赋值
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果标记为mBarbage且mSize大于或者等于key数组大小
if (mGarbage && mSize >= mKeys.length) {
//gc方法回收已删除的数据
gc();
//重新进行二叉搜索,因为索引可能已经发生变化了
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//插入key
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
//插入value
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
//mSize加1
mSize++;
}
}
- 首先通过二分查找是否在数组中存在该key,若找到,则返回对应的数组下标,否则,返回要插入的数组下标的负数
- 如果存在的话,直接将对应的value数组下标赋值
- 如果不存在的话,返回要插入的数组下标的负数,因此这里重新取反,拿到要插入数组下标的整数。再根据条件判断key是否为已删除数据,是否要进行数组压缩,最后插入key和value到各自数组中。
关于ContainerHelpers.binarySearch:
//该方法要求array必须为有序数组
//array:要查找的数组
//size:数组中有效大小,从下表0开始,到size-1为数组的有效范围
//value:要查找的值
static int binarySearch(int[] array, int size, int value) {
//定义low低数组下标,用于数组中往上查找,最小为0
int lo = 0;
//定义high高数组下标,用于数组中往下查找,最大为size-1
int hi = size - 1;
//如果lo <= hi,则循环
while (lo <= hi) {
//取数组中间下标
//右移1位即等同于除以2的1次方即除2,无符号右移,空位都以0补齐,与运算比除预算效率高
final int mid = (lo + hi) >>> 1;
//取数组中间下标的值
final int midVal = array[mid];
//若中间值小于value,则将low赋值为mid+1,继续往上查找
if (midVal < value) {
lo = mid + 1;
//若中间值大于value,则将high赋值为mid-1,继续往下查找
} else if (midVal > value) {
hi = mid - 1;
//已找到,返回数组下标
} else {
return mid; // value found
}
}
//否则返回一个小于0的值,且该值还有另外一个含义:
//要插入的位置下标,SparseArray拿到这个返回值后,便可知道要插入的下标
//该值还有另外一个作用就是可以保证数组的有序性
return ~lo; // value not present
}
关于gc
该方法为SparseArray的回收方法,用于将删除的数据从mKeys数组和mValues数组中删除,并将剩下的数据往前移动
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize; //当前数组有效大小,因为mkey大小和mValues大小相同,因此这里mSize是对两者而言的
int o = 0; //数组中非null的数量,即有效数据数量,起始值为0
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
//不是被删除的元素
if (val != DELETED) {
//若i 不等于 o的话
//证明o为被删除的下标,因此这里需要将下标为o的key数组的值
//赋值为下标为i的值,且将下标为o的value数组的值赋值为当前val
//最后将已经移走的当前下标i对应的value数组的值置为null
//但是这里可以看到,key[i]并没有置为0,因为不设置为0的话
//不会影响二叉查找,因为后面mSize为非null数据数量
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
//有效数据+1
o++;
}
}
mGarbage = false; //gc后将回收标志置为false
mSize = o; //将mSize赋值为数组中非null的数量
// Log.e("SparseArray", "gc end with " + mSize);
}
关于GrowingArrayUtils.insert
点我查看源码
//array: 要插入的数组
//currentSize: 数组中的元素数量,小于或等于array.length
//index: 要插入的下标
//element: 要插入的元素
public static T[] insert(T[] array, int currentSize, int index, T element) {
//如果数组中的元素数量大于数组长度的话,抛出异常
assert currentSize <= array.length;
//如果在当前数组再插入一个元素,并且不超过数组的大小的话
if (currentSize + 1 <= array.length) {
//将原本数据从index开始,往后移动currentSize - index个数据
System.arraycopy(array, index, array, index + 1, currentSize - index);
//将index对应的值赋值为要插入的元素
array[index] = element;
return array;
}
@SuppressWarnings("unchecked")
//否则需要重新创建数组,且将原本数组的内容赋值到新数组中
T[] newArray = ArrayUtils.newUnpaddedArray((Class)array.getClass().getComponentType(),
growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
append
直接put一个key-value到数组中,针对key大于mKeys数组所有现有键的情况进行优化。
public void append(int key, E value) {
//若当前mSize不为0且key小于mKeys数组中的最大值
if (mSize != 0 && key <= mKeys[mSize - 1]) {
//直接put
put(key, value);
return;
}
//mGarbage回收标志为true且mSize大于或等于mKeys数组长度
if (mGarbage && mSize >= mKeys.length) {
//调用gc方法
gc();
}
//插入key
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
//插入value
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
setValueAt
按照给定的index替换value
public void setValueAt(int index, E value) {
//mGarbage回收标志为true的话调用gc方法
if (mGarbage) {
gc();
}
//将对应index下表的值赋值为value
mValues[index] = value;
}
get
//根据key获取value
public E get(int key) {
return get(key, null);
}
//根据key获取value,若该key不存在或者已删除,返回valueIfKeyNotFound
public E get(int key, E valueIfKeyNotFound) {
//首先进行二分查找 找到key的数组下表
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//i小于0代表mKeys数组中不包含该Key,或者该key-value已被删除
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
remove
//删除key对应的数据
public void remove(int key) {
delete(key);
}
//删除key对应的数据
public void delete(int key) {
//先通过二叉查找是否存在该key
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//i >= 0代表存在该key且i为其在mKeys数组中的下标
if (i >= 0) {
//且该key对应的value还没被删除
if (mValues[i] != DELETED) {
//将对应mValues数组下表的值置位DELETED已删除
mValues[i] = DELETED;
//将回收标志置为true,在调用size(), put(), append()等方法时需要
//调用gc方法将被标记为已删除的数据,即对应的value值为DELETED
//从数组移除,且将后面的没被删除的数据往前移动
mGarbage = true;
}
}
}
//删除对应下表的数据
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
//将回收标志置为true
mGarbage = true;
}
}
//删除从对应下表开始,大小为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);
}
}
查询
indexOfKey
根据key获取其在数组中的下表
public int indexOfKey(int key) {
//mGarbage回收标志为true的话调用gc方法
if (mGarbage) {
gc();
}
//若数组中存在该key,则返回对应下表
//否则返回一个负数
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
indexOfValue
根据value获取其在数组中的下表
public int indexOfValue(E value) {
//mGarbage回收标志为true的话调用gc方法
if (mGarbage) {
gc();
}
//遍历mValues数组,判断是否相等
for (int i = 0; i < mSize; i++) {
if (mValues[i] == value) {
//若相等,则返回i
return i;
}
}
//否则返回-1
return -1;
}
keyAt
获取下表为index的key
public int keyAt(int index) {
//mGarbage回收标志为true的话调用gc方法
if (mGarbage) {
gc();
}
//返回mKeys数组数据
return mKeys[index];
}
valueAt
获取下表为index的value
public E valueAt(int index) {
//mGarbage回收标志为true的话调用gc方法
if (mGarbage) {
gc();
}
//返回mValues/数组数据
return (E) mValues[index];
}