SparseArray源码分析
Posted xingfeng_coder
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SparseArray源码分析相关的知识,希望对你有一定的参考价值。
android有一组自己的集合类,原因是使用java的集合太占内存。这里主要介绍下SparseXXX系列的容器。
SparseArray将int映射成object,类似Map<Integer,Object>,但是由于不需要自动装箱,因此会更节约内存。
另外,与HashMap的区别是,由于key是int的,因此其查找使用的是二分查询,而不是hashMap的哈希查询,因此SparseArray不适合大数据的存储,二分查找毕竟比不上哈希查询的效率哈。对于删除key而言,SparseArray不会删除key后立即收缩数据,而会先在该位置做个标记,如果后面再插入相同key,那么是可以复用的。
另外SparseXXX系列还有:
- SparseIntArray:int映射到int
- SparseBooleanArray:int映射到boolean
- SparseLongArray:int映射到long
- LongSparseArray:long映射到object
SparseArray使用二分查找key,意味着key是有序的。
API说明
SparseArray的接口和Map的接口很类似,put、get、indexOfKey、indexOfValue、clear、size等等。
唯一多的一个是append(int key,E value),等同于put,但是如果key大于目前所有的key,那么会得到优化。至此为什么?下面来细细看一看。
源码解释
字段
public class SparseArray<E> 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方法
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
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++;
查询方法
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];
二分查找得到索引,如果不存在key或者key已经标记清除了,返回默认值;否则返回值。
size
public int size()
if (mGarbage)
gc();
return mSize;
可以看到,如果需要执行标记清理,那么首先要执行标记清理,然后才能返回size。在put中,如果重用删除的位置,size不递增,可以发现这个size如果不先gc,那么是不准确的。
删除操作
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,将其标记清除,且将mGarbage置为true,该值初始化时为false。也意味着只要有成功删除的操作,mGarbage就会为true,也就表示当前有碎片存在,那么在下次插入,如果需要扩展数组时,首先应该先去执行一把整理操作。
总结
SparseArray是Android特有的数据结构,目的是代替Map<Integer,Object>这种情况,内部使用了二分查找进行查找。同时利用了gc算法:标记清除-标记整理的思路,只要理解了这个,SparseArray的核心思想迎刃而解。
关注我的技术公众号,与君共同学习。微信扫一扫下方二维码即可关注:
以上是关于SparseArray源码分析的主要内容,如果未能解决你的问题,请参考以下文章