上篇文章是SparseArray源码解析,这篇文章分析下ArrayMap
的源码。
在移动设备端内存资源很珍贵,HashMap为实现快速查询带来了很大内存的浪费。为此,2013年5月20日Google工程师Dianne Hackborn在Android系统源码中新增ArrayMap类,从Android源码中发现有不少提交专门把之前使用HashMap的地方改用ArrayMap,不仅如此,大量的应用开发者中广为使用。
引文来自——深度解读ArrayMap优势与缺陷- Gityuan博客| 袁辉辉的Android博客
在移动开发中,推荐使用ArrayMap代替HashMap,主要考虑到内存的优化。ArrayMap使用两个数组进行数据存储,一个是记录key的hash值的数组mHashes
,另外一个是记录Value值的数组mArray
,和SparseArray一样,ArrayMap会对key的hash值使用二分法进行从小到大自然排序,在操作增删改查的时候都是先使用二分查找法得到相应的index,然后通过index来进行增删改查的操作。所以,应用场景和SparseArray的一样,如果在数据量比较大的情况下,那么它的性能将退化至少50%。采用网上的一张图片,很清晰地解释了ArrayMap
的内部结构:
简单的使用demo:
ArrayMap arrayMap = new ArrayMap();
arrayMap.put(1, "a");
arrayMap.put(5, "e");
arrayMap.put(4, "d");
arrayMap.put(10, "h");
arrayMap.put(2, null);
arrayMap.put(null, "i am null");
arrayMap.put(null, "i am null too");
Log.d("zzh", "onCreate: onCreate() called with: arrayMap = [" + arrayMap + "]");
//输出log
//zzh: onCreate: onCreate() called with: arrayMap = [{null=i am null too, 1=a, 2=null, 4=d, 5=e, 10=h}]
1 ArrayMap
类图
一般的集合类都会实现
Cloneable
接口并重写
Object
的
clone()
方法,但是
ArrayMap
并没有这样做,
ArrayMap
仅仅实现了
Map
接口。
2 ArrayMap
成员变量和构造器
/**
* Attempt to spot concurrent modifications to this data structure.
*
* It's best-effort, but any time we can throw something more diagnostic than an
* ArrayIndexOutOfBoundsException deep in the ArrayMap internals it's going to
* save a lot of development time.
*
* Good times to look for CME include after any allocArrays() call and at the end of
* functions that change mSize (put/remove/clear).
*/
private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;
/**
* The minimum amount by which the capacity of a ArrayMap will increase.
* This is tuned to be relatively space-efficient.
*/
private static final int BASE_SIZE = 4;
/**
* Maximum number of entries to have in array caches.
*/
private static final int CACHE_SIZE = 10;
//不可变的数组,是个哨兵
static final int[] EMPTY_IMMUTABLE_INTS = new int[0];
/**
* @hide Special immutable empty ArrayMap.
* 不可变的ArrayMap实例
*/
public static final ArrayMap EMPTY = new ArrayMap<>(-1);
/**
* Caches of small array objects to avoid spamming garbage. The cache
* Object[] variable is a pointer to a linked list of array objects.
* The first entry in the array is a pointer to the next array in the
* list; the second entry is a pointer to the int[] hash code array for it.
*/
//用于缓存大小为4的ArrayMap,mBaseCacheSize记录着当前已缓存的数量,超过10个则不再缓存;
static Object[] mBaseCache;
//记录已经缓存的数目
static int mBaseCacheSize;
//用于缓存大小为8的ArrayMap,mTwiceBaseCacheSize记录着当前已缓存的数量,超过10个则不再缓存
static Object[] mTwiceBaseCache;
//记录已经缓存的数目
static int mTwiceBaseCacheSize;
//这个变量决定key的hash的生成方法,你应该明白System.identityHashCode(key)和key.hashCode()的区别
final boolean mIdentityHashCode;
//保存key的hash值的数组
int[] mHashes;
//保存key/value的数组。
Object[] mArray;
//已经存放的元素的个数
int mSize;
MapCollections mCollections;
ArrayMap
有四个构造器:
public ArrayMap() {
this(0, false);
}
public ArrayMap(int capacity) {
this(capacity, false);
}
/** {@hide} */
public ArrayMap(int capacity, boolean identityHashCode) {
mIdentityHashCode = identityHashCode;
// If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS
// instance instead of the usual EmptyArray.INT. The reference
// is checked later to see if the array is allowed to grow.
if (capacity < 0) {
mHashes = EMPTY_IMMUTABLE_INTS;
mArray = EmptyArray.OBJECT;
} else if (capacity == 0) {
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
} else {
allocArrays(capacity);
}
mSize = 0;
}
public ArrayMap(ArrayMap map) {
this();
if (map != null) {
putAll(map);
}
}
其中第三个构造器是@hide
的,无法被外界调用。第三个构造器调用了allocArrays
方法:
private void allocArrays(final int size) {
if (mHashes == EMPTY_IMMUTABLE_INTS) {
throw new UnsupportedOperationException("ArrayMap is immutable");
}
if (size == (BASE_SIZE*2)) {
synchronized (ArrayMap.class) {
if (mTwiceBaseCache != null) {
//查看之前是否有缓存的 容量为8的int[]数组和容量为16的object[]数组
//如果有,复用给mArray mHashes
final Object[] array = mTwiceBaseCache;
mArray = array;
mTwiceBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mTwiceBaseCacheSize--;
return;
}
}
} else if (size == BASE_SIZE) {
synchronized (ArrayMap.class) {
if (mBaseCache != null) {
//查看之前是否有缓存的 容量为4的int[]数组和容量为8的object[]数组
//如果有,复用给mArray mHashes
final Object[] array = mBaseCache;
mArray = array;
mBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mBaseCacheSize--;
return;
}
}
}
//构建mHashes和mArray,mArray是mHashes容量的两倍。因为它既要存key还要存value。
mHashes = new int[size];
mArray = new Object[size<<1];
}
构造器中调用allocArrays
方法,由于mTwiceBaseCache
和mBaseCache
这两个数组默认都是null,因此方法里面的if
和else if
这两个条件语句肯定不会执行,方法也不会提前return,这两个条件暂时先不管,后续再分析,所以只看最后两行代码,很简单,分别初始化两个数组,其中mArray
是mHashes
的容量的两倍。
3 ArrayMap
核心操作
3.1 增加操作:put(K key, V value)
public V put(K key, V value) {
final int osize = mSize;
final int hash;
int index;
if (key == null) {
hash = 0;
index = indexOfNull();
} else {
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);
}
if (index >= 0) {
index = (index<<1) + 1;
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
//index<0,说明是插入操作。 对其取反,得到应该插入的下标
index = ~index;
//这个if表示需要扩容
if (osize >= mHashes.length) {
//如果容量大于8,则扩容一半。
//否则容量大于4,则扩容到8.
//否则扩容到4
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();
}
//复制临时数组中的数组进新数组
if (mHashes.length > 0) {
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
//释放临时数组空间并进行缓存
freeArrays(ohashes, oarray, osize);
}
//表示要插入的位置index在原来的数组的中间,则需要移动数组,腾出中间的位置
if (index < osize) {
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();
}
}
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
首先根据key计算出hash,然后根据hash利用二分查找计算出index
,index<0
表示这个key不存在(那么就需要插入而且~index
就是要插入的位置),否则表示key存在。key存在的话就很简单了,只需要把mArray
保存的value覆盖更新下就行了,然后提前return
。后续的代码是对key不存在的情况进行的处理,可以看出代码很多,有点复杂。index = ~index
表示取到要插入的位置,然后我们开始分析这句代码后面的代码,可以看到后面的代码有三个if
语句:
- 第一个
if
含义很清晰,表示当前元素个数osize
>= 数组的容量mHashes.length
,那么肯定就要扩容,扩容后的容量分三种情况,一行条件表达式代码完成的,大家肯定都能看懂,不解释了。如果容量大于8,则扩容一半(1.5倍),否则容量大于4,则扩容到8,否则扩容到4。有了扩容后的容量n
,对n
调用allocArrays(final int size)
方法,这个方法继续根据新的容量处理了扩容后续的操作,在介绍第三个构造器的时候讲过这个方法,在这个时机allocArrays(final int size)
方法里的if
和else if
这两个条件语句也不会执行,因为mTwiceBaseCache
和mBaseCache
这两个数组还是null还没有初始化,后续再分析这两个条件语句。扩容操作还没结束,需要把老的两个数组拷贝到扩容后的新的两个数组,两行System.arraycopy
完成了最后的扩容操作。扩容之后会调用freeArrays
方法,这个方法主要是释放原来的两个数组并添加缓存,缓存用mTwiceBaseCache
和mBaseCache
保存,这两个缓存主要在扩容的时候用到,扩容时,先查看之前是否有缓存的 int[]数组和object[]数组,如果有,复用给mArray
和mHashes
。 - 完成了上面的扩容操作,后续的第二个
if
表示进行插入操作,如果index < osize
,表示需要在原来数组的中间的某个index下标插入,进行数组的拷贝操作System.arraycopy
来腾出index的位置。 - 第三个
if
表示并发操作异常。然后对插入的位置下标进行赋值,最后对mSize进行自增操作。
3.2 删除操作:remove(Object key)
和removeAt(int index)
public V remove(Object key) {
final int index = indexOfKey(key);
if (index >= 0) {
return removeAt(index);
}
return null;
}
public V removeAt(int index) {
final Object old = mArray[(index << 1) + 1];
final int osize = mSize;
final int nsize;
//如果之前的集合长度小于等于1,则执行过删除操作后,集合现在就是空的了
if (osize <= 1) {
// Now empty.
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
freeArrays(ohashes, oarray, osize);
nsize = 0;
} else {//根据元素数量和集合占用的空间情况,判断是否要执行收缩操作
nsize = osize - 1;
//如果 mHashes长度大于8,且 集合长度 小于当前空间的 1/3,则执行一个 shrunk,收缩操作,避免空间的浪费
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
// Shrunk enough to reduce size of arrays. We don't allow it to
// shrink smaller than (BASE_SIZE*2) to avoid flapping between
// that and BASE_SIZE.
//如果当前集合长度大于8,则n为当前集合长度的1.5倍。否则n为8.
//n 为收缩后的 mHashes长度
final 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 {//不需要收缩
//类似ArrayList,用复制操作去覆盖元素达到删除的目的。
if (index < nsize) {
System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
(nsize - index) << 1);
}
//记得置空,以防内存泄漏
mArray[nsize << 1] = null;
mArray[(nsize << 1) + 1] = null;
}
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
mSize = nsize;
//返回删除的值
return (V)old;
}
删除操作的解释都放在注释里了。
3.3 查找操作:get(Object key)
、keyAt(int index)
和valueAt(int index)
public V get(Object key) {
final int index = indexOfKey(key);
return index >= 0 ? (V)mArray[(index<<1)+1] : null;
}
public int indexOfKey(Object key) {
return key == null ? indexOfNull()
: indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
}
public K keyAt(int index) {
return (K)mArray[index << 1];
}
public V valueAt(int index) {
return (V)mArray[(index << 1) + 1];
}
查找操作的核心方法是indexOfNull()
和indexOf(Object key, int hash)
:
int indexOfNull() {
final int N = mSize;
//如果当前集合是空的,返回~0即-1
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
//根据hash值=0,通过二分查找,查找到目标index
int index = binarySearchHashes(mHashes, N, 0);
//如果index<0,表示数组中还不存在key为null的情况
// If the hash code wasn't found, then we have no entry for this key.
if (index < 0) {
return index;
}
//如果index>=0,已经存在key等于null的key,找到mArray数组中的对应的key,比对key是否等于null。相等的话,返回index,否则表示hash冲突
//关于array中对应数据的位置,是index*2 = key ,index*2+1 = value.
// If the key at the returned index matches, that's what we want.
if (null == mArray[index<<1]) {
return index;
}
//以下两个for循环是在出现hash冲突的情况下,找到正确的index的过程:
//从index+1,遍历到数组末尾,找到hash值相等,且key相等的位置,返回
// Search for a matching key after the index.
int end;
for (end = index + 1; end < N && mHashes[end] == 0; end++) {
if (null == mArray[end << 1]) return end;
}
//从index-1,遍历到数组头,找到hash值相等,且key相等的位置,返回
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
if (null == mArray[i << 1]) return i;
}
// key没有找到,返回一个负数。代表应该插入的位置
// Key not found -- return negative value indicating where a
// new entry for this key should go. We use the end of the
// hash chain to reduce the number of array entries that will
// need to be copied when inserting.
return ~end;
}
int indexOf(Object key, int hash) {
final int N = mSize;
//表示当前元素个数为0,如果当前集合是空的,返回~0即-1
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
//根据hash值,通过二分查找,查找到目标index
int index = binarySearchHashes(mHashes, N, hash);
//如果index < 0,说明该hash值之前没有存储过数据
// If the hash code wasn't found, then we have no entry for this key.
if (index < 0) {
return index;
}
//如果index>=0,说明该hash值,之前存储过数据,找到对应的key,比对key是否相等。相等的话,返回index,否则表示hash冲突
// If the key at the returned index matches, that's what we want.
if (key.equals(mArray[index<<1])) {
return index;
}
//以下两个for循环是在出现hash冲突的情况下,找到正确的index的过程:
//从index+1,遍历到数组末尾,找到hash值相等,且key相等的位置,返回
// Search for a matching key after the index.
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
if (key.equals(mArray[end << 1])) return end;
}
//从index-1,遍历到数组头,找到hash值相等,且key相等的位置,返回
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
if (key.equals(mArray[i << 1])) return i;
}
// key没有找到,返回一个负数。代表应该插入的位置
// Key not found -- return negative value indicating where a
// new entry for this key should go. We use the end of the
// hash chain to reduce the number of array entries that will
// need to be copied when inserting.
return ~end;
}
从源码中可以看出,查找的时候可能存在hash冲突的情况,那么则从需要从目标点向两头遍历(两个for
循环),找到正确的index。
参考文献:
- 面试必备:ArrayMap源码解析
- 深度解读ArrayMap优势与缺陷- Gityuan博客| 袁辉辉的Android博客