Android有一组自己的集合类,原因是使用java的集合太占内存。这里主要介绍下SparseXXX系列的容器。
SparseArray将int映射成object,类似Map
另外,与HashMap的区别是,由于key是int的,因此其查找使用的是二分查询,而不是hashMap的哈希查询,因此SparseArray不适合大数据的存储,二分查找毕竟比不上哈希查询的效率哈。对于删除key而言,SparseArray不会删除key后立即收缩数据,而会先在该位置做个标记,如果后面再插入相同key,那么是可以复用的。
另外SparseXXX系列还有:
SparseArray使用二分查找key,意味着key是有序的。
SparseArray的接口和Map的接口很类似,put、get、indexOfKey、indexOfValue、clear、size等等。
唯一多的一个是append(int key,E value),等同于put,但是如果key大于目前所有的key,那么会得到优化。至此为什么?下面来细细看一看。
public class SparseArray implements Cloneable {
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
}
可以看到由于键是int的,又为了避免自动装箱,因此用了int数组。mGarbage字段表示是否要进行删除标记点整理。DELETED字段就是上面所讲的删除标记。
put方法如下所示:
public void put(int key, E value) {
//二分查找得到该key的index,i>=0表示存在该值;负数表示不存在
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//已经存在该key了,替换值即可
if (i >= 0) {
mValues[i] = value;
}
//没找到该key,执行插入操作
else {
//取反,得到小于key的第一个索引位置
i = ~i;
//如果这个位置之前做了删除标记,那么回收该位置,这种case不需要扩展数组
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//执行gc,
if (mGarbage && mSize >= mKeys.length) {
gc();
// 重新二分查找
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//插入数据,可能会扩展数组
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
可以看到,put操作主要是执行二分查找,找到要插入key的位置,这个位置可能是一个已经做了删除标记的位置,那么直接复用好了;如果不是,那么可能需要先做一次gc,然后在找到新的位置,最后是插入数据。
上面的方法有一点需要注意:重用删除标记时,没有增加mSize,看来这个mSize的计算另有门道。
其中gc方法如下:
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
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) {
//i和o不相同,执行复制操作,从mKeys、mValues引用切换到keys、values上
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方法主要是整理现有数据。可以发现gc并没有去动态减小之前申请的数组大小,因此如果一开始不断插入,将数组扩展的很大,后面删除了不用,那空间还是放在那里的。
触发gc方法的条件是:mGarbage=true&&mSize>=mKeys.Length
每真正插入一个数据,mSize会+1,当当前分配的数组都插入过了,即mSize==mKeys.Length,且mGarbage=true,那么需要执行因此gc。
看到这里,是不是想到了虚拟机的gc算法中的:标记清除-标记整理。
删除操作是标记清除,添加操作是标记整理。整理数据,解决碎片
append是SparseArray与Map区分的一个方法,其源码如下:
public void append(int key, E value) {
//如果key的范围已经包含了,那么调用put
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
//如果key大于已有key的范围,那么理论上,该key的位置应该是最后一个
if (mGarbage && mSize >= mKeys.length) {
gc();
}
//追加一个数据
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
查询方法大同小异,看一个就好。
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];
}
}
二分查找得到索引,如果不存在key或者key已经标记清除了,返回默认值;否则返回值。
public int size() {
if (mGarbage) {
gc();
}
return mSize;
}
可以看到,如果需要执行标记清理,那么首先要执行标记清理,然后才能返回size。在put中,如果重用删除的位置,size不递增,可以发现这个size如果不先gc,那么是不准确的。
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,将其标记清除,且将mGarbage置为true,该值初始化时为false。也意味着只要有成功删除的操作,mGarbage就会为true,也就表示当前有碎片存在,那么在下次插入,如果需要扩展数组时,首先应该先去执行一把整理操作。
SparseArray是Android特有的数据结构,目的是代替Map