android进阶篇06、SparseArray和ArrayMap源码解析

SparseArray和ArrayMap都是HashMap的替代品,主要目的就是为了节省内存,因为在手机上内存是比较宝贵的;虽然在效率上有一点牺牲,但是如果存储的元素个数在1000这个量级之内的,其实效率和HashMap并没有什么差别;

一、SparseArray

1、成员变量和构造方法

我们首先看一下几个比较重要的成员变量和构造方法

成员变量

注释1处的DELETED在删除元素时用于标记,后面介绍remove方法时会看到;

注释2处的mGarbage也是删除元素时用于标记,在remove方法中也会看到;

注释3处的mKeys数组用于存放键值对的key,类型为int;

注释4处的mValues数组用于存放键值对的value,类型为Object;

注释5处的mSize表示存放的键值对的个数;

//1
private static final Object DELETED = new Object();
//2
private boolean mGarbage = false;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
//3
private int[] mKeys;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, E)
//4
private Object[] mValues;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
//5
private int mSize;
复制代码

构造方法

两个构造方法如下所示,默认无参构造方法的容量为10;我们也可以通过传入int值初始化容量值;

/**
 * Creates a new SparseArray containing no mappings.
 */
public SparseArray() {
    this(10);
}

/**
 * Creates a new SparseArray containing no mappings that will not
 * require any additional memory allocation to store the specified
 * number of mappings.  If you supply an initial capacity of 0, the
 * sparse array will be initialized with a light-weight representation
 * not requiring any additional array allocations.
 */
public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
        mKeys = EmptyArray.INT;
        mValues = EmptyArray.OBJECT;
    } else {
        mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
        mKeys = new int[mValues.length];
    }
    mSize = 0;
}
复制代码

下面我们接着看几个比较重要的方法;

2、put方法

put方法如下所示,从方法参数也可以看出key只能为int;

注释1处的binarySearch方法也就是二分查找,在下面单独介绍;

注释2处分支表示存在key对应的键值对,此时直接将mValues数组更新即可;

注释3处这个i=~i注意一下,待会分析完二分查找就会明白什么意思;

注释4处表示此key对应的索引i处的value已经被删除,并且索引i小于mSize,此时直接将mKeys和mValues数组中索引i对应的值更新即可;

注释5分支表示将标记为DELETED的键值对清除,并重新通过二分查找计算一下索引值,因为清除完元素索引可能发生变化;

注释6处的insert方法会通过数组拷贝的方式重新创建,我们下边单独介绍;

/**
 * Adds a mapping from the specified key to the specified value,
 * replacing the previous mapping from the specified key if there
 * was one.
 */
public void put(int key, E value) {
    //1
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        //2
        mValues[i] = value;
    } else {
        //3
        i = ~i;

        if (i < mSize && mValues[i] == DELETED) {
            //4
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        if (mGarbage && mSize >= mKeys.length) {
            //5
            gc();

            // Search again because indices may have changed.
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        
        //6
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}
复制代码

先介绍一下上边注释1处的二分查找方法

这个方法没有什么特别需要解释的地方,就是传统的二分查找的实现,不过有一点需要注意一下,在数组中没有找到key时并不是返回null或者-1;注释1处表示没有找到key时返回~lo,也就是key在数组中的左边界的索引值的取反;这样也就解释了上边注释3处i=~i为什么那么做了;

// This is Arrays.binarySearch(), but doesn't do any argument validation.
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;  // value found
        }
    }
    //1
    return ~lo;  // value not present
}
复制代码

再看一下insert方法,注释1、2、3多处调用了arrayCopy方法,也就是数组拷贝,这也是使用数组存储元素绕不开的操作;

/**
 * Inserts an element into the array at the specified index, growing the array if there is no
 * more room.
 *
 * @param array The array to which to append the element. Must NOT be null.
 * @param currentSize The number of elements in the array. Must be less than or equal to
 *                    array.length.
 * @param element The element to insert.
 * @return the array to which the element was appended. This may be different than the given
 *         array.
 */
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
    assert currentSize <= array.length;

    if (currentSize + 1 <= array.length) {
        //1
        System.arraycopy(array, index, array, index + 1, currentSize - index);
        array[index] = element;
        return array;
    }

    @SuppressWarnings("unchecked")
    T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
            growSize(currentSize));
    //2
    System.arraycopy(array, 0, newArray, 0, index);
    newArray[index] = element;
    //3
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
}
复制代码

我们总结一下put方法,存放元素时,key是存放在int型数组mKeys中,并且在此数组中是有序排列的,这样我们就可以通过二分查找直接求得对应的索引值,然后再通过此索引值更新mKeys和mValues两个数组;

3、get方法

看完put方法,我们接着看一下get方法,理解了put方法,get方法就更容易理解了,get方法如下所示;

注释1处表示get方法有调用了两个参数的get方法,第二个参数表示如果没有找到对应的value值,就使用第2个参数的值;

注释2处使用二分查找法通过key找到索引i;

注释3处分支i小于0表示没有找到对应的索引,mValues[i]==DELETED表示此索引处的键值对已经被删除;

注释4处表示已经找到,则直接返回即可;

/**
 * Gets the Object mapped from the specified key, or <code>null</code>
 * if no such mapping has been made.
 */
public E get(int key) {
    //1
    return get(key, null);
}

/**
 * Gets the Object mapped from the specified key, or the specified Object
 * if no such mapping has been made.
 */
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
    //2
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i < 0 || mValues[i] == DELETED) {
        //3
        return valueIfKeyNotFound;
    } else {
        //4
        return (E) mValues[i];
    }
}
复制代码

4、remove方法

增删改查就还差删除方法了,我们接着介绍一下remove方法

注释1处又调用了delete方法,因此两个方法作用是相同的;我们接着看一下delete方法;

注释2处也是先使用二分查找将key转变为索引i;

注释3处的分支i大于等于0表示存在此key对应的键值对;

注释4处mValues[i] = DELETED,mGarbage = true,这两个操作执行了删除的过程,可见删除其实并不是真正的删除,而是进行了DELETED标记;

/**
 * Alias for {@link #delete(int)}.
 */
public void remove(int key) {
    //1
    delete(key);
}

/**
 * Removes the mapping from the specified key, if there was any.
 */
public void delete(int key) {
    //2
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        //3
        if (mValues[i] != DELETED) {
            //4
            mValues[i] = DELETED;
            mGarbage = true;
        }
    }
}
复制代码

5、gc方法

主要的知识点介绍完了,我们发现SparseArray就是借鉴了HashMap的思想,借助二分查找提高效率;其中最重要的就是减少了内存,在put方法中我们发现,只有当数组大小不够时才进行扩容,并且在扩容之前还执行gc方法保证不会有空间浪费;我们不妨看一下gc如何操作的,gc方法如下所示;

注释1处表示遍历mValues数组中的每个元素,如果没有被标记为DELETED才进行保存,这样就会将列表中被标记为DELETED的元素清除;从而减少了内存;

private void gc() {
    // Log.e("SparseArray", "gc start with " + mSize);

    int n = mSize;
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
        Object val = values[i];

        if (val != DELETED) {
            //1
            if (i != o) {
                keys[o] = keys[i];
                values[o] = val;
                values[i] = null;
            }

            o++;
        }
    }
    //2
    mGarbage = false;
    mSize = o;

    // Log.e("SparseArray", "gc end with " + mSize);
}
复制代码

二、ArrayMap

ArrayMap的思想跟SparseArray差不多,其存在的目的主要是为了解决SparseArray中key只能为int的局限性,在ArrayMap中key和value均为泛型,我们直接分析一下三个主要的方法即可;在分析方法之前先介绍一下几个主要的成员属性;

1、成员属性

注释1处的mHashes数组用于存放key对应的hash值,因为此时的key不跟SparseArray一样只能是int了,所以我们如果还想用数组保存,就需要使用key对应的hash值放在数组中;

注释2处的mArray数组用于保存key和value键值对,你现在可能有点疑惑,一个数组怎么保存key和value两个值,我们在put方法中会进行介绍;

注释3处的mSize表示存储的键值对个数;

@UnsupportedAppUsage(maxTargetSdk = 28) // Hashes are an implementation detail. Use public key/value API.
//1
int[] mHashes;
@UnsupportedAppUsage(maxTargetSdk = 28) // Storage is an implementation detail. Use public key/value API.
//2
Object[] mArray;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
//3
int mSize;
复制代码

2、put方法

put方法有点长,我们只看几个关键的点;

注释1处用于求得key对应的hash值;

注释2处通过indexOf方法将hash值变为索引index,我们在下面单独介绍此方法;

注释3处的分支表示已经存在此key对应的键值对,只需要将新value值覆盖掉oldValue即可,并将oldValue返回;

下面很长的这一部分主要就是进行了扩容缩容等操作,我们不详细介绍了;

我们接着看注释4处,我们前面已经讲过mHashes数组用于保存key对应的hash值,mArray用于保存键值对;从这里就可以看出是如何保存的,index就是前面求得的索引值,key对应的hash值好说,直接mHashes[index]=hash即可;但是在保存键值对时进行了将index左移一位和左移一位然后加1的操作,这样索引0在mArray数组中就对应索引0和1,索引1就对应2和3,索引2就对应4和5,索引3就对应6和7……是不是很巧妙

/**
 * Add a new value to the array map.
 * @param key The key under which to store the value.  If
 * this key already exists in the array, its value will be replaced.
 * @param value The value to store for the given key.
 * @return Returns the old value that was stored for the given key, or null if there
 * was no such key.
 */
@Override
public V put(K key, V value) {
    final int osize = mSize;
    final int hash;
    int index;
    if (key == null) {
        hash = 0;
        index = indexOfNull();
    } else {
        //1
        hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
        //2
        index = indexOf(key, hash);
    }
    if (index >= 0) {
        //3
        index = (index<<1) + 1;
        final V old = (V)mArray[index];
        mArray[index] = value;
        return old;
    }

    index = ~index;
    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;
        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) {
        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();
        }
    }
    //4
    mHashes[index] = hash;
    mArray[index<<1] = key;
    mArray[(index<<1)+1] = value;
    mSize++;
    return null;
}
复制代码

我们接着看一下上面注释2处的indexOf方法,这个方法也挺长的,但关键部分只有1个地方,就是注释1处,看这个方法名我们就知道又是二分查找;

@UnsupportedAppUsage(maxTargetSdk = 28) // Hashes are an implementation detail. Use indexOfKey(Object).
int indexOf(Object key, int hash) {
    final int N = mSize;

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

    // 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 (key.equals(mArray[index<<1])) {
        return index;
    }

    // Search for a matching key after the 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.
    return ~end;
}
复制代码

我们接着看一下binarySearchHashes方法,注释1处又调用了ContainerHelpers的binarySearch方法,这个方法我们在SparseArray中已经介绍了,就是二分查找;

private static int binarySearchHashes(int[] hashes, int N, int hash) {
    try {
        //1
        return ContainerHelpers.binarySearch(hashes, N, hash);
    } catch (ArrayIndexOutOfBoundsException e) {
        if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
            throw new ConcurrentModificationException();
        } else {
            throw e; // the cache is poisoned at this point, there's not much we can do
        }
    }
}
复制代码

3、get方法

讲完了put方法,我们看一下get方法;

注释1处又调用了indexOfKey方法;我们接着看一下indexOfKey方法;

注释2处又调用了indexOf方法,这个方法我们在put方法中已经介绍了;

注释3处通过获得的index索引去mArray数组中查找value值,通过将index左移1位然后加1,这跟put方法就对应上了;

/**
 * Retrieve a value from the array.
 * @param key The key of the value to retrieve.
 * @return Returns the value associated with the given key,
 * or null if there is no such key.
 */
@Override
public V get(Object key) {
    //1
    final int index = indexOfKey(key);
    //3
    return index >= 0 ? (V)mArray[(index<<1)+1] : null;
}

/**
 * Returns the index of a key in the set.
 *
 * @param key The key to search for.
 * @return Returns the index of the key if it exists, else a negative integer.
 */
public int indexOfKey(Object key) {
    //2
    return key == null ? indexOfNull()
            : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
}
复制代码

4、remove方法

最后看一下remove方法;

注释1处通过indexOfKey方法求索引index,这个方法我们在get方法中已经介绍了;

注释2处通过索引调用removeAt方法执行删除操作;

/**
 * Remove an existing key from the array map.
 * @param key The key of the mapping to remove.
 * @return Returns the value that was stored under the key, or null if there
 * was no such key.
 */
@Override
public V remove(Object key) {
    //1
    final int index = indexOfKey(key);
    if (index >= 0) {
        //2
        return removeAt(index);
    }

    return null;
}
复制代码

我们接着看一下removeAt方法,这个方法也很长,我们只看一下主要操作;

注释1处通过索引从mArray数组中求得待删除的value值;

注释2处直接将mArray数组中key和value置为null;

注释3处返回oldValue;

/**
 * Remove the key/value mapping at the given index.
 *
 * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
 * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
 * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
 * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
 *
 * @param index The desired index, must be between 0 and {@link #size()}-1.
 * @return Returns the value that was stored at this index.
 */
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);
    }
    //1
    final Object old = mArray[(index << 1) + 1];
    final int osize = mSize;
    final int nsize;
    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;
        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();
            }

            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);
                System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
                System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
                        (nsize - index) << 1);
            }
            //2
            mArray[nsize << 1] = null;
            mArray[(nsize << 1) + 1] = null;
        }
    }
    if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
        throw new ConcurrentModificationException();
    }
    mSize = nsize;
    //3
    return (V)old;
}

复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享