ArrayMap代码分析

本文深入探讨了Android中的ArrayMap和SparseArray。ArrayMap适用于key-value存储,其内部使用四个数组,并通过二分查找提高效率。当数据量变化时,ArrayMap通过缓存避免不必要的内存分配。SparseArray则专为int key设计,使用二分查找并延迟删除清理,适合数据量较小的情况。两者皆以空间效率换取时间效率。

Java提供了HashMap,但是HashMap对于手机端而言,对空间的利用太大,所以Android提供了SparseArray和ArrayMap。二者都是基于二分查找,所以数据量大的时候,最坏效率会比HashMap慢很多。因此建议数量在千以内比较合适。

一、SparseArray

SparseArray对应的key只能是int类型,它不会对key进行装箱操作。它使用了两个数组,一个保存key,一个保存value。

SparseArray使用二分查找来找到key对应的插入位置。所以要保证mKeys数组有序。

remove的时候不会立刻重新清理删除掉的数据,而是将对一个的数据标记为DELETE(一个Object对象)。在必要的环节调用gc清理标记为DELETE的空间。

二、ArrayMap

重点介绍一下ArrayMap。

首先从ArrayMap的四个数组说起。mHashes,用于保存key对应的hashCode;mArray,用于保存键值对(key,value),其结构为[key1,value1,key2,value2,key3,value3,......];mBaseCache,缓存,如果ArrayMap的数据量从4,增加到8,用该数组保存之前使用的mHashes和mArray,这样如果数据量再变回4的时候,可以再次使用之前的数组,不需要再次申请空间,这样节省了一定的时间;mTwiceBaseCache,与mBaseCache对应,不过触发的条件是数据量从8增长到12。

上面提到的数据量有8增长到12,为什么不是16?这也算是ArrayMap的一个优化的点,它不是每次增长1倍,而是使用了如下方法(mSize+(mSize>>1)),即每次增加1/2。

有了上面的说明,读懂代码就容易多了。

1、很多地方用到的indexOf

这里使用了二分查找来查找对应的index

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 = ContainerHelpers.binarySearch(mHashes, N, hash);

        // If the hash code wasn't found, then we have no entry for this key.
        //没找到hashCode,返回index,一个负数
        if (index < 0) {
 return index;
        }

        // If the key at the returned index matches, that's what we want.
        //对比key值,相同则返回index
        if (key.equals(mArray[index<<1])) {
 return index;
        }

        // Search for a matching key after the index.
        //如果返回的index对应的key值,与传入的key值不等,则可能对应的key在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;
    }

2、看一下put方法

public V put(K key, V value) {
        final int hash;
        int index;
        //key是空,则通过indexOfNull查找对应的index;如果不为空,通过indexOf查找对应的index
        if (key == null) {
 hash = 0;
 index = indexOfNull;
        } else {
 hash = key.hashCode;
 index = indexOf(key, hash);
        }
        
        //index大于或等于0,一定是之前put过相同的key,直接替换对应的value。因为mArray中不只保存了value,还保存了key。
        //其结构为[key1,value1,key2,value2,key3,value3,......]
        //所以,需要将index乘2对应key,index乘2再加1对应value
        if (index >= 0) {
 index = (index<<1) + 1;
 final V old = (V)mArray[index];
 mArray[index] = value;
 return old;
        }

        //取正数
        index = ~index;
        //mSize的大小,即已经保存的数据量与mHashes的长度相同了,需要扩容啦
        if (mSize >= mHashes.length) {
 //扩容后的大小,有以下几个档位,BASE_SIZE(4),BASE_SIZE的2倍(8),mSize+(mSize>>1)(比之前的数据量扩容1/2)
 final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
 : (mSize >= 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 (mHashes.length > 0) {
 if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
 System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
 System.arraycopy(oarray, 0, mArray, 0, oarray.length);
 }

 //看看被废弃的数组是否还有利用价值
 //如果被废弃的数组的数据量为4或8,说明可能利用价值,以后用到的时候可以直接用。
 //如果被废弃的数据量太大,扔了算了,要不太占内存。如果浪费内存了,还费这么大劲,加了类干啥。
 freeArrays(ohashes, oarray, mSize);
        }

        //这次put的key对应的hashcode排序没有排在最后(index没有指示到数组结尾),因此需要移动index后面的数据
        if (index < mSize) {
 if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
 + " to " + (index+1));
 System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
 System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
        }

        //把数据保存到数组中。看到了吧,key和value都在mArray中;hashCode放到mHashes
        mHashes[index] = hash;
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;
        mSize++;
        return null;
    }

3、remove方法

remove方法在某种条件下,会重新分配内存,保证分配给ArrayMap的内存在合理区间,减少对内存的占用。但是从这里也可以看出,Android使用的是用时间换空间的方式。无论从任何角度,频繁的分配回收内存一定会耗费时间的。

remove最终使用的是removeAt方法,此处只说明removeAt

    /**
     * Remove the key/value mapping at the given index.
     * @param index The desired index, must be between 0 and {@link #size}-1.
     * @return Returns the value that was stored at this index.
     */
    public V removeAt(int index) {
        final Object old = mArray[(index << 1) + 1];
        //如果数据量小于等于1,说明删除该元素后,没有数组为空,清空两个数组。
        if (mSize <= 1) {
 // Now empty.
 if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
 //put中已有说明
 freeArrays(mHashes, mArray, mSize);
 mHashes = EmptyArray.INT;
 mArray = EmptyArray.OBJECT;
 mSize = 0;
        } else {
 //如果当初申请的数组最大容纳数据个数大于BASE_SIZE的2倍(8),并且现在存储的数据量只用了申请数量的1/3,
 //则需要重新分配空间,已减少对内存的占用
 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.
 //新数组的大小
 final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2);

 if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);

 final int ohashes = mHashes;
 final Object oarray = mArray;
 allocArrays(n);

 mSize--;
 //index之前的数据拷贝到新数组中
 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);
 }
 //将index之后的数据拷贝到新数组中,和(index>0)的分支结合,就将index位置的数据删除了
 if (index < mSize) {
 if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize
 + " to " + index);
 System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index);
 System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
 (mSize - index) << 1);
 }
 } else {
 mSize--;
 //将index后的数据向前移位
 if (index < mSize) {
 if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize
 + " to " + index);
 System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index);
 System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
 (mSize - index) << 1);
 }
 //移位后最后一个数据清空
 mArray[mSize << 1] = null;
 mArray[(mSize << 1) + 1] = null;
 }
        }
        return (V)old;
    }

4、freeArrays

put中有说明,这里就不进行概述了,直接上代码,印证上面的说法。

  private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
        //已经废弃的数组个数为BASE_SIZE的2倍(8),则用mTwiceBaseCache保存废弃的数组;
        //如果个数为BASE_SIZE(4),则用mBaseCache保存废弃的数组
        if (hashes.length == (BASE_SIZE*2)) {
 synchronized (ArrayMap.class) {
 if (mTwiceBaseCacheSize < CACHE_SIZE) {
 //array为刚刚废弃的数组,mTwiceBaseCache如果有内容,则放入array[0]位置,
 //在allocArrays中会从array[0]取出,放回mTwiceBaseCache
 array[0] = mTwiceBaseCache;
 //array[1]存放hash数组。因为array中每个元素都是Object对象,所以每个元素都可以存放数组
 array[1] = hashes;
 //清除index为2和之后的数据
 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 (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++;
 if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
 + " now have " + mBaseCacheSize + " entries");
 }
 }
        }
    }

5、allocArrays

算了,感觉没啥好说的,看懂了freeArrays,allocArrays自然就理解了。

总体来说,通过新数组的个数产生3个分支,个数为BASE_SIZE(4),从mBaseCache取之前废弃的数组;BASE_SIZE的2倍(8),从mTwiceBaseCache取之前废弃的数组;其他,之前废弃的数组没有存储,因为太耗费内存,这种情况下,重新分配内存。

6、clear和erase

clear清空数组,如果再向数组中添加元素,需要重新申请空间;erase清除数组中的数组,空间还在。

7、get

主要的逻辑都在indexOf中了,剩下的代码不需要分析了,看了的都说懂(窃笑)。

<think>我们即将分析ArrayMap的源码,重点在于其内部数据结构和工作原理。根据引用[2]和[3],ArrayMap使用两个数组来存储数据:一个存储键的哈希值,另一个交替存储键值对。我们将按照以下步骤进行: 1. 数据结构:介绍ArrayMap内部的两个数组结构。 2. 核心方法:包括put、get、remove等操作的源码分析。 3. 性能分析:与HashMap的对比,特别是在内存和效率上的优势与局限。 4. 适用场景:总结ArrayMap的最佳使用场景。 注意:由于用户要求使用中文,且所有数学表达式用$...$或$$...$$格式,但本问题不涉及复杂数学公式,因此主要关注代码和文字描述。引用标识需自然地添加在引用段落末尾。 下面开始回答: </think>### ArrayMap 源码分析(基于 Android 源码) ArrayMap 是 Android 优化的键值对数据结构,核心设计为**双数组存储**: 1. **`int[] mHashes`**:存储键的哈希值(升序排列) 2. **`Object[] mArray`**:交替存储键值对(偶数索引存键,奇数索引存值) --- #### 一、核心数据结构 存储原理示意图: ``` mHashes = [hash0, hash1, hash2] mArray = [key0, value0, key1, value1, key2, value2] ``` - **键值定位**:键 `key_i` 的位置满足 `mHashes[i] = key_i.hashCode()` - **值获取**:`value_i = mArray[2*i + 1]` --- #### 二、核心方法源码分析 ##### 1. 构造函数 ```java public ArrayMap(int capacity) { if (capacity > 0) { allocArrays(capacity); // 初始化双数组 } else { mHashes = EmptyArray.INT; mArray = EmptyArray.OBJECT; } } ``` ##### 2. `put()` 方法流程 ```java public V put(K key, V value) { final int hash = key.hashCode(); int index = binarySearch(mHashes, mSize, hash); // 二分查找哈希数组 if (index >= 0) { // 键已存在 index = (index << 1) + 1; // 计算值索引 final V old = (V) mArray[index]; mArray[index] = value; // 替换旧值 return old; } index = ~index; // 计算新元素插入位置 if (mSize >= mHashes.length) { growSize(); // 扩容(通常长50%) } // 移动后续元素腾出位置 System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index); System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1); // 插入新键值对 mHashes[index] = hash; mArray[index << 1] = key; mArray[(index << 1) + 1] = value; mSize++; return null; } ``` **关键点**: - 哈希冲突处理:依赖 `key.equals()` 精确匹配(无链表/红黑树) - 扩容机制:调用 `growSize()` 创建新数组并拷贝数据[^3] ##### 3. `get()` 方法 ```java public V get(Object key) { final int hash = key.hashCode(); int index = binarySearch(mHashes, mSize, hash); // 二分查找 if (index < 0) return null; return (V) mArray[(index << 1) + 1]; // 返回对应值 } ``` **性能**:时间复杂度 $O(\log n)$(二分查找) ##### 4. `remove()` 方法 ```java public V remove(Object key) { final int hash = key.hashCode(); int index = binarySearch(mHashes, mSize, hash); if (index < 0) return null; return removeAt(index); // 实际移除操作 } public V removeAt(int index) { final V old = (V) mArray[(index << 1) + 1]; // 压缩数组:前移后续元素覆盖删除项 if (mSize > 1) { System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index - 1); System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1, (mSize - index - 1) << 1); } mSize--; return old; } ``` **优化**:直接移动数组元素避免标记删除[^2] --- #### 三、性能特性对比 | **操作** | **ArrayMap** | **HashMap** | |----------|---------------------|---------------------| | 内存占用 | 低(无链表结构) | 高(桶+链表/红黑树)| | 插入效率 | $O(\log n)$ ~ $O(n)$ | $O(1)$ ~ $O(n)$ | | 查询效率 | $O(\log n)$ | $O(1)$ ~ $O(n)$ | | 删除效率 | $O(n)$ | $O(1)$ ~ $O(n)$ | **适用场景**: - 数据量小(建议 < 1000 项) - 内存敏感场景(如移动设备) - 键为自定义对象(需正确实现 `hashCode()` 和 `equals()`)[^1][^3] --- #### 四、设计优势与局限 **优势**: 1. **内存紧凑**:双数组结构减少对象头和指针开销 2. **缓存友好**:连续内存访问提高 CPU 缓存命中率 **局限**: 1. 大规模数据时操作性能下降(数组移动成本高) 2. 非线程安全(需外部同步) ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值