SparseArray详解及源码简析

一、前言

SparseArray 是 Android 在 Android SdK 为我们提供的一个基础的数据结构,其功能类似于 HashMap。与 HashMap 不同的是它的 Key 只能是 int 值,不能是其他的类型。

二、代码分析

1. demo 及其简析

首先也还是先通过 demo 来看一看 SparseArray 的基本使用方法,主要就是插入方法以及遍历方法。这也会后面的代码分析打下一个基础。

        SparseArray<Object> sparseArray = new SparseArray<>();
        sparseArray.put(0,null);
        sparseArray.put(1,"fsdfd");
        sparseArray.put(2,new String("fjdslfjdk"));
        sparseArray.put(3,1);
        sparseArray.put(4,new Boolean(true));
        sparseArray.put(5,new Object());
        sparseArray.put(8,new String("42fsjfldk"));
        sparseArray.put(20,"jfslfjdkfj");
        sparseArray.put(0,"chongfude");

        int size = sparseArray.size();
        for (int i = 0;i < size;i++) {
            Log.d(TAG, "sparseArraySample: i = " + i + ";value = " + sparseArray.get(sparseArray.keyAt(i)) );
        }
复制代码

上面代码先是 new 了一个 SparseArray,注意声明时只能指定 value 的类型,而 key 是固定为 int 的。然后再往里面添加 key 以及 value。这里注意一下的是 key 为 0 的情况插入了 2 次。遍历时,是先通过顺序的下标取出 key ,再通过 keyAt 来 get 出 value。当然也可以一步到位通过 valueAt() 直接获取到 value。然后这个 demo 的执行结果如下。

sparseArraySample: i = 0;value = chongfude sparseArraySample: i = 1;value = fsdfd sparseArraySample: i = 2;value = fjdslfjdk sparseArraySample: i = 3;value = 1 sparseArraySample: i = 4;value = true sparseArraySample: i = 5;value = java.lang.Object@b67a0fa sparseArraySample: i = 6;value = 42fsjfldk sparseArraySample: i = 7;value = jfslfjdkfj

然后通过 Debug 来看一看在内存中,SparseArray 实际是如何存储的。如下图分别是 key 与 value 在内存中的形式。可以看出 keys 和 values 的大小都为 13,而且 keys 的值是按从小到大顺序排列的。

keys

values

2.源码分析

下面是 SparseArray 的类图结构,可以看到其属性非常的少,也可以看出其分别用了数组 int[] 和 object[] 来存储 key 以及 value。

SparseArray.jpg

扫描二维码关注公众号,回复: 4909763 查看本文章
  • SparseArray 初始化 SparseArray 的初始化也就是它的构造方法。
    public SparseArray() {
        this(10);
    }

   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 个构造方法,带参与不带参。当然,这个参数就是指定数组初始大小,也就是 SparseArray 的初始容量。而不带参数则默认指定数组大小为 10 个。

  • 插入数据 put() 方法
public void put(int key, E value) {
        // 1.先进行二分查找
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 2. 如果找到了,则 i 必大于等于 0
        if (i >= 0) {
            mValues[i] = value;
        } else {
        // 3. 没找到,则找一个正确的位置再插入
            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++;
        }
    }
复制代码

这里调用了很多外部的方法以及内部的方法。首先是 ContainerHelpers#binarySearch() 的二分查找算法。

    //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) {
            // 高位+低位之各除以 2,写成右移,即通过位运算替代除法以提高运算效率
            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
            }
        }
        //若没找到,则lo是value应该插入的位置,是一个正数。对这个正数去反,返回负数回去
        return ~lo;  // value not present
    }
复制代码

二分查找的分析属于基础内容,在注释中了。回到 put() 方法首先通过二分查找算法从当前 keys 中查找是否已经存在相同的 key 了,如果存在则会返回大于等于 0 的下标,然后接下来就会将原下标下的 values 中的旧value 替换成新的 value 值,即发生了覆盖。

那如果没有找到,那么将 i 取反就是要插入的位置了,这一结论正好来自 binarySearch() 的返回结果。可以看到其最后如果没有找到的话,就会返回 lo 的取反数。那么这里再把它取反过来那就是 lo 了。

这里如果 i 是在大小 mSizes 的范围内的,且其对应的 values[i] 又刚是被标记为删除的对象,那么就可以复用这个对象,否则就还是要依当前的 i 值进一步寻找要插入的位置,再插入相应的 value。

在插入之前,如果由于之前进行过 delete(),remoeAt() 以及 removeReturnOld() 中的某一个方法,那就可能要进行 gc() 操作。当然,这里不是指的内存的 gc()。

private void gc() {
        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;
    }
复制代码

通过代码很容易分析得出,这里的 gc ,实际就是压缩存储,简单点说就是让元素挨得紧一点。

而 gc() 完之后,下标 i 可能会发生变化,因此需要重新查找一次,以得到一个新的下标 i。

最后就是通过 GrowingArrayUtils.insert() 来进行 key 和 value 的插入。这个 insert() 根据数组类型重载了多个,这里只分析 int[] 类型的即可。

public static int[] insert(int[] array, int currentSize, int index, int element) {
        //确认 当前集合长度 小于等于 array数组长度
        assert currentSize <= array.length;
        //不需要扩容
        if (currentSize + 1 <= array.length) {
            //将array数组内从 index 移到 index + 1,共移了 currentSize - index 个,即从index开始后移一位,那么就留出 index 的位置来插入新的值。
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            //在index处插入新的值
            array[index] = element;
            return array;
        }
        //需要扩容,构建新的数组,新的数组大小由growSize() 计算得到
        int[] newArray = new int[growSize(currentSize)];
        //这里再分 3 段赋值。首先将原数组中 index 之前的数据复制到新数组中
        System.arraycopy(array, 0, newArray, 0, index);
        //然后在index处插入新的值
        newArray[index] = element;
        //最后将原数组中 index 及其之后的数据赋值到新数组中
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }
复制代码

上面的算法中,如果不需要扩容则直接进行移位以留出空位来插入新的值,如果需要扩容则先扩容,然后根据要插入的位置 index,分三段数据复制到新的数组中。这里再看看 growSize() 是如何进行扩容 size 的计算的。

    public static int growSize(int currentSize) {
        //如果当前size 小于等于4,则返回8, 否则返回当前size的两倍
        return currentSize <= 4 ? 8 : currentSize * 2;
    }
复制代码

代码相对简单,当前 size 小于等于 4 则为 8 ,否则为 2 倍大小。

  • get() 方法
    public E get(int key) {
        return get(key, null);
    }
    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,前面在分析 put() 的时候已经分析过了二分查找。那么这里如果找到了,就会通过下标直接从 mValues[] 中返回。

  • 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;
            }
        }
    }
复制代码

delete() 也非常简单,通过二分查找算法定位到下标,然后将对应的 value 标记为 DELETE,并且标记需要进行 gc 了。这里需要注意的是被标记为 DELETE 的 value 不会在 gc 中被移除掉,而只会被覆盖掉,从而提高了插入的效率。

三、总结

文章对 SparseArray 进行了简要的分析,文章也只对主要的几个方法进行了分析,其他没有分析到的方法在这个基础上再进行分析相信也是很简单的。而总结下来几点是:

  • 其内部主要通过 2 个数组来存储 key 和 value,分别是 int[] 和 Object[]。这也限定了其 key 只能为 int 类型,且 key 不能重复,否则会发生覆盖。
  • 一切操作都是基于二分查找算法,将 key 以升序的方法 “紧凑” 的排列在一起,从而提高内存的利用率以及访问的效率。相比较 HashMap 而言,这是典型的时间换空间的策略。
  • 删除操作并不是真的删除,而只是标记为 DELETE,以便下次能够直接复用。

最后,感谢你能读到并读完此文章。受限于作者水平有限,如果存在错误或者疑问都欢迎留言讨论。如果我的分享能够帮助到你,也请记得帮忙点个赞吧,鼓励我继续写下去,谢谢。

猜你喜欢

转载自juejin.im/post/5c3b713ae51d4551b80f63b8