美文网首页
SparseArray 实现原理简析

SparseArray 实现原理简析

作者: tandeneck | 来源:发表于2020-05-21 21:07 被阅读0次

    背景

    SparseArray 是用于存储 K-V 的数据结构,但是并没有像 ArrayMap 和 HashMap
    一样实现 Map 接口,仅仅实现了 Cloneable 接口,其内部 维护了 2 个数组,一个 int 数组用于存储 key,一个 Object 数组用于存储 value。mKeys 数组存储的元素是 key 值本省,没有进行 hash 值运算,避免了装箱操作,同时并没有像 HashMap 那样引入额外的数据结构 Entry(1.7)或者 Node(1.8),这大大提升了内存利用效率。

    类结构

    public class SparseArray<E> implements Cloneable {
        private static final Object DELETED = new Object();
        private boolean mGarbage = false;
    
        @UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
        private int[] mKeys;
        @UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, E)
        private Object[] mValues;
        @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
        private int mSize;
        ......
    }
    

    put() 方法

        public void put(int key, E value) {
            int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
            if (i >= 0) {
                mValues[i] = value;
            } else {
                i = ~i;
    
                if (i < mSize && mValues[i] == DELETED) {
                    mKeys[i] = key;
                    mValues[i] = value;
                    return;
                }
    
                if (mGarbage && mSize >= mKeys.length) {
                    gc();
    
                    // Search again because indices may have changed.
                    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
                }
    
                mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
                mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
                mSize++;
            }
        }
    

    如上,put() 方法主要做了以下操作:

    • 通过二分查找在 mKeys 数组中查找对应的 key(即 mKeys 数组里面的元素是有序,这是实现二分查找法的前提)。
    • 查找成功则直接更新 key 对应的 value,而且不返回旧值,而 ArrayMap 和 HashMap 都会返回旧值。
    • 如果当前要插入的 key 索引上的值为 DELETE,直接覆盖。
    • 如果需要执行 gc 方法 & 需要扩大数组容量,则会执行 gc 方法,由于 gc 方法会改变元素的位置,因此会重新计算插入的位置并插入,GrowingArrayUtils.insert() 源码如下:
    public static int[] insert(int[] array, int currentSize, int index, int element) {
        assert currentSize <= array.length;
    
        if (currentSize + 1 <= array.length) {
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            array[index] = element;
            return array;
        }
    
        int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
        System.arraycopy(array, 0, newArray, 0, index);
        newArray[index] = element;
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }
    

    在 array 不需要扩容的情况下可以添加一个元素,则先将待插入位置 index 开始的元素整体后移一位,然后再插入元素。否则执行 growSize() 方法进行扩容,每次扩容时如果当前容量小于等于 4 则扩容 为 8,否则扩容容量为原容量的 2 倍,代码如下:

    public static int growSize(int currentSize) {
            return currentSize <= 4 ? 8 : currentSize * 2;
    }
    

    删除方法

    SparseArray 提供了 2 种删除方式。

    1. 根据 key 删除对象
        /**
         * Removes the mapping from the specified key, if there was any.
         */
        public void delete(int key) {
            int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
            if (i >= 0) {
                if (mValues[i] != DELETED) {
                    mValues[i] = DELETED;
                    mGarbage = true;
                }
            }
        }
    
    1. 根据位置删除对象
        public void 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);
            }
            if (mValues[index] != DELETED) {
                mValues[index] = DELETED;
                mGarbage = true;
            }
        }
    

    这里的逻辑还是比较简单的,找到要删除的元素,标记为 DELETED,然后把 mGarbage 标记为 true。

    gc

    gc 方法将 mValue 数组中还未标记为 DELETED 的元素以及对应下标的 mKeys 数组中的元素移动到数组的前面,保证数组前面的数据都是有效数据,而不是存储着 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) {
                    if (i != o) {
                        keys[o] = keys[i];
                        values[o] = val;
                        values[i] = null;
                    }
    
                    o++;
                }
            }
    
            mGarbage = false;
            mSize = o;
    
            // Log.e("SparseArray", "gc end with " + mSize);
        }
    

    get() 方法

    get() 方法主要是通过二分查找获取到 key 的索引,通过该索引俩获取到对应的 value。源码如下:

        /**
         * 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) {
            int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
            if (i < 0 || mValues[i] == DELETED) {
                return valueIfKeyNotFound;
            } else {
                return (E) mValues[i];
            }
        }
    

    拓展

    除了 SparseArray,Android 还提供了一系列的 Sparse*() 方法,如下:

    • SparseIntArray — key(int) : value(int)
    • SparseBooleanArray — key(int) : value(boolean)
    • SparseLongArray — ikey(int) : value(long)

    总结

    至此,我们大概了解 SparseArray 原理,与 HashMap 相比:

    \color{red}{优势}

    • 避免了基本数据类型的自动装箱,不会创建多余的对象,内存利用率更高。
    • 没有额外的存储结点的数据结构,同上,内存利用率更高。

    \color{red}{劣势}

    • 插入操作、gc 操作需要复制数组,效率低。
    • 查询时间复杂度为 O(logN),数据量巨大时查询明显下降。

    参考

    相关文章

      网友评论

          本文标题:SparseArray 实现原理简析

          本文链接:https://www.haomeiwen.com/subject/xejtahtx.html