说到Android 常用的数据结构,那不得不提一下SparseArray(稀疏数组),我们在很多业务以及Android源码中能见到
简单来讲就是一个使用int作为Key的 Map ,官网的介绍就是:
SparseArrays map integers to Objects
它继承自Object,实现了Cloneable:
public class SparseArray<E> implements Cloneable {}
其中E就是我们的泛型参数,即我们要存入数据的类型
public SparseArray() {
this(10);
}
public SparseArray(int initialCapacity) {
}
我们可以看到,它有两个构造方法,一个参数为容量大小,另一个无参构造方法,最终调用的是容量为10的的构造方法。
既然是一个数据结构,当然要从增删改查来介绍它的基本用法:
提供了put和append方法让使用可以放以int 作为key,任何类型作为值的数据。
public void put(int key, E value)
public void append(int key, E value)
当然也就是指定某个key去删除
public void delete(int key)
public void remove(int key)
也可以删除某个key之后返回删除那个key的值:
public E removeReturnOld(int key)
因为SparseArray 内部存储是用数组实现的,所以提供了按照数组下标来移除元素的功能(使用的时候要注意数组越界的问题):
public void removeAt(int index)
还提供了基于数组下标的范围移除的功能(比如从数组的第1个开始往后移除大小3个的):
public void removeAtRange(int index, int size)
还提供了可以修改某个下标对应值的方法
public void setValueAt(int index, E value)
根据我们存入的key找到我们的值
public E get(int key)
public E get(int key, E valueIfKeyNotFound)
还可以根据数组下标获取值
public E valueAt(int index)
同样可以根据下标获取key:
public int keyAt(int index)
也可以根据我们的key或者value反查出下标:
public int indexOfKey(int key)
public int indexOfValue(E value)
public int indexOfValueByValue(E value)
//大小
public int size()
//清空
public void clear()
看完这里基本用法已经是都介绍完了,当然了解事物三部曲: 是什么、怎么用都讲了,最后一步为什么当然也不能少,下面就来细讲讲它都实现原理:
为了方便理解,我们先从get方法开始分析:
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方法最终调用的是这个方法,方法第一行首先调用了ContainerHelpers.binarySearch(mKeys, mSize, key)方法,这个方法接受三个参数,第三个是我们即将存数据所指定的key,第一个和第二个是我们的全局变量,分别是存我们所有key的数组和 当前存入数据的大小:
//存放我们的key
private int[] mKeys;
//存放我们的值
private Object[] mValues;
//表示存入数据量的大小
private int mSize;
从这里我们也能看到,我们的存入的键值对分别被存到量两个数组中,然后用一个全局变量表示当前存入数据量的大小,那为何要单独用一个变量来表示它的大小而不是这两个数组的长度呢?这个我们后面讲。
现在参数以及含义都知道了,我们来看看这个方法,这个方法其实就是SparseArray核心的二分查找法,后面存取等操作都会有它到身影,我们来分析一下:
/**
* 找到目标key的 位置
*
* @param keysArray 存key的数组
* @param size 数组的大小
* @param targetKey 要找的key
* @return 如果找到了返回相应的key ,
* 若未找到,则返回这个key应该被存放的位置的取反 ~location
*/
static int binarySearch(int[] keysArray, int size, int targetKey) {
//位置(初始值为0)
int location = 0;
//查找上限
int ceiling = size - 1;
//二分法查找
while (location <= ceiling) {
//除以二取先中间的key
final int mid = (location + ceiling) >>> 1;
final int midKey = keysArray[mid];
if (midKey < targetKey) {
location = mid + 1;
} else if (midKey > targetKey) {
ceiling = mid - 1;
} else {
return mid; // key found
}
}
//此时location的值就是它应该被存储的到数组的位置(0或者length+1)
return ~location; // key not present
}
我这边把源码中的命名稍微改了一下,让理解起来更容易一点,那么整体看下来,就是从我们存key的数组去找我们的targetKey,如果找到了,则直接返回,没找到返回一个赋值
一开始定义初始位置为0,上限即整个大小,然后当我们当位置小于或者等于上限时候开始循环查找,第9行,对初始位置和上限的和做一个无符号右移,也就是除以2,然后取到位于中间的key,通过比较目标key和中间key的大小去确定确定下次查找的范围,如果中间的值小,说明在中间范围以上,所以下次开始查找的范围的起始位置就是中间位置+1,再次执行循环体的内容,如果找到了则直接返回,如果始终没找到,返回location的取反,其中取反和无符号涉及到位运算,如果还不是特别了解可以参考这里,无论最终location = 0或者length+1 ,它的取反都是一个负数。
细节:
再次回到我们的get方法,现在看下来就简单了,i就是我们要找key的位置下标,如果小于0,就表示未找到该key,直接返回未找到,否则返回Value中的第i个元素。
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
再看另一个条件:mValues[i] == DELETED他其实表示的是存值数组中的第i个元素被删除了,进一步探究,我们再来看看它的删除方法
(delete方法和Remove其实都是一个方法):
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage= true;
}
}
}
还是我们熟悉的二分查找法,首先找到这个key对应的数组下标,如果大于或者等于0,表示存在,此时如果还没有被删除,将该位置负值为DELETE,然后将mGarbage标记为置为ture,表示要进行垃圾回收,而DELETED就是一个Object,用于表示该位置的元素被删除了。
private static final Object DELETED = new Object();
那么再回到刚刚的get方法的第二个条件,当存值的数组中被标记为删除之后,即使数组给该下标分配了空间,也会认为key对应的值不存在,而我们的delete操作只是给值中的元素做了标记操作,并没有对数组对象做一些操作,不会像ArrayList 会对数组做移位操作。
到这里再回想一下,一开始为何要设置全局标记位mSize而不是数组的长度来表示size了吧?因为即使我一个原来有值的某一个元素被删除了,而数组大小并没有随之变小,而实际上这个size肯定要减少一个,带着思考,我们来看看size()方法:
public int size() {
if (mGarbage) {
gc();
}
return mSize;
}
首先第一个判断条件就是我们删除操作中的标识位,当执行了删除操作以后,执行gc方法,执行完成后返回全局的mSize,那么我们跟进这个gc方法,看看到底做了什么:
private void gc() {
//原始大小
int originSize = mSize;
//回收之后的大小
int afterGcSize = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < originSize; i++) {
Object val = values[i];
//如果该位置的元素没有被删除
if (val != DELETED) {
//一旦这两个值不相等(只要一个元素发生了删除,该元素以后这两个值始终不相等,并且afterGcSize始终小于 i)
if (i != afterGcSize) {
//元素移位操作
//将第i个的元素移到 上一个位置
keys[afterGcSize] = keys[i];
values[afterGcSize] = val;
values[i] = null;
}
//没有删除元素就自增
afterGcSize++;
}
}
mGarbage = false;
mSize = afterGcSize;
}
这边把方法命名重新修改了一下,方便阅读,那么整个方法下来,其实就是数组元素移位,将标记为删除元素之后的元素往前移动到该位置,mSize被重新被赋值为为afterGcSize的大小即真正未删除元素的大小,然后将mGarbage重置为false。
如果为有一个keys为[-1,2,4]对应值为[A,B,C]的SparseArray,为现在将Key为2的删除,下面用动图模拟一下当调用size时候执行gc的过程:
其中 i 表示循环执行的次数,注意看afterGcSize 变化的时机,然后最后gc后的状态,请大家记住,后面还能用到。
gc之后 size = 2, 但是values数组长度还是3
gc过程就是把元素前移去填补删除到元素,然后返回真正存在元素到大小作为size,这也再次解释了为什么全局会有一个mSize而不是使用数组长度作为size了。
我们再来继续看看改的方法,第一个方法还是我们刚刚看过的对检查并gc的方法,gc过后然后和对相应下标做赋值操作:
public void setValueAt(int index, E value) {
if (mGarbage) {
gc();
}
mValues[index] = value;
}
看完这些,我们再回过头来分析我们“最复杂”的方法-增:
先来看看put:
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
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++;
}
}
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
int[] newArray = new int[growSize(currentSize)];
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
5.首先我们以Keys的调用来分析入参:
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
分别传入的是存Key的数组,当前大小,即将存放key的下标、即将被存放的key。
方法中第一个if体:我们的mSize小于或者等于数组的长度执行一部分逻辑,注意看System.arraycopy方法,这个方法入参的src 和 des 其实就是他本身,那么整段逻辑下来就是必要的时候对数组进行移位操作,移位操作完成后,对index进行赋值操作,所以虽然是insert方法,这里其实没有对数组进行扩容,而是重复利用了空间。那么为什么走到这个条件体呢?回顾之前我们对gc流程,一旦对元素进行删除并且调用了gc之后,存key的数组长度肯定是大于mSize的。
6. 再往下就是我们真正的扩容操作了:
int[] newArray = new int[growSize(currentSize)];
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
这里的扩容和Arraylist的自增当前容量一半的扩容方式不同的是,当小于4直接是扩容到8,否则直接翻倍。
7.这里我们inser方法分析完成,最终通过这个方法完成了对存键值对数组的互用或者扩,至此put方法分析基本完成。