SparseArray指的是稀疏数组,就是数组中并没有填满,只有部分有值,这样造成了内存浪费,往往采用的是压缩的方式来存储内容。
了解下SparseArray怎么存储的以及SparseArray的扩容。
看下代码
public class SparseArray implements Cloneable {
//内部是两个数组,一个用来存储key值得映射,一个用来存储value
private int[] mKeys;
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;
}
看下put()方法
public void put(int key, E value) {
//二分搜索,查找当前Key值在数组中的位置,这个二分查找挺有意思的
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果当前值不是负数,说明key值在数组中的映射存在,直接覆盖就行
if (i >= 0) {
mValues[i] = value;
} else {//如果是负数,说明key值得映射不存在,需要新添加
//这里为什么要取反?是因为通过上面的二分查找在mKeys中没有找到对应的key值,
//二分查找会返回一个最佳位置,
//这个最佳位置是取反过的,是一个负数,这里再次取反其实是还原了位置i。
i = ~i;
//如果key值要映射的最佳位置是小于mSize,并且当前位置的值没有被填充,那直接赋值就行。
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果要回收并且存入的输入大于等于当前key映射数组的长度,就要gc,这个代码后面看
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
//gc之后key要重新映射在当前数组中的位置,因为长度变了。
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
看下这个二分查找ContainerHelpers.binarySearch(),这个方法挺有意思的,在于即使没有找到你想要的值,但是确是会返回给你一个最佳位置。
//这里要注意的是size并不是array的长度,
//而是你已经放入对象的数目,别忘了二分查找是建立在有序数组上的,
//你存进去的数其实已经排好序了,
//这也是为什么size的大小不是array的长度,如果是整个数组长度,
//那要查找的就不是一个有序数组了,在size长度内的数是确定有序的
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
int midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found如果找到了value直接返回相应的位置,
}
}
return ~lo; // value not present如果没找到,会返回在当前数组当中的最佳位置
}
测试一数组 int[] arr = { 3,4,6,9,0,0,0,0,0} 为例,我们分别使用这个二分查找方法查找1,6,8,12,看看返回结果是什么,binarySearch(arr,4,1) = -1;
查找的值 | 返回的实际位置值 (位置取反后的值) |
---|---|
1 | 0(-1) |
6 | 2(找到了直接返回位置) |
8 | 3(-4) |
12 | 4(-5) |
为什么要取反呢?是为了区分找到和没找到,找到返回结果是正数,没找到就是负数,看上面的表一目了然。
如果要存入一个小于数组中已经存入的数1,其返回的最佳位置就是0,表示要在0这个位置插入一个新数,
查一个大于数组中已存入的数12,返回的最佳位置就是已存入的最后位置+1,而查找一个中间数8,也是其应该插入的位置,像插入1,8这两个数其实是有问题的,因为要插入的位置上已经有值了,那SparseArray怎么处理的呢?答案先给出来,就是从要插入的位置开始,数组整体往后移。
再回去看为什么put()方法里面的i为什么要区分正负就比较清楚了。来看下put()方法里面的这段代码
//如果要回收并且存入的输入大于等于当前key映射数组的长度,
//就要gc,这个代码后面看
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
//gc之后key要重新映射在当前数组中的位置,因为长度变了
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);。
}
gc代码如下
private void gc() {
//为什么要gc是因为有时候做了删除操作,
//可能导致mSize长度内的数不是有序的了,
//需要对数组重新排列,
//要注意的是其实整体上仍然是有序的,
//例如{2,3,0,5,7,0,0},3与5之间的0表示删除过的数,
//那么这么数组重排序的话是不需要所有都重拍的,
//只要把删除位置后面的数往前挪一下就行了,gc方法就是这个原理
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
//用了两个指针,0跟i,O表示遍历过程中要被覆盖的位置,i就是用来遍历整个数组的
if (val != DELETED) {
//找到了一个空位置o,把位置i上的数移到位置o,并且要把位置上的数置空
if (i != o) {
keys[o] = keys[i];//索引往前移
values[o] = val;//对应的值也移到相应位置
values[i] = null;//置空
}
o++;
}
}
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
gc这个方法其实也挺有意思的,两个指针用来排序的方法,面试的时候经常会用到。
回到正题在看下SparseArray的put里面还剩下什么没有解剖~
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
也就GrowingArrayUtils.insert这个方法了,看代码
//大概说一下把,前面二分查找帮我们找到了最佳位置,
//我们现在需要在这个最佳位置插入数据,
//前面说过就是把当前位置后面的数据正题后移再放入我们的数据就行了
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {//如果数组长度能够容下直接在原数组上插入
//调用了Java 的native方法,把array 从index开始的数复制到index+1上,
//复制长度是currentSize - index
System.arraycopy(array, index, array, index + 1, currentSize - index);
//空出来的那个位置直接放入我们要存入的值,
//也不是空出来,其实index上还是有数的,
// 比如:{2,3,4,5,0,0}从index=1开始复制,复制长度为5,复制后的结果就是{2,3,3,4,5,0}了
array[index] = element;
return array;
}
//这就是扩容了,新建了一个数组,长度*2
int[] newArray = new int[growSize(currentSize)];
//新旧数组拷贝,先拷贝最佳位置之前的到新数组
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;//直接在新数组上赋值
//然后拷贝旧数组最佳位置index起的所有数到新数组里面,
//只是做了分段拷贝而已
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
SparseArray的扩容机制大致上就这些,其实就最后这一小部分代码。至于System.arraycopy()方法就不讨论了,网上都有,就是比遍历赋值效率要高些。