最近在写framework层的系统服务,发现Android 12中用来去重注册监听的map都是用的ArrayMap,因此仔细研究了ArrayMap的原理。
目录
3. public boolean containsKey(Object key)
4. public int indexOfKey(Object key)
5. public boolean containsValue(Object value)
6. public int indexOfValue(Object value)
9. public V remove(Object key)
10. public V removeAt(int index)
14. public V valueAt(int index)
一. ArrayMap概述
ArrayMap是一个key-value的数据结构,它比HashMap有更高的内存效率。
它映射到两个数组结构:一个整数数组mHashes,保存key的hash code;一个对象数据mArray,顺序保存key-value。
它可以避免为push到map的item创建额外的对象,而且它试图控制这些数组大小的增长(因为增长数据大小只需要复制数组中的item即可,不需要重建hash map)。
它不适用于大量数据的存储,通常会比HashMap慢,因为查找需要二分法,而且添加和删除操作需要对数组中entries进行相应的插入和删除(通常数组中间元素的插入和删除效率很低,因为会导致插入或者删除位置之后的元素整体移动,请参考后边的remove和put函数解析),对于包含数百个元素的容器来说,性能差异不显著,小于50%。
因为ArrayMap是为了更好的平衡内存,和大部多数Java标准containers不同,当删除item时它会缩小数组。目前你无法控制这种(shrinking)缩小——如果您设置了capacity然后删除一个item,他可能会减少capacity来匹配目前大小。将来明确设置capacity应该会关闭这种激进的shriking行为。
二. ArrayMap源码解析
public final class ArrayMap<K, V> implements Map<K, V> {}
1.主要包含的成员变量
private static final int BASE_SIZE = 4;
private static final int CACHE_SIZE = 10;
private final boolean mIdentityHashCode;
int[] mHashes;
Object[] mArray;
int mSize;
private MapCollections<K, V> mCollections;
注意:mSize表示数组mHashes的大小,而mArray的大小为2*mSize。mHashes中升序存放key的hash值,mArray中顺序存储了key和value。若key的hash在mHashes的位置索引为index,key在mArray中的位置keyIndex= index<<1 = index*2,value在mArray中的位置valueIndex= (index<<1) + 1 = index*2 + 1。
ArrayMap的两个数组存储结构如下:
2.构造函数
public ArrayMap()
public ArrayMap(int capacity)
public ArrayMap(int capacity, boolean identityHashCode)
public ArrayMap(ArrayMap<K, V> map)
3. public boolean containsKey(Object key)
判断Array中是否存在该key,如果该key存在则返回true,否则false。该方法调用indexOfKey(key) >= 0来实现。
4. public int indexOfKey(Object key)
如果该key在数组中,则返回其index,否则返回一个负数。
它核心就是根据该key是否是null,来判断调用indexOf(Object key, int hash)还是indexOfNull(),该这两个方法中使用二分查找key。
5. public boolean containsValue(Object value)
如果该value存在则返回true,否则返回false。该方法调用indexOfValue(value) >= 0来实现。
6. public int indexOfValue(Object value)
如果该Object存在,则返回其index,否则返回-1。
该方法的查找效率没有indexOfKey()快,因为该方法是线性查找数组mArray,分为object = null和object = null的情况。
7.核心get方法
public V get(Object key)
public V get(Object key) {
final int index = indexOfKey(key);
return index >= 0 ? (V)mArray[(index<<1)+1] : null;
}
注意:<<代表左移运算符,<<1表示左移1位,低位补0,相当于乘以2。
8.核心put方法
public V put(K key, V value)
给ArrayMap中添加一个新的value,如果key已存在,则会用参数中的value覆盖其原来对应的value值。返回值是给定key对应的老的value,如果该key不存在,则返回null。
该方法很长主要分为以下部分
- 根据key是否为null,调用indexOf()或者indexOfNull()方法二分查找该key是否存在。
- 如果mHashes数组中该key存在,则用新的value值覆盖旧的value。
public V put(K key, V value) {
final int osize = mSize;
final int hash;
int index;
//查找key是否存在,若存在,则返回其index
if (key == null) {
hash = 0;
index = indexOfNull();
} else {
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);
}
//key存在,用新value替换就的value,覆盖旧值
if (index >= 0) {
index = (index<<1) + 1;
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
...
}
- 如果mHashes数组中该key不存在,则二分查找返回的iindex取反就是要该key要插入的位置。(请参考后边的二分查找算法)
-
给mHashes数组和mArray数组扩容,如果ArrayMap现有数组长度osize > (BASE_SIZE*2),则扩容到3*osize,否则扩容为8或者4。
public V put(K key, V value) {
...
index = ~index;
if (osize >= mHashes.length) {
//osize=mSize,插入前mHashes数组的长度,BASE_SIZE=4
final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
: (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
if (mHashes.length > 0) {
if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
freeArrays(ohashes, oarray, osize);
}
...
}
- 如果要加入的元素index在mHashes数组中间,则把mHashes和mArray中index及之后的元素整体后移一位,即使用System.arraycopy()实现。
- 然后把要put的key和value分别插入对应下标的数组中。
public V put(K key, V value) {
...
if (index < osize) {
if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
+ " to " + (index+1));
//要插入的key的index在mHashes数组中间,则需要将mHashes中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();
}
}
//把要push的key和value分别加入mHashes数组以及mArray数组中
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
//数组长度加1
mSize++;
return null;
}
以上就是ArrayMap的整个put方法过程,因为新增元素涉及到插入位置后的两个数组元素的整体后移(复制),这就是ArrayMap顺序存储效率慢的原因。
9. public V remove(Object key)
删除给定key对应的元素,该方法会同时删除mHashes中和mArray数组中的元素,并引起Shrink数组。
如果该key存在,则会返回删除的该key对应的value,否则返回null。
该方法先调用indexOfKey()二分查找该key对应的index,如果index>=0,则调用removeAt()实现删除。
10. public V removeAt(int index)
该方法是ArrayMap删除数据的核心,我们解析下代码:
- 已知要删除的index,可以获取该index对应的value值,仅通过一行代码就可以实现
final Object old = mArray[(index << 1) + 1];
- 判断当前数组的长度mSize<=1,如果true,直接把mHashes和mArray数组赋值为EmptyArray,然后释放空间。这是一次shrink。
public V removeAt(int index) {
if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
// The array might be slightly bigger than mSize, in which case, indexing won't fail.
// Check if exception should be thrown outside of the critical path.
throw new ArrayIndexOutOfBoundsException(index);
}
final Object old = mArray[(index << 1) + 1];
final int osize = mSize;
final int nsize;
if (osize <= 1) {
// Now empty.
if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
freeArrays(ohashes, oarray, osize);
nsize = 0;
}
...
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
mSize = nsize;
return (V)old;
}
- 如果mHashes数组长度>8或者mSize<mHashes.length/3,则会触发shrink数组,并删除该元素。
- 否则删除该index对应的mHashes和mArray中的元素。
System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1, (nsize - index) << 1);
11. public void clear()
清空arraymap,会释放所有的存储空间。该方法中将数组mHashes和mArray赋值EmptyArray,并释放存储空间。
12.public void erase()
该方法只将mArray数组中的元素全部置为null,不释放ArrayMap的存储空间。
13. public K keyAt(int index)
获取指定index对应的Key值。
public K keyAt(int index) {
if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
// The array might be slightly bigger than mSize, in which case, indexing won't fail.
// Check if exception should be thrown outside of the critical path.
throw new ArrayIndexOutOfBoundsException(index);
}
return (K)mArray[index << 1];
}
14. public V valueAt(int index)
获取指定index对应的value值
public V valueAt(int index) {
if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
// The array might be slightly bigger than mSize, in which case, indexing won't fail.
// Check if exception should be thrown outside of the critical path.
throw new ArrayIndexOutOfBoundsException(index);
}
return (V)mArray[(index << 1) + 1];
}
15. 二分查找的实现
ArrayMap的二分查找是调用的ContainerHelper工具类中的binarySearch()方法。
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size -1;
while (lo <= hi) {
final int mid = (lo + li) >>> 1;
final int minVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid;
}
}
return ~lo;
}
16. System.arraycopy()
private static void arraycopy(int[] src, int srcPos, int[] dst, int dstPos, int length)