ArrayMap和SparseArray ——Android下更优的Map

三种容器对比

项目 ArrayMap SparseArray HashMap
概念 内存优化版的HashMap key为int的性能优化版ArrayMap 以大量内存实现存取O(1)的Map
数据结构 两个数组:
一个存Key的Hash
另一个存Key和Value
两个数组:
一个存Key
另一个存Value
数组+链表/红黑树
应用场景 1. 数据量千以内;
2. 数据中有Map;
1. 数据量千以内;
2. key必须是int类;
前两者不适合时使用
  • 三者都线程不安全

ArrayMap

数据结构

两个数组:

  1. mHashes: 存放key的哈希值
  2. mArray: 一个Object数组, 偶数索引存key值, 奇数索引存value
    两个数组
mHashes[index] = hash;
//用位运算优化
mArray[index<<1] = key;  //等同于 mArray[index * 2] = key;
mArray[(index<<1)+1] = value; //等同于 mArray[index * 2 + 1] = value;

mArray的大小是mHashes的两倍.

存放逻辑

get()

  1. 根据Key计算其哈希值(如果key是null,直接取0)
  2. 在mHashes数组中寻找哈希值对应的index
  3. 如果index<0,说明没找到,返回null;若index>0则在mArray中取出Value返回

其中, 第二步耗时最多, 使用二分查找来优化.
ArrayMap的查询时间复杂度为 O(log n).

put()

  1. 根据Key计算其哈希值(如果key是null,直接取0)
  2. 在mHashes数组中寻找哈希值对应的index
  3. 若index>0则mArray已存在值,直接替换成新值即可.
  4. 若index<0, 说明当前的key不存在.将index取反,就可以得到要插入的下标.
  5. 根据index判断是否需要扩容
  6. 进行数组复制,腾出index的位置
  7. 将新值放入index位置

重点: 取下标indexOf()

int indexOf(Object key, int hash) {
        final int N = mSize;

        // Important fast case: 如果当前是空表,直接返回
        if (N == 0) {
            return ~0;//返回的是最大的负数
        }
	//二分查找,返回要插入下标的取反
        int index = binarySearchHashes(mHashes, N, hash);

        //小于0,说明key原来没有存,返回
        if (index < 0) {
            return index;
        }
	
        // 如果index>0,而且key刚好对应上,那刚刚好找到,返回
        if (key.equals(mArray[index<<1])) {
            return index;
        }
	//进行Hash碰撞下的查找
        // 向后查找
        int end;
        for (end = index + 1; end < N && mHashes[end] == hash; end++) {
            if (key.equals(mArray[end << 1])) return end;
        }

        // 向前查找
        for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
            if (key.equals(mArray[i << 1])) return i;
        }

        //如果上面都没找到, 那么最后的end就是将要插入的位置,类似的,先取反再返回
        return ~end;
    }

缓存机制

  • Android中在很多场景起初数据量都比较小, 为了避免频繁创建和回收数组对象, ArrayMap设计了两个缓冲池mBaseCache和mTwiceBaseCache.
  • 只有大小是4和8时,才会触发缓存机制. 所以最好使用 new ArrayMap(4)new ArrayMap(8) 这样可以最大程度的减少对象的创建。
  • 缓存复用的是大小为4或8的数组.
  • 实现思想: 把要缓存的数组串成单向链表, 创建缓存就是链表头部插入元素, 复用就是删除链表头元素.
private static final int BASE_SIZE = 4;

创建缓存

private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
    if (hashes.length == (BASE_SIZE * 2)) {  //缓存大小为8的数组的hashes,和array
        synchronized (ArrayMap.class) {
            // 当大小为8的缓存池的数量小于10个,则将其放入缓存池
            if (mTwiceBaseCacheSize < CACHE_SIZE) {
                //把 array[0]看作单向链表节点的next即可, 此处是把array接上原有缓存
                array[0] = mTwiceBaseCache;
                //将hashes保存到节点中
                array[1] = hashes;
                for (int i = (size << 1) - 1; i >= 2; i--) {
                    //清除不需要的数据
                    array[i] = null;  
                }
                //把头结点指向array, 完成链表插入头部节点
                mTwiceBaseCache = array; 
                mTwiceBaseCacheSize++;
            }
        }
    } 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++;
            }
        }
    }
}

3层的缓存结构:
3层的缓存结构

freeArrays()触发时机:

时机 原因
removeAt()移除最后一个元素 此时不需要数组,试图回收
执行clear()清理 同上
执行ensureCapacity()在当前容量小于预期容量 Sample
执行put()在容量满的情况 同上

复用缓存

private void allocArrays(final int size) {
    if (size == (BASE_SIZE*2)) {  //当分配大小为8的对象,先查看缓存池
        synchronized (ArrayMap.class) {
            if (mTwiceBaseCache != null) { // 当缓存池不为空时
                final Object[] array = mTwiceBaseCache;
                //从缓存池中取出mArray
                mArray = array;
                //链表删除头结点, 即把原头结点的下一位变成头结点
                mTwiceBaseCache = (Object[])array[0]; 
                //从原头结点中取出mHashes
                mHashes = (int[])array[1];  
                //清空原头结点的数据
                array[0] = array[1] = null;
                mTwiceBaseCacheSize--;  //缓存池大小减1
                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--;
                return;
            }
        }
    }

    // 分配大小除了4和8之外的情况,则直接创建新的数组
    mHashes = new int[size];
    mArray = new Object[size<<1];
}

2层缓存时的数据结构:
2层缓存时的数据结构

allocArrays触发时机

时机 原因
执行ArrayMap的构造函数 构造最开始的数组
执行removeAt()在满足容量收紧机制时 尝试进行数组复用
执行ensureCapacity()在当前容量小于预期容量 Sample
执行put()在容量满的情况 同上

容量调整机制

扩张

  • 在put中,进行检查,满足情况进行扩张
public V put(K key, V value) {
...
    final int osize = mSize;
    //当mSize大于或等于mHashes数组长度时需要扩容
    if (osize >= mHashes.length) {
        //不足4的补到4,不足8的补到8, 否则扩容1.5倍(osize + (osize >> 1))
        final int n = osize >= (BASE_SIZE * 2) ? (osize + (osize >> 1))
                : (osize >= BASE_SIZE ? (BASE_SIZE * 2) : BASE_SIZE);
        allocArrays(n);
    }
}

缩减

public V removeAt(int index) {
    final int osize = mSize;
    //当mSize大于1的情况,需要根据情况来决定是否要收紧
    if (osize > 1) {
        //当数组容量mSize大于8而且存储的数据量不足容量的1/3时, 进行缩减
        if (mHashes.length > (BASE_SIZE * 2) &amp;&amp; mSize < mHashes.length / 3) {
            //mSize不足8时补8, 否则按照mSize的1.5倍申请数组
            final int n = osize > (BASE_SIZE * 2) ? (osize + (osize >> 1)) : (BASE_SIZE * 2);
            allocArrays(n);
        }
    }
}
  • 当数组利用率不足1/3时, 其会缩减50%及以上( 1 3 \frac{1}{3} 31*1.5= 1 2 \frac{1}{2} 21)

参数mIdentityHashCode

  • 默认情况为false, 即允许mHashes中出现相同的值的.
  • 实现逻辑:
    public V put(K key, V value) {
        ...
	//如果为true, 使用的系统默认的HashCode,即根据内存地址计算, 每个对象都不同; 若为false, 则调用重写过的, 可以避免对象重复.
        hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
        index = indexOf(key, hash);
        ...
}

SparseArray

  • SparseArray的Key必须是int.
  • 数据量在百级时性能比HashMap要好大概提升0-50%;
  • 底层是两个数组mKeys和mValues分别存储Key和值两者索引对应.

扩容机制

public static int growSize(int currentSize) {
        //扩容规则:当前容量小于5返回8;否则扩容一倍
        return currentSize <= 4 ? 8 : currentSize * 2;
    }
  • 故其没有设置最大容量,

延时删除机制

  • 在SparseArray删除时, 并不会直接操作数组, 而是将要删除的位置设置为标志位DELETED,同时会有一个标志位mGarbage用于标识是否有潜在延迟删除的无效数据(只要有删除操作就会置true).
  • 优点: 减少数组操作,提升效率,如频繁删除不需要操作数组;插入元素到DELETED时,也不需要操作数组.
//一个常量Object充当标志位
 private static final Object DELETED = new Object();
  • 在插入元素时, 如果是DELETED就可以直接替换,不需要操作数组,效率更高.若不是,先根据mGarbage进行数据清除gc(),再进行数组操作.
    //将正常元素往前挤,挤掉DELETED的位置
    private void gc() {
        //n代表gc前数组的长度;
        int n = mSize;
        int o = 0;//有效的下标长度
        int[] keys = mKeys;
        Object[] values = mValues;
 
        for (int i = 0; i < n; i++) {
            Object val = values[i];
            //每遇到一次DELETED,则i-o的大小+1;
            if (val != DELETED) {
                //之后遇到非DELETED数据,则将后续元素的key和value往前挪
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
 
                o++;
            }
        }
        //此时无垃圾数据,o的序号表示mSize的大小
        mGarbage = false;
        mSize = o;
 
    }

gc()前后变化:
gc()前后变化

存取逻辑

put()

  1. 在mKeys中二份查找到key的index
  2. 如果index>0,说明数据已存在, 直接替换.
  3. 如果index<0或对应的Value是DELETE,说明数据不存在, 直接替换.
  4. 否则, 先进行gc(), 重新查找插入位置index,再将数据插入到mKeysmValues

get()

  1. 在mKeys中二份查找到key的index
  2. 如果index<0或对应的Value是DELETE,说明数据不存在, 返回null. 否则返回value.
  • 因为SparseArray的Key必须是int, 所以不需要处理key是null的情况

参考资料
ArrayMap分析1
ArrayMap分析2
ArrayMap分析3
SparseArray分析1
SparseArray分析2

猜你喜欢

转载自blog.csdn.net/Reven_L/article/details/120310809