SparseArray 源码解析

744 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第23天,点击查看活动详情

背景

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

delete()方法

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 相比:

优势

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

劣势

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