Android的SparseArray和ArrayMap

为了节省内存,Android中引入了SparseArray和ArrayMap特有的数据结构。本文分析SparseArray和ArrayMap的原理。

SparseArray

SparseArray和ArrayMap一样,都是为了更高效的保存key为int型的<key,value>数据,用了同样的数据结构,但是为了提高效率,SparseArray也做了自己的优化。接下来就分析一下SparseArray的存储,添加和删除元素。

继承结构

在这里插入图片描述
SparseArray并没有像ArrayMap一样实现Map接口,仅仅实现了Cloneable接口。

存储结构

在这里插入图片描述

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

public class SparseArray<E> implements Cloneable {
    
    
    private static final Object DELETED = new Object();
    /**
    	SparseArray中是否存在垃圾,为true表示有对象已被删除,可以启动gc,gc完成之后会把mGarbage置为false
    */
    private boolean mGarbage = false;

    private int[] mKeys;//存储int型的key值(注意存的是key本身,没有进行hash计算)
    private Object[] mValues;
    /**
    当前集合中键值对的数量
    */
    private int mSize;

    /**
     * Creates a new SparseArray containing no mappings.
     */
    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;
    }
    
    
	...

}

put

//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
}
    
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 {
    
    
        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,则直接更新并返回。如果存在垃圾(mGarbage为true),且需要扩大数组的容量(mSize >= mKeys.length),则先执行gc函数,由于执行gc函数之后元素会发生移动,故重新计算待插入位置,最后执行元素的插入。插入函数分为插入key和插入value两步。GrowingArrayUtils.insert的源码如下:

public static int[] insert(int[] array, int currentSize, int index, int element) {
    
    
    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 = ArrayUtils.newUnpaddedIntArray(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;
}

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

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

delete

SparseArray的删除有两种:根据key删除对象,删除指定index位置的对象。

根据key删除对象

    /**
     * Removes the mapping from the specified key, if there was any.
     */
    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在mKeys中找到key的位置,如果key存在,则返回key在mKeys中的下标,否则返回试图将key插入到mKeys中的位置的取反。找到待删除元素的下标后,SparseArray并没有像ArraySet和ArrayMap一样去删除元素,只是将待删除元素标记为DELETED,然后将mGarbage设置为true。DELETED实际上就是一个对象,具体申明为: Object DELETED = new Object(),SparseArray有gc的过程,后面会分析这个gc的过程。

删除指定index位置的对象

    /**
     * Removes the mapping at the specified index.
     *
     * <p>For indices outside of the range <code>0...size()-1</code>,
     * the behavior is undefined.</p>
     */
    public void removeAt(int index) {
    
    
        if (mValues[index] != DELETED) {
    
    
            mValues[index] = DELETED;
            mGarbage = true;
        }
    }

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

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

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函数实际上就是将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的元素前移,在进行元素查找时可以减少需要查找的元素的数量,减少查找的时间,在添加元素的时候也可以更加快速的找到待插入点。

总结

1.SparseArray内部主要通过 2 个数组来存储 key 和 value,分别是 int[] 和 Object[]。这也限定了其 key 只能为 int 类型,且 key 不能重复,否则会发生覆盖。

2.一切操作都是基于二分查找算法,将 key 以升序的方法 “紧凑” 的排列在一起,从而提高内存的利用率以及访问的效率。相比较 HashMap 而言,这是典型的时间换空间的策略。

3.删除操作并不是真的删除,而只是标记为 DELETE,以便下次能够直接复用。

4.SparseArray主要是为了优化int值到Object映射的存储,提高内存的使用效率。相较于HashMap,在存储上的优化如下:
1).使用int和Object类型的数组分别存储key和value,相较于HashMap使用Node,SparseArray存储key-value时不需要额外的结构体,更节省内存,
2).SparseArray使用int数组存储int类型的key,避免了int到Integer的自动装箱机制
3).虽然在存储int到Object映射时的内存使用效率更高,由于使用数组存储数组,在添加或者删除元素时需要进行二分查找,数据条数特别多的时候,效率会低于HashMap,谷歌给出的建议是数据量不要超过1000。
有优点就一定有缺点:
插入操作需要复制数组,增删效率降低
数据量巨大时,复制数组成本巨大,gc()成本也巨大
数据量巨大时,查询效率也会明显下降

ArrayMap

ArraySet

参考:
ArrayMap完全剖析
面试必备:ArrayMap源码解析
ArrayMap详解及源码分析

Android集合之SparseArray、ArrayMap详解
Android轻量级数据SparseArray详解

猜你喜欢

转载自blog.csdn.net/yzpbright/article/details/107473132