三种容器对比
项目 | ArrayMap | SparseArray | HashMap |
---|---|---|---|
概念 | 内存优化版的HashMap | key为int的性能优化版ArrayMap | 以大量内存实现存取O(1)的Map |
数据结构 | 两个数组: 一个存Key的Hash 另一个存Key和Value | 两个数组: 一个存Key 另一个存Value | 数组+链表/红黑树 |
应用场景 | 1. 数据量千以内; 2. 数据中有Map; | 1. 数据量千以内; 2. key必须是int类; | 前两者不适合时使用 |
- 三者都线程不安全
ArrayMap
数据结构
两个数组:
- mHashes: 存放key的哈希值
- mArray: 一个Object数组, 偶数索引存key值, 奇数索引存value
mHashes[index] = hash;
//用位运算优化
mArray[index<<1] = key; //等同于 mArray[index * 2] = key;
mArray[(index<<1)+1] = value; //等同于 mArray[index * 2 + 1] = value;
故mArray的大小是mHashes的两倍.
存放逻辑
get()
- 根据Key计算其哈希值(如果key是null,直接取0)
- 在mHashes数组中寻找哈希值对应的index
- 如果index<0,说明没找到,返回null;若index>0则在mArray中取出Value返回
其中, 第二步耗时最多, 使用二分查找来优化.
故ArrayMap的查询时间复杂度为 O(log n).
put()
- 根据Key计算其哈希值(如果key是null,直接取0)
- 在mHashes数组中寻找哈希值对应的index
- 若index>0则mArray已存在值,直接替换成新值即可.
- 若index<0, 说明当前的key不存在.将index取反,就可以得到要插入的下标.
- 根据index判断是否需要扩容
- 进行数组复制,腾出index的位置
- 将新值放入index位置
重点: 取下标indexOf()
int indexOf(Object key, int hash) {
final int N = mSize;
// Important fast case: 如果当前是空表,直接返回
if (N == 0) {
return ~0;//返回的是最大的负数
}
//二分查找,返回要插入下标的取反
int index = binarySearchHashes(mHashes, N, hash);
//小于0,说明key原来没有存,返回
if (index < 0) {
return index;
}
// 如果index>0,而且key刚好对应上,那刚刚好找到,返回
if (key.equals(mArray[index<<1])) {
return index;
}
//进行Hash碰撞下的查找
// 向后查找
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
if (key.equals(mArray[end << 1])) return end;
}
// 向前查找
for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
if (key.equals(mArray[i << 1])) return i;
}
//如果上面都没找到, 那么最后的end就是将要插入的位置,类似的,先取反再返回
return ~end;
}
缓存机制
- Android中在很多场景起初数据量都比较小, 为了避免频繁创建和回收数组对象, ArrayMap设计了两个缓冲池mBaseCache和mTwiceBaseCache.
- 只有大小是4和8时,才会触发缓存机制. 所以最好使用
new ArrayMap(4)
或new ArrayMap(8)
这样可以最大程度的减少对象的创建。
- 缓存复用的是大小为4或8的数组.
- 实现思想: 把要缓存的数组串成单向链表, 创建缓存就是链表头部插入元素, 复用就是删除链表头元素.
private static final int BASE_SIZE = 4;
创建缓存
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
if (hashes.length == (BASE_SIZE * 2)) { //缓存大小为8的数组的hashes,和array
synchronized (ArrayMap.class) {
// 当大小为8的缓存池的数量小于10个,则将其放入缓存池
if (mTwiceBaseCacheSize < CACHE_SIZE) {
//把 array[0]看作单向链表节点的next即可, 此处是把array接上原有缓存
array[0] = mTwiceBaseCache;
//将hashes保存到节点中
array[1] = hashes;
for (int i = (size << 1) - 1; i >= 2; i--) {
//清除不需要的数据
array[i] = null;
}
//把头结点指向array, 完成链表插入头部节点
mTwiceBaseCache = array;
mTwiceBaseCacheSize++;
}
}
} else if (hashes.length == BASE_SIZE) { //同理
synchronized (ArrayMap.class) {
if (mBaseCacheSize < CACHE_SIZE) {
array[0] = mBaseCache;
array[1] = hashes;
for (int i = (size << 1) - 1; i >= 2; i--) {
array[i] = null;
}
mBaseCache = array;
mBaseCacheSize++;
}
}
}
}
3层的缓存结构:
freeArrays()触发时机:
时机 | 原因 |
---|---|
removeAt()移除最后一个元素 | 此时不需要数组,试图回收 |
执行clear()清理 | 同上 |
执行ensureCapacity()在当前容量小于预期容量 | Sample |
执行put()在容量满的情况 | 同上 |
复用缓存
private void allocArrays(final int size) {
if (size == (BASE_SIZE*2)) { //当分配大小为8的对象,先查看缓存池
synchronized (ArrayMap.class) {
if (mTwiceBaseCache != null) { // 当缓存池不为空时
final Object[] array = mTwiceBaseCache;
//从缓存池中取出mArray
mArray = array;
//链表删除头结点, 即把原头结点的下一位变成头结点
mTwiceBaseCache = (Object[])array[0];
//从原头结点中取出mHashes
mHashes = (int[])array[1];
//清空原头结点的数据
array[0] = array[1] = null;
mTwiceBaseCacheSize--; //缓存池大小减1
return;
}
}
} else if (size == BASE_SIZE) { //同理
synchronized (ArrayMap.class) {
if (mBaseCache != null) {
final Object[] array = mBaseCache;
mArray = array;
mBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mBaseCacheSize--;
return;
}
}
}
// 分配大小除了4和8之外的情况,则直接创建新的数组
mHashes = new int[size];
mArray = new Object[size<<1];
}
2层缓存时的数据结构:
allocArrays触发时机
时机 | 原因 |
---|---|
执行ArrayMap的构造函数 | 构造最开始的数组 |
执行removeAt()在满足容量收紧机制时 | 尝试进行数组复用 |
执行ensureCapacity()在当前容量小于预期容量 | Sample |
执行put()在容量满的情况 | 同上 |
容量调整机制
扩张
- 在put中,进行检查,满足情况进行扩张
public V put(K key, V value) {
...
final int osize = mSize;
//当mSize大于或等于mHashes数组长度时需要扩容
if (osize >= mHashes.length) {
//不足4的补到4,不足8的补到8, 否则扩容1.5倍(osize + (osize >> 1))
final int n = osize >= (BASE_SIZE * 2) ? (osize + (osize >> 1))
: (osize >= BASE_SIZE ? (BASE_SIZE * 2) : BASE_SIZE);
allocArrays(n);
}
}
缩减
public V removeAt(int index) {
final int osize = mSize;
//当mSize大于1的情况,需要根据情况来决定是否要收紧
if (osize > 1) {
//当数组容量mSize大于8而且存储的数据量不足容量的1/3时, 进行缩减
if (mHashes.length > (BASE_SIZE * 2) && mSize < mHashes.length / 3) {
//mSize不足8时补8, 否则按照mSize的1.5倍申请数组
final int n = osize > (BASE_SIZE * 2) ? (osize + (osize >> 1)) : (BASE_SIZE * 2);
allocArrays(n);
}
}
}
- 当数组利用率不足1/3时, 其会缩减50%及以上( 1 3 \frac{1}{3} 31*1.5= 1 2 \frac{1}{2} 21)
参数mIdentityHashCode
- 默认情况为false, 即允许mHashes中出现相同的值的.
- 实现逻辑:
public V put(K key, V value) {
...
//如果为true, 使用的系统默认的HashCode,即根据内存地址计算, 每个对象都不同; 若为false, 则调用重写过的, 可以避免对象重复.
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);
...
}
SparseArray
- SparseArray的Key必须是int.
- 数据量在百级时性能比HashMap要好大概提升0-50%;
- 底层是两个数组mKeys和mValues分别存储Key和值两者索引对应.
扩容机制
- 使用帮助类GrowingArrayUtils 进行操作
public static int growSize(int currentSize) {
//扩容规则:当前容量小于5返回8;否则扩容一倍
return currentSize <= 4 ? 8 : currentSize * 2;
}
- 故其没有设置最大容量,
延时删除机制
- 在SparseArray删除时, 并不会直接操作数组, 而是将要删除的位置设置为标志位
DELETED
,同时会有一个标志位mGarbage
用于标识是否有潜在延迟删除的无效数据(只要有删除操作就会置true). - 优点: 减少数组操作,提升效率,如频繁删除不需要操作数组;插入元素到
DELETED
时,也不需要操作数组.
//一个常量Object充当标志位
private static final Object DELETED = new Object();
- 在插入元素时, 如果是
DELETED
就可以直接替换,不需要操作数组,效率更高.若不是,先根据mGarbage
进行数据清除gc()
,再进行数组操作.
//将正常元素往前挤,挤掉DELETED的位置
private void gc() {
//n代表gc前数组的长度;
int n = mSize;
int o = 0;//有效的下标长度
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
//每遇到一次DELETED,则i-o的大小+1;
if (val != DELETED) {
//之后遇到非DELETED数据,则将后续元素的key和value往前挪
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
//此时无垃圾数据,o的序号表示mSize的大小
mGarbage = false;
mSize = o;
}
gc()前后变化:
存取逻辑
put()
- 在mKeys中二份查找到key的index
- 如果index>0,说明数据已存在, 直接替换.
- 如果index<0或对应的Value是DELETE,说明数据不存在, 直接替换.
- 否则, 先进行gc(), 重新查找插入位置index,再将数据插入到
mKeys
和mValues
中
get()
- 在mKeys中二份查找到key的index
- 如果index<0或对应的Value是DELETE,说明数据不存在, 返回null. 否则返回value.
- 因为SparseArray的Key必须是int, 所以不需要处理key是null的情况
参考资料
ArrayMap分析1
ArrayMap分析2
ArrayMap分析3
SparseArray分析1
SparseArray分析2