SparseArray 源码解析

1、简述
  • 我们都知道 HashMap,它属于 java.util 包下,但是很多人可能对 SparseArray 并不是很熟悉,通俗来说 SparseArray 属于 android.util 包下,是用于 Android 平台某些情况替换 HashMap 的数据结构。
  • 更具体的来说,是用于替代 key 为 int 类型,value 为 Object 类型的 HashMap。和 ArrayMap 类似,它的实现相比于 HashMap 更加节省空间,而且由于 key 指定为 int 类型,也可以节省 int 到 Integer 的装箱拆箱操作带来的性能消耗。
2、归纳
  • 实现了 Cloneable 接口,并重写了 clone() 方法,所以支持复制(拷贝)。
  • 底层基于两个一维数组来实现的,其 key 为 int 类型,且 key 是按照从小到大升序存储的;value 为 Object 类型,且允许 value 为 null。
  • 扩容策略,初始化容量为10,如果当前容量小于等于4,则返回8,负责返回当前容量的2倍。
  • SparseArray 的扩容方式是直接创建一个新的数组,并将数据拷贝到新数组中。
  • 增、删和查操作都用到了二分查找【时间复杂度:O(log n)】,改操作【时间复杂度:O(1)】。
  • SparseArray 的操作单线安全,多线程不安全。
3、分析
3.1、接口
  • 在分析 SparseArray 源码之前,我们先来看看其实现的唯一接口 Cloneable,Cloneable 其实就是一个标记接口,实现这个接口后,然后重写 Object#clone() 方法,可以让其支持复制(拷贝)功能。
public interface Cloneable { }

/**
 * 重写 clone 方法
 */
@Override
@SuppressWarnings("unchecked")
public SparseArray<E> clone() {
    SparseArray<E> clone = null;
    try {
        clone = (SparseArray<E>) super.clone();
        clone.mKeys = mKeys.clone();
        clone.mValues = mValues.clone();
    } catch (CloneNotSupportedException cnse) {
        /* ignore(忽视) */
    }
    return clone;
}
3.2、成员变量
  • 在 SparseArray 的源码中,其成员变量并不多,所以在此把它们都一一列出。
public class SparseArray<E> implements Cloneable {
    
    ...
    // 用于标记 mValues 数组中的 value 值,是否作为已经删除的标记
    private static final Object DELETED = new Object();
    // 是否需要GC 
    private boolean mGarbage = false;
    
    // 存储 key 的数组
    @UnsupportedAppUsage(maxTargetSdk = 28)
    private int[] mKeys;
    // 存储 value 的数组
    @UnsupportedAppUsage(maxTargetSdk = 28) 
    private Object[] mValues;
    // 集合大小
    @UnsupportedAppUsage(maxTargetSdk = 28)
    private int mSize;
    ...
    
}
3.3、构造函数
  • 构造函数是一个类最常见的,同样也是最常被使用到的,接着我们分析 SparseArray 的两个不同的构造函数。
public class SparseArray<E> implements Cloneable {
    
    ...
    /**
     * 无参构造函数,初始化容量为10
     */
    public SparseArray() {
        this(10);
    }

    /**
     * 有参构造函数,指定初始化容量
     */
    public SparseArray(int initialCapacity) {
        // 如果初始容量为0的话,就赋值两个轻量级的引用
        if (initialCapacity == 0) {
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            // 初始化对应长度的数组
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        // 集合大小为0
        mSize = 0;
    }
    ...
    
}
3.4、增
  • SparseArray 的增操作为 put(int key, E value),下面我们来分析这种实现。
public class SparseArray<E> implements Cloneable {
    
    ...
    
    public void put(int key, E value) {
        // 利用二分查找,找到 待插入 key 的 下标 index
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 如果返回的 index>=0,说明之前这个 key存在,直接覆盖 value 即可
        if (i >= 0) {
            mValues[i] = value;
        } else {// 若返回的 index<0,说明 key 不存在
            // 先对返回的 i 取反,得到应该插入的位置 i
            i = ~i;
            // 如果 i 没有越界,且对应位置是已删除的标记,则复用这个空间
            if (i < mSize && mValues[i] == DELETED) {
                // 赋值后,返回
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            // 如果需要GC,且需要扩容
            if (mGarbage && mSize >= mKeys.length) {
                // 先触发GC
                gc();
                // gc 后,下标 i 可能发生变化,所以再次用二分查找找到应该插入的位置 i
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            // 插入key(可能需要扩容)
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            // 插入value(可能需要扩容)
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            // 集合大小递增
            mSize++;
        }
    }
    
    /**
     * 垃圾回收函数,压缩数组
     */
    private void gc() {
        // Log.e("SparseArray", "gc start with " + mSize);
        // 保存GC前的集合大小
        int n = mSize;
        // 既是下标 index,又是 GC 后的集合大小
        int o = 0;
        int[] keys = mKeys;
        Object[] values = mValues;
        //遍历 values 集合,以下算法意义为从values数组中,删除所有值为 DELETED 的元素
        for (int i = 0; i < n; i++) {
            Object val = values[i];
            // 如果当前 value 没有被标记为已删除
            if (val != DELETED) {
                // 压缩keys、values数组
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    // 并将当前元素置空,防止内存泄漏
                    values[i] = null;
                }
                // 递增 o
                o++;    
            }
        }
        // 修改标识,不需要 GC
        mGarbage = false;
        // 更新集合大小
        mSize = o;

        // Log.e("SparseArray", "gc end with " + mSize);
    }
    ...
    
}
  • ContainerHelpers.binarySearch() 方法:
class ContainerHelpers {

     /**
      * 二分查找算法
      *
      * @param array int[] 待查找的数组
      * @param size int 数组长度
      * @param value int 查找的值
      * @return int 返回查找到后的索引下标,如果返回值<0,则说明不存在
      */
    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 {
                // 成功后返回找到后的数组下标索引下标
                return mid;
            }
        }
        // 若没找到,则 lo 是 value 应该插入的位置,是一个正数。对这个正数去反,返回负数回去
        return ~lo;
    }
    
    ...
}
  • GrowingArrayUtils.insert() 方法:
public final class GrowingArrayUtils {

    ...
    
    /**
     * @param array       待插入的数组
     * @param currentSize 待插入数组当前长度
     * @param index       插入的索引下标
     * @param element     插入的元素
     * @return 返回插入后的数组
     */
    public static int[] insert(int[] array, int currentSize, int index, int element) {
        // 断言 确认当前集合长度小于等于 array 数组长度
        assert currentSize <= array.length;
        // 如果不需要扩容
        if (currentSize + 1 <= array.length) {
            // 将 array 数组内元素,从 index 开始后移一位
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            // 在 index 处赋值
            array[index] = element;
            // 返回
            return array;
        }
        // 需要扩容,则构建新的数组
        int[] newArray = new int[growSize(currentSize)];
        // 将原数组中 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;
    }

    /**
     * 根据现在的 size 长度,返回合适的扩容后的容量
     *
     * @param currentSize 当前数组长度
     * @return 返回扩容后的数组长度
     */
    public static int growSize(int currentSize) {
        // 如果当前 size<=4,则返回8,否则返回当前 size 的两倍
        return currentSize <= 4 ? 8 : currentSize * 2;
    }
    ...

}
  • 由上述源码解析可知:
  • 二分查找:若未找到返回下标时,对待插入的位置下标取反并返回,这样可以根据返回的正负可以判断是否找到的 index。对负的 index 取反,即可得到应该插入的位置下标。
  • 扩容:如果当前容量小于等于4,则返回8,负责返回当前容量的2倍。注意,其扩容操作依然是用数组的复制、覆盖完成(System.arraycopy())。
3.5、删
public class SparseArray<E> implements Cloneable {
    
    ...
    
    /**
     * 根据 key 进程删操作
     *
     * @param key 键
     */
    public void remove(int key) {
        delete(key);
    }

    /**
     * 根据 key 进行删操作
     */
    public void delete(int key) {
        // 二分查找得到要删除的 key 所在 index
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 如果 i>=0,表示存在
        if (i >= 0) {
            // 修改 values 数组对应位置为已删除的标志 DELETED
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                // 并修改 mGarbage,表示稍后需要 GC
                mGarbage = true;
            }
        }
    }

    /**
     * 根据 index 下标进行删操作
     *
     * @param index 数组对应的下标
     */
    public void removeAt(int index) {
        // 根据 index 直接索引到对应位置执行删除操作
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;
            // 并修改 mGarbage,表示稍后需要 GC
            mGarbage = true;
        }
    }

    /**
     * 批量删操作
     *
     * @param index 开始的下标
     * @param size 待删除的长度
     */
    public void removeAtRange(int index, int size) {
        // 越界修正
        final int end = Math.min(mSize, index + size);
        // for循环,执行单个删除操作
        for (int i = index; i < end; i++) {
            removeAt(i);
        }
    }
    ...   
}
3.6、改
public class SparseArray<E> implements Cloneable {
    
    ...

    /**
     * 根据下标重新赋值
     *
     * @param index 数组下标
     * @param value 赋值元素
     */
    public void setValueAt(int index, E value) {
        // 判断下标是否越界
        if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
            // 抛出异常
            throw new ArrayIndexOutOfBoundsException(index);
        }
        // 判断是否需要 gc() 操作
        if (mGarbage) {
            gc();
        }
        // 将新的赋值元素覆盖在老的元素上
        mValues[index] = value;
    }
    ...

}
3.7、查
public class SparseArray<E> implements Cloneable {

    ...

    /**
     * 根据 key 去查操作
     *
     * @param key 键
     * @return 返回查找后的元素,如果没有则返回 null
     */
    public E get(int key) {
        return get(key, null);
    }

    /**
     * 根据 key 去查操作,如果没有当前 key,则返回您输入的默认值 valueIfKeyNotFound
     *
     * @param key                键
     * @param valueIfKeyNotFound 默认返回值
     * @return 返回查找后的元素,如果没有则返回默认值
     */
    @SuppressWarnings("unchecked")
    public E get(int key, E valueIfKeyNotFound) {
        // 根据 key 进行二分查找,找到其对应的数组下标
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 下标小于0或者其标识为 DELETED,则返回默认值
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {// 返回下标对应的数组元素值
            return (E) mValues[i];
        }
    }

    /**
     * 根据下标查找 key
     *
     * @param index 数组下标
     * @return 返回 key
     */
    public int keyAt(int index) {
        // 判断是否越界
        if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
            // 抛出异常
            throw new ArrayIndexOutOfBoundsException(index);
        }
        // 判断是否需要 gc() 操作
        if (mGarbage) {
            gc();
        }
        // 返回 key 值
        return mKeys[index];
    }

    /**
     * 根据下标查找 value
     *
     * @param index 数组下标
     * @return 返回 value
     */
    public E valueAt(int index) {
        // 判断是否需要 gc() 操作
        if (mGarbage) {
            gc();
        }
        // 返回 value 值
        return (E) mValues[index];
    }

    /**
     * 根据 key 查找数组下标
     *
     * @param key 键
     * @return 返回数组下标
     */
    public int indexOfKey(int key) {
        // 判断是否需要 gc() 操作
        if (mGarbage) {
            gc();
        }
        // 二分查找返回对应的下标
        return ContainerHelpers.binarySearch(mKeys, mSize, key);
    }

    /**
     * 根据 value 查找数组下标
     *
     * @param value 元素值
     * @return 返回数组下标
     */
    public int indexOfValue(E value) {
        // 判断是否需要 gc() 操作
        if (mGarbage) {
            gc();
        }
        // 不像 key 一样使用的二分查找。是直接线性遍历去比较,而且不像其他集合类使用 equals 比较,这里直接使用的 ==
        // 如果有多个 key 对应同一个value,则这里只会返回一个更靠前的 index
        for (int i = 0; i < mSize; i++)
            if (mValues[i] == value)
                return i;

        return -1;
    }
    ...

}
4、总结
  • 总体来说 SparseAraay 的源码相对比较简单,相对于 HashMap 而言就是“时间换空间”的一个概念,所以1000以内元素个数更推荐使用 SparseArray 集合。
  • android.util 包下,还提供了三个类似思想的集合:
  • (1)SparseIntArray,value 为 int 类型;
  • (2)SparseLongArray,value 为 long 类型;
  • (3)SparseBooleanArray,value 为 boolean 类型;
  • 这三者与 SparseArray 的唯一区别在于 value 的类型,SparseArray 的 value 可以为任意类型,而上述三者是拆箱后的基本类型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值