【内存优化】SparseArray源码分析

SparseArray是一个Interger和Object的键值对,相当于HashMap<Integer,Object>

它具有以下特点:
1、它不同于Array<Object>的是它的键值可以为不连续的数字,这点应该很好理解,数组的索引值是连续固定的,它可以任意且不连续,但是它不重复且是有序的。
2、它相比于HashMap<Integer,Object>具有更好的内存效率,因为它不仅避免了键值的自动装箱,并且对于所有映射,它的数据结构不依赖于其他的实体对象,因为它内部维系的就是两个数组。它使用二分查找来索引对应的键值,通过键值就可以找到数组中对应的对象,但是它不适合包含大量的数据项,如果包含大量的数据项,它的效率将会比HashMap低,主要是因为它的插入删除操作是对数组进行插入和删除,在数组中进行插入和删除效率是比较低的,因为它要进行数据项的统一前后移动。

正是因为数组在插入和删除操作的缺陷,在SparseArray在设计的时候也进行了一些优化,在删除操作之后,它并不是立刻进行整个数组的压缩,而是将删除项标记为删除,这样在插入的时候就可以检查该项标记,这样就可以进行复用了,当需要重新进行内存分配的时候,再统一的进行内存回收,将标记的项回收掉。

下面我们来分析源码。
一、构造函数

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

public SparseArray() {
    this(10);
}

public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
    mKeys = ContainerHelpers.EMPTY_INTS;
    mValues = ContainerHelpers.EMPTY_OBJECTS;
} else {
    initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
    mKeys = new int[initialCapacity];
    mValues = new Object[initialCapacity];
}
mSize = 0;
}

从上面可以看到,它内部维系的就是两个数组,一个整形数组mKeys,代表键;一个对象数组mValues,代表值。
从上面可以看到,它的构造函数有两种,对于无参构造函数,它默认initialCapacity参数为10,所以我直接来看第二个构造函数:
1、如果initialCapacity==0:
在ContainerHelpers里面:

static final int[] EMPTY_INTS = new int[0];
    static final Object[] EMPTY_OBJECTS = new Object[0];

它为mKeys和mValues分别赋值为一个空数组。
2、initialCapacity!=0:
首先使用idealIntArraySize计算数组的大小,然后分别为它们创建一个数组。

二、删除操作

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、将要删除的项用DELETED赋值
2、mGarbage=true,表示有内存需要回收

DELETE的定义如下,它就是一个对象:

private static final Object DELETED = new Object();

三、插入函数

public void put(int key, E value) {
    //1、二分查找找到对应的index
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    //如果i>=0,说明对应的key有值,只需要进行数据的更新即可,即重新赋值
    if (i >= 0) {
        mValues[i] = value;
    } else {  // 如果 i<0
        i = ~i;
        //如果i在数组范围之内并且对应的值标记为删除,直接复用即可
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        //如果数组空间不足,并且mGarbage为真
        if (mGarbage && mSize >= mKeys.length) {
            //进行回收操作
            //它会进行数组的移动,就DELETE标记项删除
            gc();

            // 重新查找索引
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        //如果空间不足
        if (mSize >= mKeys.length) {
            //重新分配内存大小
            int n = ArrayUtils.idealIntArraySize(mSize + 1);
            //重新分配内存
            int[] nkeys = new int[n];
            Object[] nvalues = new Object[n];

            // 将数组的内存复制到新数组中
            System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
            System.arraycopy(mValues, 0, nvalues, 0, mValues.length);

            mKeys = nkeys;
            mValues = nvalues;
        }

        if (mSize - i != 0) {
            // 进行数组的统一移动,把索引为i的项移出来
            System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
            System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
        }
        //完成插入操作
        mKeys[i] = key;
        mValues[i] = value;
        mSize++;
    }
}

我们来看看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);
}

它就是进行了数组的移动,将标记为DELETE的项删掉。

在删除对象之后,mGarbage赋值true;表示有可回收的内存,下面看看在哪些情况下会触发gc函数的调用,gc调用的前提是mGarbage为ture。

1、插入元素,空间不足,就是上面插入操作的那种情况。

2、获取容器大小的时候:

public int size() {
    if (mGarbage) {
        gc();
    }

    return mSize;
}

3、获取key

public int keyAt(int index) {
    if (mGarbage) {
        gc();
    }

    return mKeys[index];
}

4、获取value

public E valueAt(int index) {
    if (mGarbage) {
        gc();
    }

    return (E) mValues[index];
}

5、设置value

public void setValueAt(int index, E value) {
    if (mGarbage) {
        gc();
    }

    mValues[index] = value;
}

6、得到指定key的索引

public int indexOfKey(int key) {
    if (mGarbage) {
        gc();
    }

    return ContainerHelpers.binarySearch(mKeys, mSize, key);
}

从这里我们也可以看出索引与key的区别。

7、得到指定值的索引

public int indexOfValue(E value) {
    if (mGarbage) {
        gc();
    }

    for (int i = 0; i < mSize; i++)
        if (mValues[i] == value)
            return i;

    return -1;
}

最后就剩下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();
    }

    int pos = mSize;
    if (pos >= mKeys.length) {
        int n = ArrayUtils.idealIntArraySize(pos + 1);

        int[] nkeys = new int[n];
        Object[] nvalues = new Object[n];

        // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
        System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
        System.arraycopy(mValues, 0, nvalues, 0, mValues.length);

        mKeys = nkeys;
        mValues = nvalues;
    }

    mKeys[pos] = key;
    mValues[pos] = value;
    mSize = pos + 1;
}

跟上面的put函数一样,只是它是在尾部就行插入,不是在指定的索引,所以相比较于put函数,它不需要进行数组的移动。

另外,与SparseArray相似的还有下面这些类,它们内部维系的都是两个数组,下面我们来进行一个简单的比较:

SparseArray:

private int[] mKeys;  //key为int类型的数组
private Object[] mValues;  //value为Object类型的数组

LongSparseArray:

private long[] mKeys; // key为long类型的数组
private Object[] mValues;  //vlaue为Object类型的数组

SparseBooleanArray:

private int[] mKeys;  //key为int类型的数组
private boolean[] mValues; //value为boolean类型的数组

SparseIntArray:

private int[] mKeys;  //key为int类型的数组
private int[] mValues;  //vlaue为int类型的数组

SparseLongArray:

private int[] mKeys;  //key为int类型的数组
private long[] mValues;  //value为long类型的数组

你可能感兴趣的:(源码,优化,内存)