不再害怕面试问ArrayMap一文完全看懂Android ArrayMap源码解析

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了不再害怕面试问ArrayMap一文完全看懂Android ArrayMap源码解析相关的知识,希望对你有一定的参考价值。

作者:VIjolie

前言

ArrayMap是谷歌推出的在安卓等设备上用于替代HashMap的数据结构,和HashMap相比,具有更高的内存使用率,因此适合在android等内存较为紧张的移动设备,下面结合源码分析ArrayMap实现原理,主要分为添加数据、查找数据、删除数据以及缓存四部分,注意本文基于api29。

构造函数

首先先来来康康ArrayMap的构造函数如下:

/** @hide */
public ArrayMap(int capacity, boolean identityHashCode) 
    mIdentityHashCode = identityHashCode;

    // If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS
    // instance instead of the usual EmptyArray.INT. The reference
    // is checked later to see if the array is allowed to grow.
    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;

初始化了两个空数组,其中mHashes是用来升序存放key的hash值,mArrays 是用来存放key和value的,所以的mArrays的长度是mHashes的两倍,key和value是紧挨着存放的,如果将key所对应的hash在mHashes的位置记作为index,key在 mArrays 中的位置记作 keyIndex,value在 mArrays 中的位置记作valueIndex,他们之间有如下关系: keyIndex = index<<1,valueIndex= index<<1+1= keyIndex+1,如下图:

添加数据

添加数据主要看下put方法,提取部分关键代码如下:

public V put(K key, V value) 
    final int osize = mSize;
    final int hash;
    int index;
    if (key == null) 
        hash = 0;
        //关键代码1
        index = indexOfNull();
     else 
        hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
        //关键代码2
        index = indexOf(key, hash);
    
    //关键代码3
    if (index >= 0) 
        index = (index<<1) + 1;
        final V old = (V)mArray[index];
        mArray[index] = value;
        return old;
    
//关键代码4
    index = ~index;
    if (osize >= mHashes.length) 
    //关键代码5
        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);

        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);
    

    if (index < osize) 

        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();
        
    
     //关键代码6
    mHashes[index] = hash;
    mArray[index<<1] = key;
    mArray[(index<<1)+1] = value;
    mSize++;
    return null;

这个时候假设我们通过put给ArayMap添加一个key为null的值,这里null的hash值为0,如下:

ArrayMap<String,String> arrayMap = new ArrayMap();
arrayMap.put(null,"Empty");

会走到注释关键代码1处的indexOfNull(),如下:

int indexOfNull() 
    final int N = mSize;
    if (N == 0) 
        return ~0;
    
    int index = binarySearchHashes(mHashes, N, 0);
    if (index < 0) 
        return index;
    

    if (null == mArray[index<<1]) 
        return index;
    

    int end;
    for (end = index + 1; end < N && mHashes[end] == 0; end++) 
        if (null == mArray[end << 1]) return end;
    

    for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) 
        if (null == mArray[i << 1]) return i;
    

    return ~end;

由于这个时候ArraMap还是空的,因此mSize=0,直接返回~0,这个时候走到了关键代码4,对返回的结果再次取反,index= ~index,index等于0,由于此时ArrayMap是空的,流程走到了关键代码5,开始给ArrayMap扩容,可以知道经过扩容mHashs的长度为4,mArray的长度为8,继续往下走到关键代码6,插入数据,插入完成之后,ArrayMap的结构示意图如下:

接下来我们来再来看看对于一个不空的 ArryMap 是怎样执行插入逻辑的,假设ArryMap现有数据如下图:

此时再往里面插入数据key4/value4,假设4的hash值为20,这事流程走到关键代码2的 indexOf 函数,最终走到了ContainerHelper的binarySearch(),见名知义通过二分查找找key4的hash值20对应的位置,代码如下:

static int binarySearch(int[] array, int size, int value) 
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) 
        final int mid = (lo + hi) >>> 1;
        final int midVal = array[mid];

        if (midVal < value) 
            lo = mid + 1;
         else if (midVal > value) 
            hi = mid - 1;
         else 
            return mid; 
        
    
    return ~lo;

显然mHashes里面找不到,返回~lo,注意此时lo的值为2,可以发现这个2就是key4的hash值的正确插入位置,对lo取反返回,取反是为了能够走到关键代码4,关键代码4对返回值再次取反,获得key4的hash值的正确插入位置,走到了关键代码7,为key4的插入腾出空间,就是比20大的往后面挪动一位,最后到关键代码6执行插入,插入之后,如下图所示:

冲突解决

假设当前 ArayMap 数据如下:

此时再往里面插入数据key4/value4,假设4的hash值为15,这事流程走到关键代码2的 indexOf 函数,最终走到了ContainerHelper的binarySearch(),通过二分查找找key4的hash值15对应的位置为1:

 int indexOf(Object key, int hash) 
        final int N = mSize;
        if (N == 0) 
            return ~0;
        
        //注释1 index=1
        int index = binarySearchHashes(mHashes, N, hash);
        if (index < 0) 
            return index;
        
        //注释2
        if (key.equals(mArray[index<<1])) 
            return index;
        
        int end;
        //注释3
        for (end = index + 1; end < N && mHashes[end] == hash; end++) 
         //注释4
            if (key.equals(mArray[end << 1])) return end;
        
        for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) 
         //注释5
            if (key.equals(mArray[i << 1])) return i;
        
        return ~end;
    

如上代码注释1,此时index为1,走到注释2,通过index<<1所以得到key2,显然key4不等于key2,走到注释3处,找到了key4的的插入位置为2,如果走到注释4的return,说明从当前index往后遍历key4已经存在的的值,直接返回索引,否则的话就往前遍历,如果走到注释5的return说明key4已经存在直接返回更新(因为采用的是二分查找,找到的index在此过程中可能会跨过若干个hash值等于15的书其中可能包含等于key的的数据)如果都没有返回end的取反走到插入数据关键代码4的逻辑,插入之后,数据如下:

删除数据

调用remove方法最终会走到removeAt,代码如下:

public V removeAt(int index) 
//注释1
    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;
    //注释2
    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;
         //注释3
        if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) 
            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();
            
//注释4
            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 
        //注释5
            if (index < nsize) 
                if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
                        + " to " + index);
                System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
                System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
                        (nsize - index) << 1);
            
            //注释6
            mArray[nsize << 1] = null;
            mArray[(nsize << 1) + 1] = null;
        
    
    if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) 
        throw new ConcurrentModificationException();
    
    mSize = nsize;
    return (V)old;

注释1处如果index大于当前ArrayMap的size,抛出数组越界,注释2如果当前的size小于等于1,直接将mHashes和mArrays置空,然后释放内存。注释3处如果mHashes的长度大于两倍的BASE_SIZE也就是8并且ArrayMap的size小于 mHashes 的长度三分之一,这个时候内存利用率不高,触发收缩逻辑,开始对mHashes和mArrays进行收缩,节省内存,具体的收缩策略是如果当前的size大于8那么就把数组长度设为size的1.5倍,否则就是8,这样的好处是避免size在两倍的BASE_SIZE和BASE_SIZE之间频繁的扩容和收缩,影响性能。逻辑走到注释4也就是删除两个数组中与index对应的数据即把index前面和后面的数据移动到一起。注释5处说明没有触发收缩逻辑,把index后面的数据往前移动一位,末尾的数据置空。

取数据

看下get函数:

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

比较简单,就是通过key二分查找index,如果index大于0,直接返回找到的值否则返回null

缓存数据

ArrayMap为了避免频繁GC,会对数组进行缓存,以便复用,缓存分为插入缓存与取缓存

插入缓存

插入缓存的代码在freeArrays,主要在扩容和删除收缩的时候触发如下:

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) 
            //注释1
                array[0] = mBaseCache;
                array[1] = hashes;
                注释2
                for (int i=(size<<1)-1; i>=2; i--) 
                    array[i] = null;
                
                注释2
                mBaseCache = array;
                mBaseCacheSize++;
                if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
                        + " now have " + mBaseCacheSize + " entries");
            
        
    

可以

以上是关于不再害怕面试问ArrayMap一文完全看懂Android ArrayMap源码解析的主要内容,如果未能解决你的问题,请参考以下文章

面试问的问题

一文看懂RTC中的呼叫服务

面试问Handler内存泄露的场景,别就只知道静态内部类&弱引用!

一文带你看懂PaddleHub

一文看懂PLC的通讯方式——AB系统

15个问题”刨根问底”TSN——让你一文看懂工业互联为何需要TSN?