说到Android 经常使用的数据结构,那不得不提一下SparseArray(稀疏数组),咱们在不少业务以及Android源码中能见到java
简单来说就是一个使用int做为Key的 Map ,官网的介绍就是: SparseArrays map integers to Objects数组
它继承自Object,实现了Cloneable:性能优化
public class SparseArray<E> implements Cloneable {}
复制代码
其中E就是咱们的泛型参数,即咱们要存入数据的类型markdown
public SparseArray() {
this(10);
}
public SparseArray(int initialCapacity) {
}
复制代码
咱们能够看到,它有两个构造方法,一个参数为容量大小,另外一个无参构造方法,最终调用的是容量为10的的构造方法。数据结构
既然是一个数据结构,固然要从增删改查来介绍它的基本用法:app
提供了put和append方法让使用能够放以int 做为key,任何类型做为值的数据。oop
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) 复制代码
indexOfValue方法经过value值查下标的话,若是多个key都使用了相同的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方法分析基本完成。