阅读 837

聊聊Android中特有数据结构ArrayMap

1、 ArrayMap特点

1.1 扩容机制上

  • HashMap当前map的容量乘以扩容因子小于等于当前数量的时候就会引发扩容机制。每次扩容都会变为当前元素的二倍。
  • ArrayMap的扩容机制,扩容时机在已经容量已经满了时候引发扩容。每次扩容的大小为原理容量的1.5倍,另外
  • ArrayMap还有缩容机制,当ArrayMap内的元素小于自己容量的三分之一的时候就会引发缩容机制,最后缩为当前元素数量的1.5倍。
  • 扩容的时候缓存机制,缓存的时候只会出现在两种情况下,扩容到mBaseSize(8)或者缩容到(4)的时候。

1.2 处理hash冲突上

  • hashmap会采用拉链法。
  • ArrayMap如果出现了hash冲突就开放地址法。

1.3 ArrayMap的存储结构

一个数组存的是key的hash值,一个存在的key value。结构如下。

ArrayMap结构

1.3 非线程安全

嗯,如标题所言是非线程安全的,多个线程共享ArrayMap的时候就有数据不一致的问题。

2、关于存/取/删元素

2.1 存数据

说起来很简单,就是往ArrayMap中存键值对,如果要存的这个key在HashMap中已经存在了的就用新值替换掉,如果不存在key就连值带键都存在里面。 大体的过程是这样的。

  • 计算key 的hashCode值。
  • 使用二分查找在mHash[]中索引hashCode的位置,找到后对比是否跟key的值相等。这个地方注意,hash值相等不代表两个值相等 ,就跟我与你同为雄性,不代表我们的身份证号一样。
  • 如果key找到已经在Array中存在,就把值替换掉,已知hash值的在mHashp[]数组中的位置,index*2就是键在mArray数组中的位置。index * 2+1就是value的位置。
  • 如果不存在就把新的键值对存进入。

2.1.2扩容

当ArrayMap的容量已满的时候就要使用扩容机制了,具体怎么扩呢源码中找答案。

   if (osize >= mHashes.length) {
            final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                    : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(n);
       ...
复制代码

一波三目运算道出真理:

  • 当前容量大于等于8的时候就只扩充1.5倍
  • 当前容量大于等于4小于8时,扩容到8.
  • 小于4时扩容到4

2.2 取数据

没什么好说的 什么底层源码 什么设计模式 一梭子代码

   @Override
    public V get(Object key) {
        final int index = indexOfKey(key);
        return index >= 0 ? (V)mArray[(index<<1)+1] : null;
    }
复制代码

2.3 删元素

2.3.1 删除

删除操作,本质就是对数组进行复制操作,正常情况下,

2.3.2 缩容机制

当ArrayMap中的元素数小于容量的1/3的时候启动缩容模式,缩容到当期数量(不是容量)的1.5倍,如果数量小于8直接扩容到8。

     if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
              final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
     		....
     }
复制代码

3、 关于缓存

  static Object[] mBaseCache;//容量为4,ArrayMap缓存链表
  static int mBaseCacheSize;//容量为4的缓存数量
  static Object[] mTwiceBaseCache;//容量为8的Array缓存链表
  static int mTwiceBaseCacheSize;//容量为8的缓存数量
复制代码
  • ArrayMap会对容量为4和8的两种ArrayMap进行缓存。
  • 而且对每一种ArrayMap缓存的数量也是有限制 :10。

3.1 回收

    private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
    //当前hashs数据的长度为8
        if (hashes.length == (BASE_SIZE*2)) {
        ...
        } else if (hashes.length == BASE_SIZE) {
            synchronized (ArrayMap.class) {
                if (mBaseCacheSize < CACHE_SIZE) {
                    array[0] = mBaseCache;//把array的第一个元素指向当前的mBaseCache
                    array[1] = hashes;//把array的第二个元素指向hash数组
                    for (int i=(size<<1)-1; i>=2; i--) {//把第二个以后的元素都置空
                        array[i] = null;
                    }
                    mBaseCache = array;//mBaseCache执行array
                    mBaseCacheSize++;//数字加1
                }
            }
        }
    }

复制代码

看注释,应该就明白了,如果还不明白看看下面的这张图。

关注下,freeArrays()方法中的synchronized关键字,不是说好的线程不安全吗,怎么还有出现synchronized呢

线程安全问题的本质是,可变数据在多条线程中共享。 mBaseCache和mTwiceBaseCache都是静态的,也就是说会存在他们多个ArrayMap对象访问他们的情况。反过来思考,一个ArrayMap对象,在单一线程中创建使用,扩容缩容的时候如果没有加同步块会出现线程安全问题。

3.2 重用

就缓存两种容量的map,容量为4和容量为8的

 private void allocArrays(final int size) {
    if (size == (BASE_SIZE*2)) {
        //当要分配数组的容量为8时
      	...
    } else if (size == BASE_SIZE) {
        //当分配数组的容量为4时
       synchronized (ArrayMap.class) {
            if (mBaseCache != null) {
             final Object[] array = mBaseCache;
               mArray = array;//mArray指向缓存链表中第一个数组
               mBaseCache = (Object[])array[0];//mBaseCache指向下一个数组
               mHashes = (int[])array[1];//mHashes数组指向arry的第一个元素
               array[0] = array[1] = null;//置空
               mBaseCacheSize--;     
               return;
           }
        }
     }
     mHashes = new int[size];
     mArray = new Object[size<<1];
    }
复制代码

3、适用条件

  • google官方建议当数据量小于1000的时候,推荐使用ArrayMap,但于1000使用HashMap。ArrayMap查询效率为O(logN),删除和添加元素的时候都要移动数组效率较低 。HashMap的查询更改速度为O(1)。但是ArrayMap优势就是省内存!

  • 除了ArrayMap之外还有SpareArray,原理和ArrayMap差不多,但他的key为int类型,避免了基本数据类型的拆箱装箱带来的效率问题。当确定value的值为int/long/boolean的时候就可以使用SparseIntArray/SparseLongArray/SpareseBoolean。

附录

补充一个二分查找算法

public static int binarySearch(int[] array,int value){
    if (array == null){
        throw new IllegalArgumetsException("不能为空");
    }
     int lo = 0;
     int li = array.length - 1;
     while(lo<=li){
         int index = (lo+li)/2;
         int midValue = array[index];
         if(value>midValue){
             li= index+1;
         }else if(value<midValue){
             lo = index-1;
         }else{
             return index;
         }
     }
     return ~lo;   
}
复制代码