ArrayMap 原理

一 概述

在移动设备端,内存资源很珍贵,HashMap 为实现快速查询带来了很大内存的浪费。为此,2013年5月20日 Google 工程师 Dianne Hackborn 在 Android 系统源码中新增 ArrayMap 类,从 Android 源码中发现有不少提交,专门把之前使用 HashMap 的地方改用 ArrayMap,不仅如此,大量的应用开发者中也广为使用。

然而,你是否了解这么广泛使用的基础数据结构存在缺陷?要回答这个问题,需要先从源码角度来理解 ArrayMap 的原理。

ArrayMap 是 Android 专门针对内存优化而设计的,用于取代 Java API 中的 HashMap 数据结构。

为了更进一步优化 key 是 int 类型的 Map,Android 再次提供效率更高的数据结构 SparseArray,可避免自动装箱过程。对于 key 为其他类型,则可以使用 ArrayMap。

HashMap 的查找和插入时间复杂度为 O(1) 的代价是牺牲大量的内存来实现的,而 SparseArray 和 ArrayMap 性能略逊于 HashMap,但更节省内存。

接下来,从源码看看 ArrayMap,为了全面解读,文章有点长,请耐心阅读。

二 源读 ArrayMap

2.1 基本成员变量

public final class ArrayMap<K, V> implements Map<K, V> {
   

    private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;
    
    private static final int BASE_SIZE = 4;  // 容量增量的最小值
    private static final int CACHE_SIZE = 10; // 缓存数组的上限

    static Object[] mBaseCache; // 用于缓存大小为 4 的 ArrayMap    
    static Object[] mTwiceBaseCache; // 用于缓存大小为 8 的 ArrayMap
    
    static int mBaseCacheSize;
    static int mTwiceBaseCacheSize;

    final boolean mIdentityHashCode;
    int[] mHashes;         // 由 key 的 hashcode 所组成的数组
    Object[] mArray;       // 由 key-value 对所组成的数组,是 mHashes 大小的 2 倍
    int mSize;             // 成员变量的个数
}

ArrayMap 对象的数据储存格式如下图所示:

  • mHashes 是一个记录所有 key 的 hashcode 值组成的数组,是从小到大的排序方式
  • mArray 是一个记录着 key-value 键值对所组成的数组,是 mHashes 大小的2倍

在这里插入图片描述
其中 mSize 记录着该 ArrayMap 对象中有多少对数据,执行 put() 或者 append() 操作,则 mSize 会加 1,执行 remove(),则 mSize 会减 1。

mSize 往往小于 mHashes.length,如果 mSize 大于或等于 mHashes.length,则说明 mHashes 和 mArray 需要扩容。

ArrayMap 类有两个非常重要的静态成员变量 mBaseCache 和 mTwiceBaseCache,用于 ArrayMap 所在进程的全局缓存功能:

  • mBaseCache:用于缓存大小为 4 的 ArrayMap,mBaseCacheSize 记录着当前已缓存的数量,超过 10 个则不再缓存
  • mTwiceBaseCache:用于缓存大小为 8 的 ArrayMap,mTwiceBaseCacheSize 记录着当前已缓存的数量,超过 10 个则不再缓存

为了减少频繁地创建和回收 Map 对象,ArrayMap 采用了两个大小为 10 的缓存队列来分别保存大小为 4 和 8 的 Map 对象。为了节省内存使用了更加保守的内存扩张以及内存收缩策略。 接下来分别说说缓存机制和扩容机制。

2.2 缓存机制

ArrayMap 是专为 Android 优化而设计的 Map 对象,使用场景比较高频,很多场景可能起初都是数据很少,为了减少频繁地创建和回收,特意设计了两个缓存池,分别缓存大小为 4 和 8 的 ArrayMap 对象。要理解缓存机制,那就需要看看内存分配 (allocArrays) 和内存释放 (freeArrays)。

2.2.1 ArrayMap.freeArrays

private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
   
    if (hashes.length == (BASE_SIZE*2)) {
     // 当释放的是大小为 8 的对象
        synchronized (ArrayMap.class) {
   
            // 当大小为 8 的缓存池的数量小于 10 个,则将其放入缓存池
            if (mTwiceBaseCacheSize < CACHE_SIZE) {
    
                array[0] = mTwiceBaseCache;  // array[0] 指向原来的缓存池
                array[1] = hashes;
                for (int i=(size<<1)-1; i>=2; i--) {
   
                    array[i] = null;  // 清空其他数据
                }
                mTwiceBaseCache = array; // mTwiceBaseCache 指向新加入缓存池的 array
                mTwiceBaseCacheSize++; 
            }
        }
    } else if (hashes.length == BASE_SIZE) {
     // 当释放的是大小为 4 的对象,原理同上
        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++;
            }
        }
    }
}

最初 mTwiceBaseCache 和 mBaseCache 缓存池中都没有数据,在 freeArrays 释放内存时,如果同时满足释放的 array 大小等于 4 或者 8,且相对应的缓冲池个数未达上限,则会把该 arrya 加入到缓存池中。

加入的方式是将数组 array 的第 0 个元素指向原有的缓存池,第 1 个元素指向 hashes 数组的地址,第 2 个元素以后的数据全部置为 null。再把缓存池的头部指向最新的 array 的位置,并将该缓存池大小执行加 1 操作。具体如下所示。
在这里插入图片描述
ArrayMap 的 freeArrays() 的触发条件:

  • 当执行 removeAt() 移除最后一个元素的情况
  • 当执行 clear() 清理的情况
  • 当执行 ensureCapacity() 在当前容量小于预期容量的情况下,先执行 allocArrays,再执行 freeArrays
  • 当执行 put() 在容量满的情况下,先执行 allocArrays,再执行 freeArrays

2.2.2 ArrayMap.allocArrays

private void allocArrays(final int size) {
   
    if (size == (BASE_SIZE*2)) {
     // 当分配大小为 8 的对象,先查看缓存池
        synchronized (ArrayMap.class) {
   
            if (mTwiceBaseCache != null) {
    // 当缓存池不为空时
                final Object[] array = mTwiceBaseCache; 
                mArray = array;         // 从缓存池中取出 mArray
                mTwiceBaseCache = (Object[])array[0]; // 将缓存池指向上一条缓存地址
                mHashes = (int[])array[1];  // 从缓存中 mHashes
                array[0] = array[1] = null;
                mTwiceBaseCacheSize--;  // 缓存池大小减 1
                return;
            }
        }
    } else if (size == BASE_SIZE) {
    // 当分配大小为 4 的对象,原理同上
        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--;
                return;
            }
        }
    }
    
    // 分配大小除了 4 和 8 之外的情况,则直接创建新的数组
    mHashes = new int[size];
    mArray = new Object[size<<1];
}

当 allocArrays 分配内存时,如果所需要分配的大小等于 4 或者 8,且相对应的缓冲池不为空,则会从相应缓存池中取出缓存的 mArray 和 mHashes。

从缓存池取出缓存的方式是将当前缓存池赋值给 mArray,将缓存池指向上一条缓存地址,将缓存池的第 1 个元素赋值为 mHashes,再把 mArray 的第 0 和第 1 个位置的数据置为 null,并将该缓存池大小执行减 1 操作,具体如下所示。
在这里插入图片描述
ArrayMap 的 allocArrays 的触发时机:

  • 当执行 ArrayMap 的构造函数的情况
  • 当执行 removeAt() 在满足容量收紧机制的情况
  • 当执行 ensureCapacity() 在当前容量小于预期容量的情况下,先执行 allocArrays,再执行 freeArrays
  • 当执行 put() 在容量满的情况下,先执行 allocArrays,再执行 freeArrays

这里需要注意的是只有大小为 4 或者 8 的内存分配,才有可能从缓存池取数据,因为 freeArrays 过程放入缓存池的大小只有 4 或 8,对于其他大小的内存分配则需要创建新的数组。

优化小技巧,对于分配数据不超过 8 的对象的情况下,一定要创建 4 或者 8 大小,否则浪费了缓存机制。比如 ArrayMap[7] 就是不友好的写法,建议写成 ArrayMap[8]。

2.3 扩容机制

2.3.1 容量扩张

public V put(K key, V value) {
   
    ...
    final int osize = mSize;
    if (osize >= mHashes.length) {
    // 当 mSize 大于或等于 mHashes 数组长度时需要扩容
        final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
        allocArrays(n);  // 分配更大的内存【小节2.2.2】
    }
    ...
}

当 mSize 大于或等于 mHashes 数组长度时,则扩容,完成扩容后需要将老的数组拷贝到新分配的数组,并释放老的数组内存。

  • 当 map 个数满足条件 osize<4 时,则扩容后的大小为 4
  • 当 map 个数满足条件 4<= osize < 8 时,则扩容后的大小为 8
  • 当 map 个数满足条件 osize>=8 时,则扩容后的大小为原来的 1.5 倍

可见 ArrayMap 大小在不断增加的过程,size 的取值一般情况依次会是 4,8,12,18,27,40,60,…

2.3.2 容量收紧

public V removeAt(int index) {
   
    final int osize = mSize;
    final int nsize;
    if (osize > 1) {
     // 当 mSize 大于 1 的情况
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值