SparseArray 是 Android 在 Android SdK 为咱们提供的一个基础的数据结构,其功能相似于 HashMap。与 HashMap 不一样的是它的 Key 只能是 int 值,不能是其余的类型。java
首先也仍是先经过 demo 来看一看 SparseArray 的基本使用方法,主要就是插入方法以及遍历方法。这也会后面的代码分析打下一个基础。算法
SparseArray<Object> sparseArray = new SparseArray<>();
sparseArray.put(0,null);
sparseArray.put(1,"fsdfd");
sparseArray.put(2,new String("fjdslfjdk"));
sparseArray.put(3,1);
sparseArray.put(4,new Boolean(true));
sparseArray.put(5,new Object());
sparseArray.put(8,new String("42fsjfldk"));
sparseArray.put(20,"jfslfjdkfj");
sparseArray.put(0,"chongfude");
int size = sparseArray.size();
for (int i = 0;i < size;i++) {
Log.d(TAG, "sparseArraySample: i = " + i + ";value = " + sparseArray.get(sparseArray.keyAt(i)) );
}
复制代码
上面代码先是 new 了一个 SparseArray,注意声明时只能指定 value 的类型,而 key 是固定为 int 的。而后再往里面添加 key 以及 value。这里注意一下的是 key 为 0 的状况插入了 2 次。遍历时,是先经过顺序的下标取出 key ,再经过 keyAt 来 get 出 value。固然也能够一步到位经过 valueAt() 直接获取到 value。而后这个 demo 的执行结果以下。数组
sparseArraySample: i = 0;value = chongfude sparseArraySample: i = 1;value = fsdfd sparseArraySample: i = 2;value = fjdslfjdk sparseArraySample: i = 3;value = 1 sparseArraySample: i = 4;value = true sparseArraySample: i = 5;value = java.lang.Object@b67a0fa sparseArraySample: i = 6;value = 42fsjfldk sparseArraySample: i = 7;value = jfslfjdkfjbash
而后经过 Debug 来看一看在内存中,SparseArray 实际是如何存储的。以下图分别是 key 与 value 在内存中的形式。能够看出 keys 和 values 的大小都为 13,并且 keys 的值是按从小到大顺序排列的。数据结构
下面是 SparseArray 的类图结构,能够看到其属性很是的少,也能够看出其分别用了数组 int[] 和 object[] 来存储 key 以及 value。 源码分析
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;
}
复制代码
其有 2 个构造方法,带参与不带参。固然,这个参数就是指定数组初始大小,也就是 SparseArray 的初始容量。而不带参数则默认指定数组大小为 10 个。ui
public void put(int key, E value) {
// 1.先进行二分查找
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 2. 若是找到了,则 i 必大于等于 0
if (i >= 0) {
mValues[i] = value;
} else {
// 3. 没找到,则找一个正确的位置再插入
i = ~i;
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
复制代码
这里调用了不少外部的方法以及内部的方法。首先是 ContainerHelpers#binarySearch() 的二分查找算法。this
//This is Arrays.binarySearch(), but doesn't do any argument validation. static int binarySearch(int[] array, int size, int value) { int lo = 0; int hi = size - 1; while (lo <= hi) { // 高位+低位之各除以 2,写成右移,即经过位运算替代除法以提升运算效率 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; // value found } } //若没找到,则lo是value应该插入的位置,是一个正数。对这个正数去反,返回负数回去 return ~lo; // value not present } 复制代码
二分查找的分析属于基础内容,在注释中了。回到 put() 方法首先经过二分查找算法从当前 keys 中查找是否已经存在相同的 key 了,若是存在则会返回大于等于 0 的下标,而后接下来就会将原下标下的 values 中的旧value 替换成新的 value 值,即发生了覆盖。spa
那若是没有找到,那么将 i 取反就是要插入的位置了,这一结论正好来自 binarySearch() 的返回结果。能够看到其最后若是没有找到的话,就会返回 lo 的取反数。那么这里再把它取反过来那就是 lo 了。3d
这里若是 i 是在大小 mSizes 的范围内的,且其对应的 values[i] 又刚是被标记为删除的对象,那么就能够复用这个对象,不然就仍是要依当前的 i 值进一步寻找要插入的位置,再插入相应的 value。
在插入以前,若是因为以前进行过 delete(),remoeAt() 以及 removeReturnOld() 中的某一个方法,那就可能要进行 gc() 操做。固然,这里不是指的内存的 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 ,实际就是压缩存储,简单点说就是让元素挨得紧一点。
而 gc() 完以后,下标 i 可能会发生变化,所以须要从新查找一次,以获得一个新的下标 i。
最后就是经过 GrowingArrayUtils.insert() 来进行 key 和 value 的插入。这个 insert() 根据数组类型重载了多个,这里只分析 int[] 类型的便可。
public static int[] insert(int[] array, int currentSize, int index, int element) {
//确认 当前集合长度 小于等于 array数组长度
assert currentSize <= array.length;
//不须要扩容
if (currentSize + 1 <= array.length) {
//将array数组内从 index 移到 index + 1,共移了 currentSize - index 个,即从index开始后移一位,那么就留出 index 的位置来插入新的值。
System.arraycopy(array, index, array, index + 1, currentSize - index);
//在index处插入新的值
array[index] = element;
return array;
}
//须要扩容,构建新的数组,新的数组大小由growSize() 计算获得
int[] newArray = new int[growSize(currentSize)];
//这里再分 3 段赋值。首先将原数组中 index 以前的数据复制到新数组中
System.arraycopy(array, 0, newArray, 0, index);
//而后在index处插入新的值
newArray[index] = element;
//最后将原数组中 index 及其以后的数据赋值到新数组中
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
复制代码
上面的算法中,若是不须要扩容则直接进行移位以留出空位来插入新的值,若是须要扩容则先扩容,而后根据要插入的位置 index,分三段数据复制到新的数组中。这里再看看 growSize() 是如何进行扩容 size 的计算的。
public static int growSize(int currentSize) {
//若是当前size 小于等于4,则返回8, 不然返回当前size的两倍
return currentSize <= 4 ? 8 : currentSize * 2;
}
复制代码
代码相对简单,当前 size 小于等于 4 则为 8 ,不然为 2 倍大小。
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];
}
}
复制代码
get() 方法就是经过 key 来返回对应的 value,前面在分析 put() 的时候已经分析过了二分查找。那么这里若是找到了,就会经过下标直接从 mValues[] 中返回。
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
复制代码
delete() 也很是简单,经过二分查找算法定位到下标,而后将对应的 value 标记为 DELETE,而且标记须要进行 gc 了。这里须要注意的是被标记为 DELETE 的 value 不会在 gc 中被移除掉,而只会被覆盖掉,从而提升了插入的效率。
文章对 SparseArray 进行了简要的分析,文章也只对主要的几个方法进行了分析,其余没有分析到的方法在这个基础上再进行分析相信也是很简单的。而总结下来几点是:
最后,感谢你能读到并读完此文章。受限于做者水平有限,若是存在错误或者疑问都欢迎留言讨论。若是个人分享可以帮助到你,也请记得帮忙点个赞吧,鼓励我继续写下去,谢谢。