SparseArray 源码分析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

因为泛型擦除的原因基本数据类型不能作为泛型参数,如果需要就得使用它们的包装类就带来了装箱拆箱的开销,为了解决这个问题才有了 SparseArray 相关的几个类,SparseArray 比 HashMap 要慢一些是时间换空间的实现。


提示:基于 Android SDK 28

一、SparseArray 是什么 ?

SparseArray 是 Android 平台用来存储 key 为 int ,value 为 Object 的数据结构,它的内部实现是两个数组,一个 int 数组保存有序的 key 一个 Object 数组保存 value 并且 key 与 value 所在数组中的索引位置一致。另外为了提高性能对删除做了优化,当删除一个数据时并不是立即从 value 中删除而是先标记为删除,当对应的位置再次存储数据时可以直接复用这个空间避免每次删除添加元素时都移动元素

二、源码分析

1.delete

    public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

通过二分查找拿到指定 key 在 key 数组中的位置,如果 i 大于 0 说明指定 key 存在,如果指定位置的 value 没有标记为删除则标记为删除并且把 mGarbage 标记为 true 表示可以进行垃圾回收

2.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++;
        }
    }

    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
            }
        }
        return ~lo;  // value not present
    }

binarySearch 方法的作用是如果指定的 key 存在则返回指定 key 的位置否则返回 key 应该插入位置的取反值。如果 i 大于 0 说明指定的位置已经存储了值直接覆盖 value,否则对 i 取反得到应该插入的位置,如果这个位置是删除标记则直接覆盖 key 和 value ,否则判断如果数组有已经满了并且数组已经满了则调用 gc 把 “稀疏数组” 变为正常数组

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

gc 方法的作用是移动有效数据把标记为删除的数据覆盖,使 “稀疏数组” 变成连续数组并且重新记录 mSize 的值并且把 mGarbage 置为 false,进行了 gc 操作之后因为数据变化再次使用二分查找拿到 key 应该插入的位置,以为 key 可能不存在所以直接取反再把 key value 插入到数组中

    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;
    }
	
	// 根据当前数组大小返回扩容后的数组大小
    public static int growSize(int currentSize) {
    	// 如果当前数组大小小于 4 则返回 8 否则返回当前数组大小 * 2 
        return currentSize <= 4 ? 8 : currentSize * 2;
    }

insert 做了真正的数据存储的工作,判断是否需要扩容把元素存储到指定位置,扩容规则也大致是当前容量的 2 倍

3.append

    public void append(int key, E value) {
        if (mSize != 0 && key <= mKeys[mSize - 1]) {
            put(key, value);
            return;
        }

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

        mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
        mValues = GrowingArrayUtils.append(mValues, mSize, value);
        mSize++;
    }

有一种情况,要插入的 key 比 key 数组中所有的元素都大,append 对这种场景做了优化直接调用 GrowingArrayUtils.append 插入元素而不需要进行二分查找并且减少了移动数组的操作,所以应优先使用 append 添加数据

4.get

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

get 操作比较简单如果通过 key 找到了对应位置并且对应位置的 value 没有标记为删除则返回对应位置的 value 否则返回指定的默认值

5.其他

  • size 返回有效元素的个数
  • clear 清除所有元素 mSize 置为 0 mGarbage 置为 false
  • indexOfKey 返回指定 key 在数组中的索引找不到则返回负值
  • indexOfValue 返回指定 value 在数组中的索引找不到则返回 -1(使用 == 比较元素的值)
  • indexOfValueByValue 返回指定 value 在数组中的索引找不到则返回 -1(使用 equals 比较元素的值)
  • keyAt 获取指定位置的 key
  • valueAt 获取指定位置的 value
  • setValueAt 设置指定位置的 value 的值
  • remove 与 delete 一致
  • removeAt 将指定位置的值标记为删除
  • removeAtRange 将指定区域的值标记为删除

三、最后

SparseArray 还有几个其他变体

  • SparseIntArray 是 key 为 int 类型 value 为 int 类型容器
  • SparseLongArray 是 key 为 int 类型 value 为 long 类型容器
  • SparseBooleanArray 是 key 为 int 类型 value 为 boolean 类型容器
  • LongSparseArray 是 key 为 long 类型 value 为 Object 类型的容器

SparseArray 的使用场景

  • 数据量不大(千以内)
  • 空间比时间重要
  • 需要使用 key 为 int 类型的容器

参考

面试必备:SparseArray源码解析
这一次,彻底搞懂SparseArray实现原理
面试还在问 SparseArray?记住 3 句话让你临时把佛脚抱好!
Android数据容器之SparseArray
内存优化:充满矛盾的SparseArray

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值