SparseArray原理分析

系列文章地址:
Android容器类-ArraySet原理解析(一)
Android容器类-ArrayMap原理解析(二)
Android容器类-SparseArray原理解析(三)
Android容器类-SparseIntArray原理解析(四)

SparseArray和其他的Android容器类一样,都是为了更加有效地利用内存,说直白点,就是为了节省内存。SparseArrayArrayMap一样,都是为了更高效的保存int值到非原始类型的映射,用了同样的数据结构,但是为了提高效率,SparseArray也做了自己的优化。接下来就分析一下SparseArray的存储,添加和删除元素。

继承结构

image


上图表明,SparseArray并没有像ArrayMap一样实现Map接口,仅仅实现了Cloneable接口。

存储结构

image


存储结构和ArraySet以及ArrayMap一脉相承,都使用int数组存储key值,使用Object数组存储对象。不同点在于mKeys数组中存储的是添加元素的key值本身,没有进行hash值得计算。

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,如果找到,则直接更新对应下标的value。如果未找到,binarySearch方法返回待插入的下标的取反,故i = ~i。如果待插入的位置的元素已经被标记为DELETED,则直接更新并返回。如果需要执行gc函数,且需要扩大数组的容量(mSize >= mKeys.lengt),则先执行gc函数。由于执行gc函数之后元素会发生移动,故重新计算待插入位置,最后执行元素的插入。插入函数分为插入key和插入valueGrowingArrayUtils.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;
}

函数的逻辑很简单,首先断言了currentSize <= array.length;如果array在不需要扩大容量的情况下可以添加一个元素,则先将待插入位置index开始的元素整体后移一位,然后插入元素,否则先扩容,然后将元素拷贝到新的数组中。

删除

为什么删除的时候我没有使用一个具体的函数呢,是因为SparseArray的删除有两种:根据key删除对象,删除指定位置的对象。

根据key删除对象

public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    if (i >= 0) {
        if (mValues[i] != DELETED) {
            mValues[i] = DELETED;
            mGarbage = true;
        }
    }
}

ContainerHelpers.binarySearch函数在ArraySetArrayMap的元素查找中都出现过,作用是使用二分查找,在mKeys中找到key的位置,如果key存在,则返回keymKeys中的下标,否则返回试图将key插入到mKeys中的位置的取反。找到待删除元素的下标后,SparseArray并没有像ArraySetArrayMap一样去删除元素,只是将待删除元素标记为DELETED,然后将mGarbage设置为trueDELETED实际上就是一个对象,具体申明为: Object DELETED = new Object()SparseArraygc的过程,后面会分析这个gc的过程。

删除执行位置的对象

public void removeAt(int index) {
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}

删除指定位置元素的逻辑比较简单,判断待删除位置的元素是否已经被标记为DELETED,如果没有被标记,则标记指定位置的元素,并将mGarbage设置为true

元素在被删除之后,都会将标志mGarbage设置为true,这是执行gc的必要条件。

gc

说到gc,给我的第一感觉应该是什么高深的c/c++源码,其实不是,贴上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函数实际上就是将mValues数组中还未标记为DELETED的元素以及对应下标的mKeys数组中的元素移动到数组的前面,保证数组在0到mSize之间的元素都是未被标记为DELETED,经过gc之后,数据的位置可能会发生移动。

在元素被删除后,标志mGarbage设置为true,表示可以执行gc函数了。那么gc函数会在什么位置执行呢?
分析SparseArray源码可以发现,如果mGarbage设置为true,在以下函数调用中gc函数会执行:

put,append,size,keyAt,valueAt,setValueAt,indexOfKey,indexOfValue,indexOfValueByValue

将以上函数总结一下可以归纳为三类:

  • 向SparseArray添加元素
  • 修改SparseArray的mValues数组
  • 获取SparseArray的属性

通过执行gc将未被标记为DELETED的元素前移,在进行元素查找时可以减少需要查找的元素的数量,减少查找的时间,在添加元素的时候也可以更加快速的找到待插入点。

总结

SparseArray主要是为了优化int值到Object映射的存储,提高内存的使用效率。相较于HashMap,在存储上的优化如下:

  • 使用int和Object类型的数组分别存储key和value,相较于HashMap使用Node,SparseArray在存储单个key-value时更节省内存
  • SparseArray使用int数组存储int类型的key,避免了int到Integer的自动装箱机制

虽然在存储int到Object映射时的内存使用效率更高,由于使用数组存储数组,在添加或者删除元素时需要进行二分查找,元素较多(超过1000)时效率较低,谷歌给出的建议是数据量不要超过1000,这种情况下,相较于HashMap,效率降低不会超过50%。

关注微信公众号,最新技术干货实时推送

image

你可能感兴趣的:(SparseArray原理分析)