Android中的SparseArray你了解吗?

1,390 阅读4分钟

SparseArray

SparseArrayAndroid 中特有的 key-value 存储结构,它类似于 HashMapArrayMap,不同的是 SparseArraykey 值 只能是 int 类型。除了 SparseArray 之外还有 SparseBooleanArraySparseIntArraySparseLongArraySparseSetArray,顾名思义他们只是将存储的 value 具体化而已。下面我们只分析 SparseArray ,其他的实现几乎一致

本篇文章我们就来解析下 SparseArray ,文中 SparseArray 基于 Android 10

SparseArray 的源码其实并不多,大概就500行

二分查找

在源码解析之前,我们需要先弄懂二分查找。SparseArray 在插入、查找和删除等都是通过二分查找去实现的,调用了 ContainerHelpers.binarySearch 方法,ArrayMap 中也是通过这个方法查找的,我们就来看下 ContainerHelpers.binarySearch 怎么实现的吧

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

        // 中间值小于目标值,从(mid,hi]区间查找
        if (midVal < value) {
            lo = mid + 1;
        // 中间值大于目标值,从[lo,mid)区间查找  
        } else if (midVal > value) {
            hi = mid - 1;
        // 这里就是找到了目标值了,然后mid索引    
        } else {
            return mid;  // value found
        }
    }
    // 数组中没有目标值,返回~lo,可以通过负数判断是否存在目标值,如果需要将目标值存储,那么目标值所在的位置就是lo
    return ~lo;  // value not present
}

首先我们要知道二分查找是对于有序的数组,ContainerHelpers.binarySearch 传入的数组是正序排序的(从小到大)。这里就用示例图展示下二分查找

假设我们需要从数组[0,1,2,4,8,9,13,16]查找13所在的位置

步骤一: 首次查找时,我们将 lo 置为0,hi 置为7,那么mid就等于3

search0.png

步骤二: 此时由于midVal=4<13,我们需要从(mid,hi]之间查找,将lo置为4,那么mids就等于5

search1.png

步骤三: 此时由于midVal=9<13,我们需要从(mid,hi]之间查找,将lo置为6,那么mids就等于6

search2.png

步骤四: 此时midVal=13,刚好等于我们的目标值13,所以我们就找到了13所在的位置是6

假如我们需要查找的元素是12呢?
我们还是用示例图分析下,步骤一二三都不变,步骤三 之后,我们的 lo = mids = 6,hi = 7,但是此时midVal = 13 > 12,所以我们需要从[lo,mid)之间查找,随后我们会将hi置为5,如下图所示

search3.png

很显然 lo > hi,不满足条件 while (lo <= hi) 循环了,然后我们返回了 ~6。这个~6是什么概念?假如我们需要将12存进数组,是不是需要存放在9和13的中间,那么~(~6)不就是我们所要存储的位置吗?

想必到这里大家就知道了 ContainerHelpers.binarySearch 的作用了吧,它不光是可以查找已经存在元素的索引,还能将不存在的元素索引计算出来方便存储

构造函数

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

可以看到 SparseArray 的默认长度是10,构造函数里面会实例 mKeysmValues,看下 mKeysmValues 是啥

private int[] mKeys;
private Object[] mValues;

mKeysint 类型的数组,mValuesobject 类型的数组,想必大家都知道了 mKeys 就是存放的 key 值,mValues 存放的是 value,并且 mKeys 是正序排序的。

存放数据

SparseArray 中存放数据时,调用 putappend 方法,类似这样

val sparseArray = SparseArray<String>()
sparseArray.put(1, "1")
sparseArray.append(2, "2")

append 方法其实内部也是调用了 put 方法这里就不分析 append 了,那么我们就来看下 put 方法是如何实现的吧

public void put(int key, E value) {
    // 二分查找key所在的索引位置
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    // 数组中存在该key值,覆盖该key值
    if (i >= 0) {
        mValues[i] = value;
    } else {
        // 数组中不存在该key值,获取存放key值的位置
        i = ~i;

        // i < 数据容量并且mValues[i]是DELETED,将key和value存在i索引上
        // 为什么要设置成DELETED?如果不设置成DELETED,那么往i位置添加元素,那么i位置之后的元素都要往后移,性能又是比较大的损耗
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        // mValues中存在DELETED并且mKeys存满了
        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++;
    }
}
  1. 通过二分查找获取 key 是否存在 mKeys 中,如果存在那么将 mValues 对应索引的值置为 value,相当于发生了替换。

  2. 假如 key 不在 mKeys 中,将 i 取反获取 key 存放的索引位置。如果 i < mSize 并且 mValues[i] 又是被标记成了 DELETED(调用了 delete 或 remove 方法),就把 keyvalue 存放在 i 索引上

  3. 假如 mValues 中存在 DELETED 并且 数据容量大于 mKeys 的长度时,调用 gc 方法移除 DELETED,调用 gc 之后 mKeysmValues 的排序都发生了变化,所以要通过二分查找重新获取元素保存的位置,最后通过 GrowingArrayUtils.insertkeyvalue 分别插入 mKeysmValues

我们再来看下 GrowingArrayUtils.insert 是如何插入元素的

// com.android.internal.util.GrowingArrayUtils.java 
public static int[] insert(int[] array, int currentSize, int index, int element) {
    // 断言数组还未越界
    assert currentSize <= array.length;

    // 假如可以插入元素,那么只需要将index及后面的元素往后挪一位,然后往index位置插入元素即可
    if (currentSize + 1 <= array.length) {
        System.arraycopy(array, index, array, index + 1, currentSize - index);
        // 将数组index位置的元素置为element
        array[index] = element;
        return array;
    }

    // 原数组无法插入元素(容量不够了),根据growSize的长度创建新的数组
    int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
    // 将原数组[0,index)位置的元素拷贝到新数组
    System.arraycopy(array, 0, newArray, 0, index);
    // 将新数组index位置的元素置为element
    newArray[index] = element;
    // 最后将原数组[index,array.length)的元素全部拷贝到新数组index+1之后的位置上
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
}

上面的注释写得很清楚了,相信大家都能看得懂,其实就是扩容与不扩容的问题。再来看下 growSize 的大小是如何计算的

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

当前 Size <= 4 时恒定大小为8,否则就是 Size 的两倍了。

查找数据

SparseArray 提供了 get 方法供我们获取数据, get 方法有两个重载函数,归根到底都是调用了两个参数的。

public E get(int key) {
    return get(key, null);
}

// 默认值
public E get(int key, E valueIfKeyNotFound) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    // 查找不到该key值或者该位置上是DELETED时,返回默认值
    if (i < 0 || mValues[i] == DELETED) {
        return valueIfKeyNotFound;
    } else {
        // 返回该位置的元素
        return (E) mValues[i];
    }
}

两个参数的 get 方法可以设置默认值,当查找不到该 key 值返回默认值,一个参数的 get 方法默认值是 null

删除数据

SparseArray 中提供了 两种删除方式。

  1. 根据 key 值删除,remove 方法和 delete 方法
  2. 根据索引位置删除,removeAt 方法和 removeAtRange 方法

先来看一下 根据 key 值删除的实现

public void delete(int key) {
    // 通过二分查找获取key值的位置
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    // 存在该key值
    if (i >= 0) {
        // 该位置上不是DELETED
        if (mValues[i] != DELETED) {
            mValues[i] = DELETED;
            // mGarbage置为true,为调用gc方法提供条件
            mGarbage = true;
        }
    }
}

// 直接调用了delete方法
public void remove(int key) {
    delete(key);
}

其实很简单,通过二分查找获取 key 值的位置,如果 key 值是存在的并且该位置不是 DELETED,那么将该位置置为 DELETED

再来看下根据索引位置删除

public void removeAt(int index) {
    // 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) {
        // 标记为DELETED
        mValues[index] = DELETED;
        mGarbage = true;
    }
}

// 范围内删除,可以删除多个数据
public void removeAtRange(int index, int size) {
    final int end = Math.min(mSize, index + size);
    for (int i = index; i < end; i++) {
        // 其实还是通过removeAt删除
        removeAt(i);
    }
}

这两种方式的实现逻辑其实是一样的,都是将该位置上的对象标记为DELETED

gc

这里的 gc 不是我们常说的 jvm gc,而是 SparseArray 中的 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];

        // 判断DELETED
        if (val != DELETED) {
            if (i != o) {
                // o位置上置为keys[i]
                keys[o] = keys[i];
                // o位置上置为values[i]
                values[o] = val;
                // i位置置为null
                values[i] = null;
            }

            o++;
        }
    }

    // 完成之后置为false,避免再次gc
    mGarbage = false;
    mSize = o;

    // Log.e("SparseArray", "gc end with " + mSize);
}

这里用示例图演示下 gc 方法, SparseArray 的长度默认为10,并且 mValues 中存在两个DELETED

gc0.png

第一次出现DELETED的是3索引上,那么 mKeysmValues 有如下变化

gc1.png

gc2.png

gc3.png

第二次出现DELETED的是7索引上,那么此时 mKeysmValues 也会变化,如下所示

gc4.png

gc5.png

经过 gc 之后,mKeysmValues 都发生了变化,mValues 会将非DELETED整体往前迁移并且将末尾的元素都置为空,mKeys 由于要对应上 mValues 的变化 对应位置上的 key 值也要向前迁移。

总结

上来就是本篇文章的内容了,主要针对 SparseArray 的存储、删除进行了详解,当然了还有一些其他方法大家可以自行查阅。这里对 SparseArray 做一个小结

  1. SparseArray 存放的 key 值只能是 int 类型,因为存放 key 值的数组有序排序并且是升序的

  2. 所有的操作都是基于二分查找,当查找到 key 值时返回该位置的索引,否则返回该位置索引的逻辑非

  3. 删除操作中只是将该位置上的元素标记为DELETED,并没有移除该位置的元素,当 gc 时才会将非 DELETED对象往前迁移