ArrayMap源码解析

658 阅读13分钟

上一篇文章提到了SparseArrayHashMap的关系,与SparseArray同样用于改进HashMap效率的还有ArrayMap,当然这两个类都是在特定情况下用于改进效率,并不是任何情况下都优于HashMap,下面将会一步步讲解ArrayMap的内部解构以及代码的组成是什么样的


1. 初始化

  // 传入初始容量
  public ArrayMap(int capacity) {
      this(capacity, false);
  }
  
  public ArrayMap(int capacity, boolean identityHashCode) {
      mIdentityHashCode = identityHashCode;

      // 根据传入的初始容量给`mArray` `mHashes`两个数组初始化
      if (capacity < 0) {
          mHashes = EMPTY_IMMUTABLE_INTS;
          mArray = EmptyArray.OBJECT;
      } else if (capacity == 0) {
          mHashes = EmptyArray.INT;
          mArray = EmptyArray.OBJECT;
      } else {
          allocArrays(capacity);
      }
      mSize = 0;
  }

2. put操作

put相关的代码比较长,涉及到的逻辑也比较多,在这其中也会设计到扩容以及缓存的优化操作

@Override
public V put(K key, V value) {
    // 记录下当前长度
    final int osize = mSize;
    final int hash;
    int index;
    // 如果传入的key为null,那么hash值默认为0,并且indexOfNull会通过二分查找
    // 返回一个下标,至于具体返回值情况可以看下面的代码分析
    if (key == null) {
        hash = 0;
        index = indexOfNull();
    } else {
        // 计算出key的hash值并且计算出对应下标
        hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
        index = indexOf(key, hash);
    }
    // 如果下标大于0,也就是说key是存在的,那么直接更新对应的value
    if (index >= 0) {
        // 因为array是一个键值对占两个位置,比如0和1存放的是key和对应的value
        // 所以这里对index左移一位并且+1
        index = (index<<1) + 1;
        // 把旧的值更新并且返回
        final V old = (V)mArray[index];
        mArray[index] = value;
        return old;
    }

    index = ~index;
    // 判断现有长度和hash数组的长度,去进行扩容操作
    if (osize >= mHashes.length) {
        // 计算出扩容后的数组长度
        final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                : (osize >= 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;
        
        // 对mHashes mArray两个数组进行扩容操作
        allocArrays(n);

        // 检查长度避免多线程操作了map
        if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
            throw new ConcurrentModificationException();
        }
        
        // 把旧的数组移动到扩容后的数组
        if (mHashes.length > 0) {
            if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
            System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
            System.arraycopy(oarray, 0, mArray, 0, oarray.length);
        }
        
        // 缓存无用的数组
        freeArrays(ohashes, oarray, osize);
    }
    
    // 当下标是在中间的时候,把index后面(包括index)的所有数据往后移动一位
    // 然后再把index对应的数据更新就达到了插入的操作
    if (index < osize) {
        if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
                + " to " + (index+1));
        System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
        System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
    }

    if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
        if (osize != mSize || index >= mHashes.length) {
            throw new ConcurrentModificationException();
        }
    }
    // 更新对应index的值,把size+1记录个数
    mHashes[index] = hash;
    mArray[index<<1] = key;
    mArray[(index<<1)+1] = value;
    mSize++;
    return null;
}

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

    // Important fast case: if nothing is in here, nothing to look for.
    // 当前长度为0的时候直接对0取反,也就是返回了-1
    if (N == 0) {
        return ~0;
    }
    // 二分查找找到hash值要插入的下标
    int index = binarySearchHashes(mHashes, N, hash);

    // If the hash code wasn't found, then we have no entry for this key.
    // binarySearchHashes里会把index取反,在put的时候也会再取反一次
    if (index < 0) {
        return index;
    }

    // If the key at the returned index matches, that's what we want.
    // 如果根据找到的index在mArray里的key值也是相同的就可以直接返回
    if (key.equals(mArray[index<<1])) {
        return index;
    }

    // Search for a matching key after the index.
    // 如果没找到,那么先向后查找再向前查找有没有对应的键值对存在
    // 因为上面的二分查找有可能会漏了相同hash值的key,也就是说不同key但是hash值相同
    // 这个时候直接用index去找可能会漏查,因为相同hash值的时候会插入到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.
    // 返回 ~0
    return ~end;
}

// 和indexof方法的区别在于代码的判断中把hash换成0,把key换成了null,其他逻辑不变
int indexOfNull() {
    final int N = mSize;

    // Important fast case: if nothing is in here, nothing to look for.
    if (N == 0) {
        return ~0;
    }

    int index = binarySearchHashes(mHashes, N, 0);

    // If the hash code wasn't found, then we have no entry for this key.
    if (index < 0) {
        return index;
    }

    // If the key at the returned index matches, that's what we want.
    if (null == mArray[index<<1]) {
        return index;
    }

    // Search for a matching key after the index.
    int end;
    for (end = index + 1; end < N && mHashes[end] == 0; end++) {
        if (null == mArray[end << 1]) return end;
    }

    // Search for a matching key before the index.
    for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
        if (null == 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;
}
依照代码可以看出,ArrayMapput数据的时候会先将keyhash之后把找出hash值在hash数组的位置,然后对应的把value插入到对应的下标,而hash数组中的顺序是升序排的,所以mArray中的键值对不是按put的顺序。

上面还提到了一个扩容allocArrays以及缓存freeArrays的方法,也是ArrayMap一大特色
@SuppressWarnings("ArrayToString")
private void allocArrays(final int size) {
    // 先判断size,因为arraymap只对4/8的长度做优化
    if (size == (BASE_SIZE*2)) {
        synchronized (SimpleArrayMap.class) {
            // mTwiceBaseCache是一个缓存的数组,默认为null,如果有缓存的数组就拿来用
            if (mTwiceBaseCache != null) {
                // 先把mTwiceBaseCache保存到array
                final Object[] array = mTwiceBaseCache;
                // 把原本存放键值对的数组设为新的array,达到数组扩容的目的
                mArray = array;
                // 把缓存节点指向当前数组的第0个元素,把mHash数组指向第1个元素
                mTwiceBaseCache = (Object[])array[0];
                mHashes = (int[])array[1];
                // 再清空数组,这样mArray指向的就是一个长度为size的空数组
                array[0] = array[1] = null;
                mTwiceBaseCacheSize--;
                if (DEBUG) System.out.println(TAG + " Retrieving 2x cache " + mHashes
                        + " now have " + mTwiceBaseCacheSize + " entries");
                return;
            }
        }
    } else if (size == BASE_SIZE) {
        // 逻辑同上
        synchronized (SimpleArrayMap.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) System.out.println(TAG + " Retrieving 1x cache " + mHashes
                        + " now have " + mBaseCacheSize + " entries");
                return;
            }
        }
    }

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

@SuppressWarnings("ArrayToString")
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
    // 先判断传入长度,决定要不要进行缓存优化
    if (hashes.length == (BASE_SIZE*2)) {
        synchronized (SimpleArrayMap.class) {
            // 达到缓存次数上限就不再缓存
            if (mTwiceBaseCacheSize < CACHE_SIZE) {
                // 把传入的数组第0存放当前缓存节点,把1用来存放hash数组
                array[0] = mTwiceBaseCache;
                array[1] = hashes;
                // 把其他下标的数据清空
                for (int i=(size<<1)-1; i>=2; i--) {
                    array[i] = null;
                }
                // 再把缓存节点指向传入的array
                // 也就是说这里面是把传入的数组当成了新的缓存节点,然后把旧的缓存节点存放在了第0个元素
                // hash数组放在第1个元素
                mTwiceBaseCache = array;
                mTwiceBaseCacheSize++;
                if (DEBUG) System.out.println(TAG + " Storing 2x cache " + array
                        + " now have " + mTwiceBaseCacheSize + " entries");
            }
        }
    } else if (hashes.length == BASE_SIZE) {
        // 逻辑同上
        synchronized (SimpleArrayMap.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) System.out.println(TAG + " Storing 1x cache " + array
                        + " now have " + mBaseCacheSize + " entries");
            }
        }
    }
}
这里可能有的人会比较困惑这两个方法的逻辑和作用,实际上扩容以及缓存的逻辑分成以下几步
  1. put的过程中判断数组长度是否足够,如果需要扩容,会计算出扩容后的长度,然后先保存现有的mArraymHashs
  2. 扩容的时候如果长度是符合缓存优化长度的,就会尝试去找缓存节点,如果有缓存数组(用array代替)可以用,那就用临时变量把先取出0和1两个下标的元素保存起来,然后array的0和1设成null,这样就可以拿到一个符合长度的数组用来存放mArray的数据,然后下标1的数组给mHashs用,也就是是说原有的缓存节点数组中0会变成新的缓存节点,1会给mHash复用,整个数组arraymArray复用
  3. 当空余位置比较多的时候,会尝试缩短数组长度节省空间,同样先是判断当前有多少个数组,符合优化长度的才会试着去缓存,并且缓存次数不能超过10次,然后把当前数组的01两个元素分别指向缓存节点以及当前的hash值数组,然后把其他元素清空,把缓存节点指向当前元素,也就是说每一次缓存会把缓存节点指向自身,并且自身array0保存原有的缓存节点,1保存当前的hash数组

借用网上的一张图,缓存数组会保存上一个缓存数组节点以及当前hash数组,然后复用的时候就拿当前数组来用,这样通过下标1可以得到一个hash数组,然后清空之后就得到一个可以给mArray复用的数组,然后把缓存节点指向下标0更新缓存节点,这样就完成了缓存复用的流程。
而缓存的时候也是同样的方式,把mHash数组保存在了mArray数组的第1个元素中,把原有的缓存节点保存在下标0中,然后把缓存节点指向当前array,完成缓存数组的更新以及叠加。

一般情况下都是先扩容再检查缓存节点,所以相当于是给把数据换到一个扩容size的新数组,然后把原本的数组加到缓存链中去。还有就是clear remove这种操作


3. remove操作

@Override
public V remove(Object key) {
    // 找到对应key的下标之后再调用removeAt
    final int index = indexOfKey(key);
    if (index >= 0) {
        return removeAt(index);
    }
    return null;
}

// 也是用到了indexOf和indexOfNull两个方法,具体逻辑在上面有提到
public int indexOfKey(Object key) {
        return key == null ? indexOfNull()
                : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
    }


public V removeAt(int index) {
    // 先检查数据合法性
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
        // The array might be slightly bigger than mSize, in which case, indexing won't fail.
        // Check if exception should be thrown outside of the critical path.
        throw new ArrayIndexOutOfBoundsException(index);
    }
    
    final Object old = mArray[(index << 1) + 1];
    final int osize = mSize;
    final int nsize;
    
    // 如果原有长度只有1/0,那直接清空数组和数据就行了,并且会检查一下当前数组是否可以添加到缓存中去
    if (osize <= 1) {
        // Now empty.
        if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
        final int[] ohashes = mHashes;
        final Object[] oarray = mArray;
        mHashes = EmptyArray.INT;
        mArray = EmptyArray.OBJECT;
        freeArrays(ohashes, oarray, osize);
        nsize = 0;
    } else {
        nsize = osize - 1;
        // 判断一下map中数据长度和数组的长度,如果数据个数不到数组长度的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 = osize > (BASE_SIZE*2) ? (osize + (osize>>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);
            
            // 检查数据避免多线程操作的时候出现问题
            if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
                throw new ConcurrentModificationException();
            }
            
            // 这里remove数据的方式是复制index前以及后的数据,跳过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);
            }
            if (index < nsize) {
                if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + nsize
                        + " to " + index);
                System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
                System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
                        (nsize - index) << 1);
            }
        } else {
            // 这里逻辑是不涉及重新分配数组的,并且检查一下下标合法性
            if (index < nsize) {
                if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
                        + " to " + index);
                // 直接把index后的数据都复制往前移一位
                System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
                System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
                        (nsize - index) << 1);
            }
            // 因为上面的操作把数据都复制了一份,这样会多出最后一个元素,所以要把最后一个设成null
            // 这样就完成了对index的remove
            mArray[nsize << 1] = null;
            mArray[(nsize << 1) + 1] = null;
        }
    }
    if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
        throw new ConcurrentModificationException();
    }
    // 更新数据长度
    mSize = nsize;
    return (V)old;
}

4. append putAll


public void append(K key, V value) {
    // append默认是添加到末尾
    int index = mSize;
    // 根据key算出hash值
    final int hash = key == null ? 0
            : (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
    
        
    // 检查index的合法性,做一些判断
    if (index >= mHashes.length) {
        throw new IllegalStateException("Array is full");
    }
    
    // 找到位置存放,但是hash值没有按升序排列的时候,会打印异常然后用put去存放
    if (index > 0 && mHashes[index-1] > hash) {
        RuntimeException e = new RuntimeException("here");
        e.fillInStackTrace();
        Log.w(TAG, "New hash " + hash
                + " is before end of array hash " + mHashes[index-1]
                + " at index " + index + " key " + key, e);
        put(key, value);
        return;
    }
    
    // 如果index合法并且hash值也合法,直接把数据加到末尾
    mSize = index+1;
    mHashes[index] = hash;
    index <<= 1;
    mArray[index] = key;
    mArray[index+1] = value;
}
    
/**
 * Perform a {@link #put(Object, Object)} of all key/value pairs in <var>array</var>
 * @param array The array whose contents are to be retrieved.
 */
public void putAll(ArrayMap<? extends K, ? extends V> array) {
    final int N = array.mSize;
    // 先确定容量是否足够
    ensureCapacity(mSize + N);

    // map是空的时候直接把数据都复制过去
    if (mSize == 0) {
        if (N > 0) {
            System.arraycopy(array.mHashes, 0, mHashes, 0, N);
            System.arraycopy(array.mArray, 0, mArray, 0, N<<1);
            mSize = N;
        }
    } else {
        // 不为空的时候for循环使用put
        for (int i=0; i<N; i++) {
            put(array.keyAt(i), array.valueAt(i));
        }
    }
}

public void ensureCapacity(int minimumCapacity) {
    final int osize = mSize;
    if (mHashes.length < minimumCapacity) {
        // 需要扩容的时候照样是保存旧数据然后去扩容,复制数据,接着缓存数组
        final int[] ohashes = mHashes;
        final Object[] oarray = mArray;
        allocArrays(minimumCapacity);
        if (mSize > 0) {
            System.arraycopy(ohashes, 0, mHashes, 0, osize);
            System.arraycopy(oarray, 0, mArray, 0, osize<<1);
        }
        freeArrays(ohashes, oarray, osize);
    }
    if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) {
        throw new ConcurrentModificationException();
    }
}

5. setValueAt

public V setValueAt(int index, V value) {
    // 先检查index是否合法
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
        // The array might be slightly bigger than mSize, in which case, indexing won't fail.
        // Check if exception should be thrown outside of the critical path.
        throw new ArrayIndexOutOfBoundsException(index);
    }
    // 把对应index的value更新,然后返回旧的value,这里hash值不需要改变
    index = (index << 1) + 1;
    V old = (V)mArray[index];
    mArray[index] = value;
    return old;
}

总结

  • ArrayMap中有两个数组来存放数据,mArray存放键值对数据,mHashs存放键的hash值。
  • 查找下标的时候用二分法去找到插入的地方,mHash中数据是按升序存放,所以mArray的键值对数据不是按存放顺序排。
  • ArrayMap中对应的会进行数组缓存优化,但是只针对容量为4/8的长度,所以如果初始化的时候用4/8会提高效率,但是也不一定就得用4/8,具体场景具体分析
  • ArrayMap用一个变量存放无用的数组来作为缓存,并且可以重复连接来形成一个缓存链,最大缓存次数为10
  • 每次扩容的时候如果超出8的长度,会变成原长度的1.5
  • ArrayMap为非线程安全类,所以代码里做了很多标记判断