美文网首页
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 实现原理简析

    背景 SparseArray 是用于存储 K-V 的数据结构,但是并没有像 ArrayMap 和 HashMap一...

  • ArrayMap详解及源码分析

    一、前言 在 《SparseArray详解及源码简析》 中,我们熟悉了 SparseArray 的基本用法、特点以...

  • shiro原理简析+基于springboot基础实践

    1、shiro原理简析 原理简析: 1、subject支持不通调用获取用户信息 2、SecurityManager...

  • 数据结构篇

    数据结构篇 LruCache实现原理(分为内存lru和diskLru两种实现) SparseArray与HashM...

  • JSONP实现原理-简析

    使用script标签是如何实现跨域请求的,它是一个新技术,还是一个技巧?下面我们来看看,其中简单的原理: 我们写一...

  • 简析KVOController实现原理

    KVOController是FaceBook的一个开源库,提供了方便的姿势让你去使用KVO。https://git...

  • 简析vue实现原理

    本文学习自https://github.com/DMQ/mvvm,建议大家去阅读原文。 几种实现双向绑定的做法 目...

  • Android-SparseArray源码解析

    一、SparseArray原理 SparseArray中采用的是双数组的方式,在SparseArray有一个int...

  • Android SparseArray ViewHolder

    SparseArray原理 单纯从字面上来理解,SparseArray指的是稀疏数组(Sparse array),...

  • sudo命令实现原理简析

    sudo是Linux上的常用命令,其目的是为了给普通用户提升操作权限,完成一些需要root权限才能完成的任务。那么...

网友评论

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

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