SparseArray 笔记整理

源码基于 API 25

参考文章:

1、面试必备:SparseArray源码解析
2、谈谈源码中的SparseArray


1、概述

概括的说,SparseArray<E> 是用于在 Android 平台上替代 HashMap 的数据结构。
更具体的说,是用于替代 key 为 int 类型,value 为 Object 类型的 HashMap。

和 ArrayMap 类似,它的实现相比于 HashMap 更加节省空间,而且由于 key 指定为 int 类型,也可以节省 int-Integer 的装箱拆箱操作带来的性能消耗。

它仅仅实现了 implements Cloneable 接口,所以使用时不能用 Map 作为声明类型来使用。
它也是线程不安全的,允许 value 为 null。

从原理上说,
它的内部实现也是基于两个数组。
一个int[]数组mKeys,用于保存每个 item 的 key,key 本身就是 int 类型,所以可以理解 hashCode 值就是 key 的值;一个Object[]数组mValues,保存 value 。容量和key数组的一样。

它扩容的更合适,扩容时只需要数组拷贝工作,不需要重建哈希表。
同样它不适合大容量的数据存储。存储大量数据时,它的性能将退化至少50%。
比传统的HashMap时间效率低。
因为其会对 key 从小到大排序,使用二分法查询 key 对应在数组中的下标。

在添加、删除、查找数据的时候都是先使用二分查找法得到相应的 index,然后通过 index 来进行添加、查找、删除等操作。

所以其是按照 key 的大小排序存储的

另外,SparseArray 为了提升性能,在删除操作时做了一些优化:
当删除一个元素时,并不是立即从 value 数组中删除它,而是将其在 value 数组中标记为已删除。这样当存储相同的 key 的 value 时,可以直接覆盖这个空间。

如果该空间没有被重用,随后将在合适的时机里执行 gc(垃圾收集)操作,将数组压缩,以免浪费空间。

适用场景:
1、数据量不大(千以内)
2、空间比时间重要
3、需要使用Map,且key为int类型。

引用自:面试必备:SparseArray源码解析


2、成员及构造函数

private static final Object DELETED = new Object();
// 是否需要执行 gc() 的标记变量
private boolean mGarbage = false;

// key 数组
private int[] mKeys;
// value 数组
private Object[] mValues;
private int mSize;

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;
}

3、成员方法

3.1 增、改
public void put(int key, E value) {
    // 通过二分查找找出 key 在 mKeys 中对应的下标
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    // 如果 >=0,即 key 在 mKeys 中存在,且 i 即为对应下标,
    // 直接覆盖 mValues[i] 中的值
    if (i >= 0) {
        mValues[i] = value;
    } else {
        // 否则 ~i 即为 key-value 要插入的位置
        i = ~i;
        // 如果i没有越界,且对应位置是已删除的标记,则复用这个空间,
        // 复用后直接返回
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
        // 如果需要进行回收,且已经存储的元素值已经达到了 mKeys 的长度,
        // 则需要通过 gc() 方法清除被标记为 DELETED 的对应位置的内容
        if (mGarbage && mSize >= mKeys.length) {
            // gc() 方法会把标记为 DELETED 的 key-value 给清除,此时
            // mKeys、mVulues 两个数组都会的有效值都会往前靠缩紧(即压缩)
            gc();
            // 因此 gc() 之后需要重新获取正确的
            // Search again because indices may have changed.
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        // 将 key-value 插入到对应的位置,
        // 在插入的时候,可能会扩容
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}

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;
    mSiz

以及辅助方法:

// ContainerHelpers.java
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];
        if (midVal < value) {
            lo = mid + 1;
        } else if (midVal > value) {
            hi = mid - 1;
        } else {
            // 直接返回了 key 所在的位置,mid 是属于 [0, size - 1] 的
            return mid;  // value found
        }
    }
    
    // 基于二分查找,array 是排序好的,如果在 array 中没有 value 值,则会走到这里
    // 当 value 大于 array[size - 1] 时,此时 lo == size,则 ~lo == -(size+1)
    // 当 value 小于 array[0] 时,此时 lo == 0,则 ~lo == -1
    // 无论如何 ~lo 都会为负,因为 ~lo 为负与 value 不存在 array 中是等价的,
    // 且 lo 表示 value 应该插入的位置
    return ~lo;  // value not present
}
// GrowingArrayUtils.java
public static int[] insert(int[] array, int currentSize, int index, int element) {
    assert currentSize <= array.length;
    // currentSize + 1 <= array.length 表示还可以插入至少一个元素,因此不需要扩容
    if (currentSize + 1 <= array.length) {
        // System.arraycopy() 会根据 srcPos、dstPos 来判断是从前往后复制,还是从后往前复制
        // 避免操作同一个数组时里面的值在操作的时候被覆盖
        System.arraycopy(array, index, array, index + 1, currentSize - index);
        array[index] = element;
        return array;
    }
    
    // 原数组已经满了,无法插入新元素了,因此需要扩容
    // 扩容规则是如果当前容量大小 <= 4,则直接扩容为 8,否则变为原来的 2 倍
    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.2 删
public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
    // 在删除的时候,只会把对应位置的值标记为 DELETE,并不会更改 mSize,且不会马上触发 gc(),
    // 这样的话可以针对某些情况提高效率,
    // 比如先 delete(X) 然后 put(X, value)
    if (i >= 0) {
        if (mValues[i] != DELETED) {
            mValues[i] = DELETED;
            mGarbage = true;
        }
    }
}

public void remove(int key) {
    delete(key);
}

// 根据索引 remove
public void removeAt(int index) {
	// 只是把对应索引位置的元素标记为 DELETED
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}

// 删除从 index 开始的 size 个元素
public void removeAtRange(int index, int size) {
	// 修正结束的位置
    final int end = Math.min(mSize, index + size);
    for (int i = index; i < end; i++) {
        removeAt(i);
    }
}

需要注意的是,有关删除的方法,都没有更改 mSize,但是在调用 size() 的时候(如 mGarbage 为 true),一定会触发 gc(),从而得到正确的 mSize。

3.3 查
public E get(int key) {
    return get(key, null);
}

public E get(int key, E valueIfKeyNotFound) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    if (i < 0 || mValues[i] == DELETED) {
        return valueIfKeyNotFound;
    } else {
        return (E) mValues[i];
    }
}

// 查找 key 对应的索引
// 因此在必要的时候要先 gc,避免影响准确性
public int indexOfKey(int key) {
    if (mGarbage) {
        gc();
    }
    return ContainerHelpers.binarySearch(mKeys, mSize, key);
}

public int indexOfValue(E value) {
    if (mGarbage) {
        gc();
    }
    // 不像 key 一样使用的二分查找。是直接线性遍历去比较,
    // 而且不像其他集合类使用 equals 比较,这里直接使用的 ==
    // 如果有多个 key 对应同一个 value,则这里只会返回一个更靠前的 index
    for (int i = 0; i < mSize; i++)
        if (mValues[i] == value)
            return i;
    return -1;
}

// 根据索引获取对应的 key
// 因此在必要的时候要先 gc,避免影响准确性
public int keyAt(int index) {
    if (mGarbage) {
        gc();
    }
    return mKeys[index];
}
3.4 size() 方法
public int size() {
	// 必要时,先触发 gc,修正 mSize
    if (mGarbage) {
        gc();
    }
    return mSize;
}

4、补充内容

在这里插入图片描述
截图自:面试必备:SparseArray源码解析

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值