提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
一、ArrayMap 是什么 ?
ArrayMap 跟 HashMap 一样实现了 Map 接口所以也是存储键值对结构的数据容器,但是存储元素的方式不 HashMap 不太一样,ArrayMap 使用两个数组,一个 int 数组用于保存所有 key 的 hash 值,一个 Object 数组保存所有的 key 和 value 并且这两个数组的元素顺序是一致的。另外为了减少频繁的创建和回收设计了两个缓存池分别缓存大小为 4 和 8 的 ArrayMap。ArrayMap 更节省内存但是效率低于 HashMap
提示:基于 Android SDK 30
二、源码分析
1. 主要成员
// ArrayMap 扩容后的最小值
private static final int BASE_SIZE = 4;
// 最大缓存 ArrayMap 的数量
private static final int CACHE_SIZE = 10;
// 缓存大小为 4 的 ArrayMap
static Object[] mBaseCache;
// 已经缓存的大小为 4 的 ArrayMap 的数量,大于等于 CACHE_SIZE 则不再缓存
static int mBaseCacheSize;
// 缓存大小为 8 的 ArrayMap
static Object[] mTwiceBaseCache;
// 已经缓存的大小为 8 的 ArrayMap 的数量,大于等于 CACHE_SIZE 则不再缓存
static int mTwiceBaseCacheSize;
// 当前存储的元素个数
int mSize;
2. 缓存机制
2.1 freeArrays
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
if (hashes.length == (BASE_SIZE*2)) {
synchronized (sTwiceBaseCacheLock) {
if (mTwiceBaseCacheSize < CACHE_SIZE) {
array[0] = mTwiceBaseCache;
array[1] = hashes;
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
mTwiceBaseCache = array;
mTwiceBaseCacheSize++;
if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
+ " now have " + mTwiceBaseCacheSize + " entries");
}
}
} else if (hashes.length == BASE_SIZE) {
synchronized (sBaseCacheLock) {
// 如果没有超出最大的缓存数量
if (mBaseCacheSize < CACHE_SIZE) {
// 将数组第一个位置指向原来的缓存数组
array[0] = mBaseCache;
// 将数组第二个位置指向 key 的 hash 数组
array[1] = hashes;
// 将除第一二个位置外其他位置置空
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
// 再把 mBaseCache 指向 Array 数组
mBaseCache = array;
// 缓存个数加 1
mBaseCacheSize++;
if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
+ " now have " + mBaseCacheSize + " entries");
}
}
}
}
freeArrays 只缓存大小为 4 或 8 的 ArrayMap,这里的 mBaseCache 相当于一个链表只不过链表中的元素是数组,数组中第一个元素指向下一个引用,第二个元素是所有 key 的 hash 数组,数组的其他元素都为 null
2.2 allocArrays
private void allocArrays(final int size) {
if (mHashes == EMPTY_IMMUTABLE_INTS) {
throw new UnsupportedOperationException("ArrayMap is immutable");
}
if (size == (BASE_SIZE*2)) {
synchronized (sTwiceBaseCacheLock) {
if (mTwiceBaseCache != null) {
final Object[] array = mTwiceBaseCache;
mArray = array;
try {
mTwiceBaseCache = (Object[]) array[0];
mHashes = (int[]) array[1];
if (mHashes != null) {
array[0] = array[1] = null;
mTwiceBaseCacheSize--;
if (DEBUG) {
Log.d(TAG, "Retrieving 2x cache " + mHashes
+ " now have " + mTwiceBaseCacheSize + " entries");
}
return;
}
} catch (ClassCastException e) {
}
// Whoops! Someone trampled the array (probably due to not protecting
// their access with a lock). Our cache is corrupt; report and give up.
Slog.wtf(TAG, "Found corrupt ArrayMap cache: [0]=" + array[0]
+ " [1]=" + array[1]);
mTwiceBaseCache = null;
mTwiceBaseCacheSize = 0;
}
}
} else if (size == BASE_SIZE) {
synchronized (sBaseCacheLock) {
if (mBaseCache != null) {
// 创建一个新的数组指向 mBaseCache 相当于链表头
final Object[] array = mBaseCache;
mArray = array;
try {
// 将 mBaseCache 指向下一个引用
mBaseCache = (Object[]) array[0];
// mHashes 赋值为链表中第一个元素的第二个元素(因为链表中是数组)
mHashes = (int[]) array[1];
if (mHashes != null) {
// 置空
array[0] = array[1] = null;
// 缓存数量减 1
mBaseCacheSize--;
if (DEBUG) {
Log.d(TAG, "Retrieving 1x cache " + mHashes
+ " now have " + mBaseCacheSize + " entries");
}
return;
}
} catch (ClassCastException e) {
}
// 如果出现异常把缓存置空
mBaseCache = null;
mBaseCacheSize = 0;
}
}
}
// 容量不是 4 也不是 8 直接创建数组
mHashes = new int[size];
mArray = new Object[size<<1];
}
allocArrays 相当于在缓存池中取数据,只不过只有容量为 4 或 8 时才会从缓存中取,其他容量大小则直接创建数组
3. put
public V put(K key, V value) {
final int osize = mSize;
final int hash;
int index;
// 先计算 hash 值再根据 hash 值计算应该存储的位置
if (key == null) {
hash = 0;
index = indexOfNull();
} else {
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);
}
// 如果大于等于 0 说明存在 key 相等的旧值则使用新值覆盖旧值并返回旧值
if (index >= 0) {
index = (index<<1) + 1;
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
// 小于 0 则取反就是应该存储的位置
index = ~index;
// 如果当前数组已经存满了则扩容
if (osize >= mHashes.length) {
// 扩容规则,尽可能是使用容量为 4 或 8 的 ArrayMap 也是为了可以使用缓存
// 如果当前容量大小小于 4 则扩容为 4
// 如果当前容量大小大于 4 小于 8 则扩容为 8
// 否则扩容为当前容量大小的 1.5 倍
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;
// 创建容量为扩容后大小的数组赋值给 mHashes 和 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);
}
if (index < osize) {
if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
+ " to " + (index+1));
// 如果要存储的位置不是数组的末尾则把要存储位置和之后位置的元素向后移动一位
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();
}
}
// 存储元素并且 mSize 加 1
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
先拿到 key 的 hash 值再根据 hash 值计算 key 应该存储的位置 index,如果 index 大于 0 说明 key 已经存在则覆盖,index 小于 0 说明 key 不存在,判断是否需要扩容,扩容后需要把老数组中的元素拷贝到新的数组中,如果不是存储在数组的末尾需要移动数组中的元素把新元素插入。key 可以为 null 计算索引值时也区分处理了为 null 的情况
// key 为 null 时计算应该存储的位置
int indexOfNull() {
final int N = mSize;
// 如果数组中的元素为空则存储在第一个位置(索引 0)
if (N == 0) {
return ~0;
}
// 二分查找
// 如果找到则返回元素的位置不存在返回应该储存的位置的取反值
int index = binarySearchHashes(mHashes, N, 0);
// 如果小于 0 说明没有找到指定的 hash 值则返回应该存储位置的取反值
if (index < 0) {
return index;
}
// 如果找到了并且 index 对应的 value 为 null 则返回这个位置
if (null == mArray[index<<1]) {
return index;
}
// 如果对应位置存储的 value 不为 null 则处理哈希冲突的情况
// 先从找到的位置往后遍历判断 hash 值为 0 并且 value 为 null 则返回
int end;
for (end = index + 1; end < N && mHashes[end] == 0; end++) {
if (null == mArray[end << 1]) return end;
}
// 再从找到的位置往前遍历判断 hash 值为 0 并且 value 为 null 则返回
for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
if (null == mArray[i << 1]) return i;
}
// 没有找到,把最后一个等于 hash 的下标取反后返回
// 使用最后一个是为了减少数组元素的移动
// 比如现在有 5 个 hash 为 0 的元素如果在最后的下标则这 5 个元素不需要移动
// 只移动后面的元素就可以了
return ~end;
}
// key 不为 null 时计算应该存储的位置,与为 null 时的逻辑基本一致只是把比较 value 等于 null 改成了比较 key equals 相等
int indexOf(Object key, int hash) {
final int N = mSize;
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
int index = binarySearchHashes(mHashes, N, hash);
// If the hash code wasn't found, then we have no entry for this key.
if (index < 0) {
return index;
}
// If the key at the returned index matches, that's what we want.
if (key.equals(mArray[index<<1])) {
return index;
}
// 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;
}
// 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 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;
}
indexOfNull 和 indexOf 都是为了计算 key 应该存储的位置,如果已经存在相等的则返回存在的位置,如果不存在则返回应该存储的位置的取反的值
3. append
public void append(K key, V value) {
int index = mSize;
final int hash = key == null ? 0
: (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
if (index >= mHashes.length) {
throw new IllegalStateException("Array is full");
}
if (index > 0 && mHashes[index-1] > hash) {
RuntimeException e = new RuntimeException("here");
e.fillInStackTrace();
Log.w(TAG, "New hash " + hash
+ " is before end of array hash " + mHashes[index-1]
+ " at index " + index + " key " + key, e);
put(key, value);
return;
}
mSize = index+1;
mHashes[index] = hash;
index <<= 1;
mArray[index] = key;
mArray[index+1] = value;
}
append 优化了添加元素到数组尾部的情况与 SparseArray 相似
4. get
public V get(Object key) {
final int index = indexOfKey(key);
// 大于等于 0 说明找到了 hash 值相等并且 key 相等的元素,返回指定位置的 value
// 小于 0 说明没找到
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());
}
get 方法比较简单通过 key 计算 hash 值再通过 hash 值计算索引位置
5. remove
public V remove(Object key) {
// 找到 key 所在的位置
final int index = indexOfKey(key);
if (index >= 0) {
return removeAt(index);
}
return null;
}
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;
} else {
nsize = osize - 1;
// 如果数组容量大于 8 并且存储的元素个数少于数组的三分之一则缩容
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
// 如果数组容量大于 8 则缩容到当前容量的 1.5 否则缩容到 8
final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
// 创建新容量的数组如果是 4 或 8 则复用缓存
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
if (index > 0) {
if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
// 将待移除位置之前的元素复制到新数组
System.arraycopy(ohashes, 0, mHashes, 0, index);
System.arraycopy(oarray, 0, mArray, 0, index << 1);
}
if (index < nsize) {
if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + nsize
+ " to " + index);
// 将待移除位置之后的元素复制到新数组
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) {
if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
+ " to " + index);
// 将待移除元素之后位置的元素向前移动一位(复制到新的位置)把待移除的元素覆盖相当于移除了
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;
}
remove 方法主要就是缩容和移动数组元素,通过将待移除的元素后的所有元素复制到新的位置实现删除
三、与 HashMap、SparseArray 的比较
ArrayMap | SparseArray | HashMap | |
---|---|---|---|
Key 类型 | Object | int | Object |
存储方式 | 它的内部实现是基于两个数组。一个int数组,用于保存每个item的hashCode.一个Object数组,保存key/value键值对。容量是上一个数组的两倍 | 一个 int 数组存储所有的 key 一个 Object 数组存储所有的 value | 使用数组、链表和红黑树存储 Node(包含 key、value 和下一个引用) |
扩容机制 | 如果当前容量大小小于 4 则扩容为 4,如果当前容量大小大于 4 小于 8 则扩容为 8,否则扩容为当前容量大小的 1.5 倍 | 如果当前数组大小小于 4 则扩容至 8 否则扩容至当前数组大小 * 2 | 扩容触发条件是当发生哈希冲突,并且当前实际键值对个数是否大于或等于阈值threshold,默认为0.75 * capacity,扩容操作是针对哈希表table来分配内存空间,每次扩容是至少是当前大小的2倍,扩容的大小一定是2^n,; 另外,扩容后还需要将原来的数据都transfer到新的table,这是耗时操作。 |
缩容机制 | 当数组内存的大小大于8,且已存储数据的个数mSize小于数组空间大小的1/3的情况下,需要收紧数据的内容容量,分配新的数组,老的内存靠虚拟机自动回收。如果mSize<=8,则设置新大小为8;如果mSize> 8,则设置新大小为mSize的1.5倍。 | 无 | 无 |
缓存机制 | ArrayMap针对容量为4和8的对象进行缓存,可避免频繁创建对象而分配内存与GC操作,这两个缓存池大小的上限为10个,防止缓存池无限增大 | SparseArray有延迟回收机制,提高删除效率,同时减少数组成员来回拷贝的次数 | 无 |
1. 如何选择
参考
面试必备:ArrayMap源码解析
读源码 | ArrayMap 是如何高效利用内存的?
深度解读ArrayMap优势与缺陷
如何避免 oom 的产生