ArrayMap 源码分析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、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 的比较

ArrayMapSparseArrayHashMap
Key 类型ObjectintObject
存储方式它的内部实现是基于两个数组。一个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 的产生

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值