源码理解之SparseArray

SparseArray

SparseArray、HashMap对比

  • SparseArray采用时间换取空间的方式来提高手机App的运行效率
  • HashMap通过空间换取时间,查找迅速;(HashMap中当table数组中内容达到总容量0.75时,则扩展为当前容量的两倍)
  • 当小于某个数量临界点的时候,SparseArray的查询性能是优于 HashMap的,超过这个临界值SparseArray的查询性能是落后于 HashMap的;
  • 使用int[]数组存放key,避免了HashMap中基本数据类型需要装箱的步骤,其次不使用额外的结构体(Entry),单个元素的存储成本下降。

源码理解之SparseArray_第1张图片

使用方法

SparseArray sparseArray = new SparseArray<>();
SparseArray sparseArray = new SparseArray<>(capacity);

sparseArray.put(int key,Student value);
sparseArray.get(int key);
sparseArray.get(int key,Student valueIfNotFound);
sparseArray.remove(int key);

index

index就是SparseArray特有的属性了,SparseArray从名字上看就能猜到跟数组有关系,事实上他底层是两条数组,一组存放key,一组存放value,知道了这一点应该能猜到index的作用了。index 就是 key在数组中的位置

sparseArray.indexOfKey(int key);
sparseArray.indexOfValue(T value);
sparseArray.keyAt(int index);
sparseArray.valueAt(int index);
sparseArray.setValueAt(int index);
sparseArray.removeAt(int index);
sparseArray.removeAt(int index,int size);

实现原理

使用int[]数组存放key,避免了HashMap中基本数据类型需要装箱的步骤,其次不使用额外的结构体(Entry),单个元素的存储成本下降。

构造方法

构造方法初始化SparseArray只是简单的创建了两个数组:

private int[] mKeys;
private Object[] mValues;
private int mSize;//当前实际存放的数量
public SparseArray() {this(10);}//默认长度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(int key, E value)

public void put(int key, E value) {
        // 二分查找,key在mKeys列表中对应的index
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 如果找到相同Key值,则直接赋值
        if (i >= 0) {
            mValues[i] = value;
        } 
        // 找不到
        else {
            // binarySearch方法中,找不到时,i取了其非,这里再次取非,则非非则正
            i = ~i;
            // 如果该位置的数据正好被删除,则赋值
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            // 如果有数据被删除了,则gc
            if (mGarbage && mSize >= mKeys.length) {
                gc();
                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            // 插入数据,增长mKeys与mValues列表
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
}
  • 因为key为int,不存在hash冲突
  • mKeys为有序列表,通过二分查找,找到要插入的key对应的index (这里相对于查找hash表应该算是费时间吧,但节省了内存,所以是 时间换取了空间)
  • 如果找到相同Key值,则直接覆盖
  • 否则,如果该位置的数据正好被标记将要删除,则赋值
  • 如果需要执行gc()(注意不是系统的gc方法),则调用gc()方法并重新重新查找需要插入的index
  • 最后执行插入操作

GrowingArrayUtils.insert(mKeys, mSize, i, key)

public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;//断言
// 如果当前存储的长度加1还是小于数组长度
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;
}

get(int key)

// 通过key查找对应的value
public E get(int key) {
        return get(key, null);
}
// 通过key查找对应的value
public E get(int key, E valueIfKeyNotFound) {
        // mKeys数组中采用二分查找,找到key对应的index
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 没有找到,则返回空
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
        // 找到则返回对应的value
            return (E) mValues[i];
        }
}

每次调用get,则需经过一次mKeys数组的二分查找,因此mKeys数组越大则二分查找的时间就越长,因此SparseArray在大量数据,千以上时,会效率较低

ContainerHelpers.binarySearch(mKeys, mSize, key)二分查找

// array为有序数组
// size数组中内容长度
// value要查找的值
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];
            // 如果中间元素小于要查找元素,则midIndex赋值给 lo 
            if (midVal < value) {
                lo = mid + 1;
            }
            // 如果中间元素大于要查找元素,则midIndex赋值给 hi  
            else if (midVal > value) {
                hi = mid - 1;
            }
            // 找到则返回 
            else {
                return mid;  // value found
            }
        }
        // 找不到,则lo 取非
        return ~lo;  // value not present
}

remove()

public void remove(int key) {
delete(key);
}
public void delete(int key) {
// 找到该 key 的索引
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 如果存在,将该索引上的 value 赋值为 DELETED
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
// 标记当前状态为待回收
mGarbage = true;
}
}
}
private static final Object DELETED = new Object();

事实上,SparseArray在进行remove()操作的时候分为两个步骤:

删除value — 在remove()中处理
删除key — 在gc()中处理,注意这里不是系统的 GC,只是SparseArray 的一个方法
remove()中,将这个key指向了DELETED,这时候value失去了引用,如果没有其它的引用,会在下一次系统内存回收的时候被干掉。

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];
// 当前这个 value 不等于 DELETED
if (val != DELETED) {
if (i != o) {
// i != o
// 将索引 i 处的 key 赋值给 o 处的key
keys[o] = keys[i];
// 同时将值也赋值给 o 处
values[o] = val;
// 最后将 i 处的值置为空
values[i] = null;
}
// o 向后移动一位
o++;
}
}
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}

主要是理解 o 只有在值等于DELETED的时候才不会向后移,也就是说,当i向后移动一位的时候,o还在值为DELETED的地方,而这时候因为i != o,就会触发第二个判断条件,将i位置的元素向前移动到o处

SparseArray 的系列

除了前面分析的SparseArray,其实还有其它的一些类似的数据结构,它们总结起来就是用于存放基本数据类型的键值对:

SparseIntArray — int:int
SparseBooleanArray— int:boolean
SparseLongArray— int:long
总结

优势:
避免了基本数据类型的装箱操作
不需要额外的结构体,单个元素的存储成本更低
数据量小的情况下,随机访问的效率更高

缺点
插入操作需要复制数组,增删效率降低
数据量巨大时,复制数组成本巨大,gc()成本也巨大
数据量巨大时,查询效率也会明显下降

你可能感兴趣的:(Android源码解析)