Android源码分析-SparseArray<E>详解

本文深入解析了SparseArray的实现机制,包括其初始化过程、put方法的插入逻辑、remove方法及gc()方法的工作原理。SparseArray相比HashMap,在内存占用和性能方面具有显著优势,尤其是在数据量较小、需要频繁随机访问的场景下。
SparseArray 实现原理

在使用HashMap的时候IDE会给出如下提示:

这里写图片描述

SparseArray 类注释如下:

 SparseArrays map integers to Objects.  Unlike a normal array of Objects,
 there can be gaps in the indices.  It is intended to be more memory efficient
 than using a HashMap to map Integers to Objects, both because it avoids
 auto-boxing keys and its data structure doesn't rely on an extra entry object
 for each mapping.

SparseArray来代替HashMap会有更好性能,使用int[]数组存放key,避免了HashMap中基本数据类型需要装箱的操作,不使用额外的结构体(Entry),单个元素的存储成本下降。

1、类的初始化

初始化SparseArray只是简单的创建了两个数组,并将mSize赋值为0;

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;
}
2、put 方法:
public void put(int key, E value) {
    //通过二分法去查找key,返回索引
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        //如果 i > 0 说明数组中已经有了key,直接覆盖原来的值
        mValues[i] = value;
    } else {
        //取反得到key的插入位置
        i = ~i;
        // 如果索引小于当前已经存放的长度,并且这个位置上的值为DELETED(即被标记为删除的值)
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        // 到这一步说明直接赋值失败,检查当前是否被标记待回收且当前存放的长度已经大于或等于了数组长度
        if (mGarbage && mSize >= mKeys.length) {
            gc();

            // 重新再获取一下索引,因为数组发生了变化
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        // 最终在 i 位置上插入键与值,并且size +1
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}

插入的逻辑可以分为四点:

冲突直接覆盖:如果key 值存在则直接覆盖。
插入索引上为DELETED,直接给key 与value 赋值
索引上有值,但是应该触发gc() 然后再赋值
满容且无法gc() 扩容然后再拷贝

put 方法中使用了二分查找的算法,当找不到这个值的时候return ~lo,实际上到这一步的时候,理论上lo==mid==hi。所以这个位置是最适合插入数据的地方。但是为了让能让调用者既知道没有查到值,又知道索引位置,做了一个取反操作,返回一个负数。这样调用处可以首先通过正负来判断命中,之后又可以通过取反获取索引位置。

数据的插入方式

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;
}
3、remove() 方法:
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;
        }
    }
}

但是可以看到key仍然保存在数组中,并没有马上删除,目的应该是为了保持索引结构,同时不会频繁压缩数组,保证索引查询不会错位,那么key什么时候被删除呢?当SparseArray的gc()被调用时。

4、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处。

优势:

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

缺点

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值