ArrayMap是谷歌推出的一个数据容器,也是为内存更珍贵的移动端设计的。和SparseArray不同之处在于,ArrayMap选择了使用<K,V>结构,当key的类型不为Integer的时候,可以选择使用ArrayMap,因此它的使用场景更广。
源码解析
以android.util.ArrayMap为例,SDK=28
整体结构
ArrayMap实现了Java中的Map接口,拥有相同的方法特征,但是ArrayMap没有实现额外的Entry,而是通过数组存储了所有的hash code和key-value pair。
成员变量
ArrayMap中比较重要的成员变量:
private static final int BASE_SIZE = 4; //扩容的最小值
private static final int CACHE_SIZE = 10; //缓存数组的上限
static final int[] EMPTY_IMMUTABLE_INTS = new int[0]; //没有其他用处,只是初始化容量传负数时标记抛异常用
static Object[] mBaseCache; //缓存大小为4的ArrayMap,该进程内唯一
static int mBaseCacheSize; //当前已缓存的大小为4的ArrayMap数量,超过10不再缓存
static Object[] mTwiceBaseCache; //缓存大小为8的ArrayMap
static int mTwiceBaseCacheSize;
int[] mHashes; //存储key的hashcode的数组
Object[] mArray; //存储key-value对的数组,大小是mHashes的两倍
int mSize; //实际存储个数
数据结构
初始化
public ArrayMap() {
this(0, false);
}
默认创建的ArrayMap初始容量为0,mHashes和mArray都是空数组。元素被添加进来的时候会触发扩容。如果创建时指定一个大于0的Capacity,则调用allocArrays分配初始化容量
put
核心的方法当然还是元素的存储过程,和HashMap一样,如果要插入的位置没有值则返回null,否则返回旧值
public V put(K key, V value) {
final int osize = mSize;
final int hash;
int index;
//计算key和hashCode
if (key == null) {
hash = 0; //key为null就取0
index = indexOfNull();
} else {
//否则取的Object.hashCode()
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);
}
//这里查找index和SparseArray类似,都是通过二分查找,如果找到了直接覆盖旧值
if (index >= 0) {
index = (index<<1) + 1;
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
//没找到,通过取反获取要插入的位置
index = ~index;
//如果mSize>= mHashes数组的长度,要进行扩容
if (osize >= mHashes.length) {
final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
: (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
//分配新的空间
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
//拷贝数据到新数组中,包括mHashes和mArray
if (mHashes.length > 0) {
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
//将原数组进行回收缓存,因为原先的hash数组大小有可能为4或8
freeArrays(ohashes, oarray, osize);
}
if (index < osize) {
//index在mSize之内,需要将原数组index处的元素都向后移一位,空出index的位置存储新元素
System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
if (osize != mSize || index >= mHashes.length) {
throw new ConcurrentModificationException();
}
}
//插入hash、key、value,当前元素个数+1
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
扩容的逻辑是:
- 当前元素个数mSize<4,扩容后大小为4
- 4<=mSize<8,扩容后大小为8
- mSize>=8,扩容后大小为原先的1.5倍
indexOf
indexOf方法对null和非null的key做了区分,根据key的hashCode进行查找,主要看非null的情况:
int indexOf(Object key, int hash) {
final int N = mSize;
// 为了提高效率先做了判断,如果是空数组肯定是找不到的
if (N == 0) {
return ~0;
}
//通过二分查找找到该key对应的hash在mHashes中的位置
int index = binarySearchHashes(mHashes, N, hash);
// 为负说明没找到
if (index < 0) {
return index;
}
// mArray相应位置的元素和key相等,说明是同一个key,找到了
if (key.equals(mArray[index<<1])) {
return index;
}
// 如果key不相同,说明出现了hash冲突,和上面的数据结果示意的那样。那么就从index处先向后再向前遍历寻找
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;
}
// 还是没找到,就取反返回。这种情况比如新插入的key和已有的发生了hash冲突,那么就将该key插入到发生冲突的所有旧key的最后面。所以发生了hash冲突的情况下后加的key永远在后面,并且多个相同的hashCode一定是连续的
return ~end;
}
get、remove
get方法其实就是根据key的值,调用indexOf算出该key在mHashes数组中的位置,得出对应的mArray数组中的value。remove也是先计算出index,再调用removeAt完成元素的删除
public V removeAt(int index) {
final Object old = mArray[(index << 1) + 1];
final int osize = mSize;
final int nsize;
if (osize <= 1) {
// 一个小优化,如果数组中只有一个key-value对,remove后就成空数组了,所以直接置空和回收数组,不做其它判断
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
freeArrays(ohashes, oarray, osize);
nsize = 0;
} else {
//记录hash数组的尾部下标,同时也是删除元素后新的容量
nsize = osize - 1;
//如果hash数组的长度大于8,并且元素个数小于hash数组长度的1/3,则进行压缩
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
//n 为压缩后的mHashes长度,如果当前mSize大于8,则n=1.5*mSize,否则为8
int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
//容量改变了,要将原有元素拷贝到新数组
if (index > 0) {
System.arraycopy(ohashes, 0, mHashes, 0, index);
System.arraycopy(oarray, 0, mArray, 0, index << 1);
}
if (index < nsize) {
System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,(nsize - index) << 1);
}
} else {
if (index < nsize) {
//要删除的元素不在末位,需要将数组index后的元素都向前移一位
System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,(nsize - index) << 1);
}
//末位元素置为null
mArray[nsize << 1] = null;
mArray[(nsize << 1) + 1] = null;
}
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
mSize = nsize;
return (V)old;
}
这里设计的压缩后的容量至少是8,注释是避免在8和4之间来回调整,也就是阈值过小的话会频发触发扩容和压缩,用差别不大的内存使用来换取执行效率。能注意到多个方法中都出现了allocArrays和freeArrays,这两个方法是用来管理ArrayMap内存空间的,并且包含了对缓存的相关操作。
freeArrays
该方法的作用是回收原数组,缓存下来以复用。因为缓存是进程内唯一的,ArrayMap使用的场景比较多的时候,符合大小的数组都可以利用到缓存,从而避免了对象的频繁创建和回收
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
//释放的对象大小为8
if (hashes.length == (BASE_SIZE*2)) {
//值得注意的是,缓存是静态变量,被进程内所有ArrayMap共享,而ArrayMap本身是线程不安全的,所以这里用了类锁来同步,避免对缓存的并发修改。
synchronized (ArrayMap.class) {
//存储大小为8的对象的缓存池数量小于10,则加入缓存
if (mTwiceBaseCacheSize < CACHE_SIZE) {
//array[0]指向原先的缓存池
array[0] = mTwiceBaseCache;
//array[1]指向hash数组
array[1] = hashes;
for (int i=(size<<1)-1; i>=2; i--) {
//情况数组中的其它数据
array[i] = null;
}
//mTwiceBaseCache重新赋值,指向新加入缓存池的array,缓存池个数+1
mTwiceBaseCache = array;
mTwiceBaseCacheSize++;
}
}
} else if (hashes.length == BASE_SIZE) {
//释放的对象大小为4,逻辑同上
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++;
}
}
}
}
先理解一下这个缓存数组的数据结构,其实就是类似于LinkedList,新缓存的数组中的第0个位置指向原先的缓存数组,然后原先缓存数组重新指向当前的新缓存数组第0个位置供下次指向使用。第一个位置则一直指向mHashes。以mBaseCache为例,开始的时候为null,mBaseCacheSize=0,整体过程如下图:
在ArrayMap中的使用场景
- 数组不为空,clear清空数组时
- put时发现需要扩容,先allocArrays再freeArrays
- ensureCapacity时,如果当前容量小于预期容量,则先allocArrays再freeArrays
- remove时发现数组只有一个元素
allocArrays
//size为目标容量大小,该方法作用是将mHashes、mArray设置成指定容量
private void allocArrays(final int size) {
//如果要设置的容量为8或4,要特殊处理
if (size == (BASE_SIZE*2)) {
synchronized (ArrayMap.class) {
//如果缓存池不为空
if (mTwiceBaseCache != null) {
final Object[] array = mTwiceBaseCache;
//从缓存池取出array
mArray = array;
//将缓存池指向上一条缓存
mTwiceBaseCache = (Object[])array[0];
//从缓存中获取mHashes
mHashes = (int[])array[1];
array[0] = array[1] = null;
//缓存池大小减一
mTwiceBaseCacheSize--;
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,那么直接给hash和key-value数组分配空间即可
mHashes = new int[size];
mArray = new Object[size<<1];
}
如上图,和前面的freeArrays示意图相对应,这里参考了Gityuan的图例:链接,用自己更容易理解的方式画了一遍。标红色的部分是方法执行结束后各变量的结果,开始的时候mBaseCacheSize=2,将mArray指向当前缓存池,mHashes指向缓存池的第1个位置。因为缓存池第0个位置指向上一个缓存,所以只要将mBaseCache指向第0个位置就完成了缓存池的更新。allocArrays方法涉及了扩容和压缩,使用场景如下
- ArrayMap构造方法指定容量
- put时发现需要扩容,先allocArrays再freeArrays
- ensureCapacity时,如果当前容量小于预期容量,则先allocArrays再freeArrays
- remove时数组元素数量大于1,并且满足容量收缩的条件
ArraySet
ArraySet实现了Collection和Set接口,相比HashSet更节省内存。在设计上和ArrayMap很相似,但实现上还有区别。ArraySet的去重,是将元素的hashCode按二分查找插入到mHashes中,value插入到mArray对应的下标index。如果该value对应的hashCode已经存在则跳出。
内存比较
private HashMap<Integer, String> mMap = new HashMap<>();
private ArrayMap<Integer, String> mArrayMap = new ArrayMap<>();
public void test(){
for (int i = 0; i < 100000; i++) {
mMap.put(i, String.valueOf(i));
//mArrayMap.put(i, String.valueOf(i));
}
}
使用adb shell dumpsys meminfo 查看进程内存占用情况,下图是使用HashMap创建数据前内存使用情况(ArrayMap和这个差不多):
** MEMINFO in pid 19999 [com.example.dataset] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 11553 11476 0 0 16384 14930 1453
Dalvik Heap 1623 1468 0 0 3064 1528 1536
Dalvik Other 574 560 0 0
Stack 68 68 0 0
Ashmem 5 0 0 0
Other dev 12 0 12 0
.so mmap 3562 136 4 0
.apk mmap 3544 2092 252 0
.ttf mmap 50 0 0 0
.dex mmap 2640 0 576 0
.oat mmap 77 0 0 0
.art mmap 7074 6736 0 0
Other mmap 16 4 0 0
Unknown 422 416 0 0
TOTAL 31220 22956 844 0 19448 16458 2989
App Summary
Pss(KB)
------
Java Heap: 8204
Native Heap: 11476
Code: 3060
Stack: 68
Graphics: 0
Private Other: 992
System: 7420
TOTAL: 31220 TOTAL SWAP PSS: 0
使用HashMap创建数据后内存使用情况:
** MEMINFO in pid 19999 [com.example.dataset] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 11698 11636 0 0 16896 14986 1909
Dalvik Heap 7807 7752 0 0 15197 9053 6144
Dalvik Other 528 528 0 0
Stack 72 72 0 0
Ashmem 5 0 0 0
Other dev 12 0 12 0
.so mmap 3639 140 4 0
.apk mmap 3556 2092 252 0
.ttf mmap 50 0 0 0
.dex mmap 2884 0 640 0
.oat mmap 77 0 0 0
.art mmap 7248 6816 56 0
Other mmap 16 4 0 0
Unknown 448 444 0 0
TOTAL 38040 29484 964 0 32093 24039 8053
App Summary
Pss(KB)
------
Java Heap: 14624
Native Heap: 11636
Code: 3128
Stack: 72
Graphics: 0
Private Other: 988
System: 7592
TOTAL: 38040 TOTAL SWAP PSS: 0
使用ArrayMap创建数据后内存使用情况
** MEMINFO in pid 20275 [com.example.dataset] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 11759 11696 0 0 16896 14880 2015
Dalvik Heap 6639 6584 0 0 13370 7226 6144
Dalvik Other 496 496 0 0
Stack 72 72 0 0
Ashmem 5 0 0 0
Other dev 12 0 12 0
.so mmap 3637 140 4 0
.apk mmap 3556 2092 252 0
.ttf mmap 50 0 0 0
.dex mmap 2871 0 640 0
.oat mmap 77 0 0 0
.art mmap 7231 6796 56 0
Other mmap 16 4 0 0
Unknown 440 436 0 0
TOTAL 36861 28316 964 0 30266 22106 8159
App Summary
Pss(KB)
------
Java Heap: 13436
Native Heap: 11696
Code: 3128
Stack: 72
Graphics: 0
Private Other: 948
System: 7581
TOTAL: 36861 TOTAL SWAP PSS: 0
以Dalvik Heap- Heap Alloc作为参考标准,多次比较前后使用内存,计算出来ArrayMap相比HashMap能减少20%-30%的内存。