ArrayMap

本文深入解析了Android中的ArrayMap,一种高效且节省内存的键值对存储结构。文章详细介绍了ArrayMap的工作原理,包括查找、插入、删除操作的具体实现,并探讨了其内部结构优化和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据集合在任何一门编程语言中都是很重要的一部分,在 Android 开发中,我们会实用到ArrayList, LinkedList, HashMap等。其中HashMap是用来处理键值对需求的常用集合。 而Android中引入了一个新的集合,叫做ArrayMap,为键值对存储需求增加了一种选择。

ArrayMap是什么

一个通用的key-value映射数据结构
相比HashMap会占用更少的内存空间
android.util和android.support.v4.util都包含对应的ArrayMap类
ArrayMap的内部结构

ArrayMap internal strucuture

如上图所示,在ArrayMap内部有两个比较重要的数组,一个是mHashes,另一个是mArray。

mHashes用来存放key的hashcode值
mArray用来存储key与value的值,它是一个Object数组。
其中这两个数组的索引对应关系是

1
2
3
mHashes[index] = hash;
mArray[index<<1] = key; //等同于 mArray[index * 2] = key;
mArray[(index<<1)+1] = value; //等同于 mArray[index * 2 + 1] = value;
注:向左移一位的效率要比 乘以2倍 高一些。

查找数据

查找数据是容器常用的操作,在Map中,通常是根据key找到对应的value的值。

ArrayMap中的查找分为如下两步

根据key的hashcode找到在mHashes数组中的索引值
根据上一步的索引值去查找key所对应的value值
其中占据时间复杂度最多的属于第一步:确定key的hashCode在mHahses中的索引值。

而这一步对mHashes查找使用的是二分查找,即Binary Search。所以ArrayMap的查询时间复杂度为 ‎O(log n)

确定key的hashcode在mHashes中的索引的代码的逻辑

int indexOf(Object key, int hash) {
    final int N = mSize;
    //快速判断是ArrayMap是否为空,如果符合情况快速跳出
    if (N == 0) {
        return ~0;
    }
    //二分查找确定索引值
    int index = ContainerHelpers.binarySearch(mHashes, N, hash);

    // 如果未找到,返回一个index值,可能为后续可能的插入数据使用。
    if (index < 0) {
        return index;
    }

    // 如果确定不仅hashcode相同,也是同一个key,返回找到的索引值。
    if (key.equals(mArray[index<<1])) {
        return index;
    }

    // 如果key的hashcode相同,但不是同一对象,从索引之后再次找
    int end;
    for (end = index + 1; end < N && mHashes[end] == hash; end++) {
        if (key.equals(mArray[end << 1])) return end;
    }

    // 如果key的hashcode相同,但不是同一对象,从索引之前再次找
    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;
}

既然对mHashes进行二分查找,则mHashes必须为有序数组。

插入数据

ArrayMap提供给我们进行插入数据的API有

append(key,value)
put(key,value)
putAll(collection)
以put方法为例,需要注意的有

新数据位置确定
key为null
数组扩容问题
新数据位置确定

为了确保mHashes能够进行二分查找,我们需要保证mHashes始终未有序数组。

在确定新数据位置过程中

根据key的hashcode在mHashes表中二分查找确定合适的位置。
如果新添加的数据的索引不是最后位置,在需要对这个索引之后的全部数据向后移动
ArrayMap put

key为null时

当key为null时,其实和其他正常的key差不多,只是对应的hashcode会默认成0来处理。

public V put(K key, V value) {
    final int hash;
    int index;
    if (key == null) {
        hash = 0;//如果key为null,其hashcode算作0
        index = indexOfNull();
    }
  ...
}

数组扩容问题

首先数组的容量会扩充到BASE_SIZE
如果BASE_SIZE无法容纳,则扩大到2 * BASE_SIZE
如果2 * BASE_SIZE仍然无法容纳,则每次扩容为当前容量的1.5倍。
具体的计算容量的代码为

/**
 * 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;
final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
  : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

删除数据

删除ArrayMap中的一项数据,可以分为如下的情况

如果当前ArrayMap只有一项数据,则删除操作将mHashes,mArray置为空数组,mSize置为0.
如果当前ArrayMap容量过大(大于BASE_SIZE*2)并且持有的数据量过小(不足1/3)则降低ArrayMap容量,减少内存占用
如果不符合上面的情况,则从mHashes删除对应的值,将mArray中对应的索引置为null
ArrayMap的缓存优化

ArrayMap的容量发生变化,正如前面介绍的,有这两种情况

put方法增加数据,扩大容量
remove方法删除数据,减小容量
在这个过程中,会频繁出现多个容量为BASE_SIZE和2 * BASE_SIZE的int数组和Object数组。ArrayMap设计者为了避免创建不必要的对象,减少GC的压力。采用了类似对象池的优化设计。

这其中设计到几个元素

BASE_SIZE 值为4,与ArrayMap容量有密切关系。
mBaseCache 用来缓存容量为BASE_SIZE的int数组和Object数组
mBaseCacheSize mBaseCache缓存的数量,避免无限缓存
mTwiceBaseCache 用来缓存容量为 BASE_SIZE * 2的int数组和Object数组
mTwiceBaseCacheSize mTwiceBaseCache缓存的数量,避免无限缓存
CACHE_SIZE 值为10,用来控制mBaseCache与mTwiceBaseCache缓存的大小
这其中

mBaseCache的第一个元素保存下一个mBaseCache,第二个元素保存mHashes数组
mTwiceBaseCache和mBaseCache一样,只是对应的数组容量不同
具体的缓存数组逻辑的代码为


private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
    if (hashes.length == (BASE_SIZE*2)) {
        synchronized (ArrayMap.class) {
            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 (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");
            }
        }
    }
}

具体的利用缓存数组的代码为

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) {
                final Object[] array = mTwiceBaseCache;
                mArray = array;
                mTwiceBaseCache = (Object[])array[0];
                mHashes = (int[])array[1];
                array[0] = array[1] = null;
                mTwiceBaseCacheSize--;
                if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                        + " now have " + mTwiceBaseCacheSize + " entries");
                return;
            }
        }
    } else if (size == BASE_SIZE) {
        synchronized (ArrayMap.class) {
            if (mBaseCache != null) {
                final Object[] array = mBaseCache;
                mArray = array;
                mBaseCache = (Object[])array[0];
                mHashes = (int[])array[1];
                array[0] = array[1] = null;
                mBaseCacheSize--;
                if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                        + " now have " + mBaseCacheSize + " entries");
                return;
            }
        }
    }

    mHashes = new int[size];
    mArray = new Object[size<<1];
}

在Android中的应用

在Android Performance Pattern中,官方给出的使用场景为

1.item数量小于1000,尤其是插入数据和删除数据不频繁的情况。

2.Map中包含子Map对象

### ArrayMap 类在并发场景下的使用与安全性分析 #### 1. ArrayMap 的基本特性 ArrayMapAndroid 提供的一种轻量级键值映射容器,其底层基于数组实现。相比于传统的 `HashMap`,ArrayMap 在存储少量数据时具有更高的性能和更低的内存占用[^2]。 然而,ArrayMap 并未提供任何内置的线程安全保障机制。这意味着,在多线程环境下直接操作同一个 ArrayMap 实例可能会引发竞态条件或其他不可预测的行为。 --- #### 2. ArrayMap 的线程安全性问题 由于 ArrayMap 不具备线程安全的设计,因此在多线程环境中可能出现以下问题: - **数据一致性破坏**:当多个线程同时修改 ArrayMap 中的数据时,可能导致内部状态不一致。 - **读写冲突**:如果一个线程正在向 ArrayMap 添加新条目,而另一个线程尝试读取这些尚未完全初始化的数据,则可能抛出异常或返回错误的结果。 - **死锁风险**:虽然 ArrayMap 自身不会引入死锁,但如果开发者试图通过手动加锁的方式保护 ArrayMap 而未能妥善管理同步逻辑,则容易造成死锁。 以上问题的根本原因在于 ArrayMap 缺乏必要的同步控制措施以及对并发访问的支持[^3]。 --- #### 3. 解决方案:如何在线程安全的情况下使用 ArrayMap? 以下是几种常见的解决方案及其适用场景: ##### (1) 使用外部同步块包裹 ArrayMap 操作 可以通过显式的 synchronized 关键字来确保每次只有一个线程能够执行特定的操作序列。例如: ```java public class ThreadSafeArrayMap<K, V> { private final ArrayMap<K, V> map = new ArrayMap<>(); public void put(K key, V value) { synchronized (map) { map.put(key, value); } } @Nullable public V get(@NonNull K key) { synchronized (map) { return map.get(key); } } } ``` 这种方法简单易懂,但对于高并发环境来说效率较低,因为所有的操作都需要等待获取同一把锁。 --- ##### (2) 将 ArrayMap 替换为 ConcurrentHashMap 或其他线程安全集合 尽管 ArrayMap 更适合处理小型数据集,但在需要支持高度并行化的应用场合下,可以考虑改用 Java 标准库中的 `ConcurrentHashMap`。不过需要注意的是,切换至更复杂的结构通常会带来额外的空间开销。 对于某些特殊需求(比如希望保持插入顺序),还可以评估是否能采用 CopyOnWriteArrayList 结合自定义索引来模拟类似功能[^4]。 --- ##### (3) 运用 ThreadLocal 隔离各线程间的状态 借助 ThreadLocal 技术可以让每个独立运行的任务拥有各自专属的一份副本,从而彻底规避因共享资源而导致的竞争隐患。具体做法如下所示: ```java private static final ThreadLocal<ArrayMap<String, String>> threadLocalMap = ThreadLocal.withInitial(ArrayMap::new); // 访问当前线程对应的实例 threadLocalMap.get().put("key", "value"); String result = threadLocalMap.get().get("key"); // 清理工作以防潜在泄露 threadLocalMap.remove(); ``` 此策略特别适用于那些仅需临时保存局部信息而不必长期保留全局视图的情形之下[^5]。 --- #### 4. 总结 综上所述,ArrayMap 并不适合未经改造便投入大规模并发任务之中去运用;倘若非要如此行事的话,则务必采取适当手段加以防护才行——无论是依靠外层锁定还是选用更适合应对这种情况的产品替代品均可达成目标。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值