SparseArray 稀疏数组源码分析

SparseArray sparse 稀疏

介绍

SparseArray 用来实现 int 类型与 Object 类型的映射,跟普通的 Map 不同,普通 Map 中有更多的空索引,对比 HashMap 来说,稀疏数组实现了更高效的内存使用,因为稀疏数组避免了 int 类型 key 的自动装箱,且稀疏数组每个 value 都不需要使用 Entry 对象来包装。所以在 Android 开发中,我们可以使用 SparseArray 来实现更高效的实现 Map

SparseArray 实现了 Cloneable 接口,说明时支持克隆操作的,下面慢慢分析增删改查以及克隆等操作

一、成员变量

/**
 * 删除操作时替换对应位置 value 的默认值
 */
private static final Object DELETED = new Object();
/**
 * 是否需要回收
 */
private boolean mGarbage = false;
/**
 * 存储 key 的数组
 */
private int[] mKeys;
/**
 * 存储 value 的数组
 */
private Object[] mValues;
/**
 * 当前存储的键值对数量
 */
private int mSize;

SparseArray 中声明了一个 int 类型的数组和一个 Object 类型的数组

二、构造函数

/**
 * 创建一个空 map 初始容量为 10
 */
public SparseArray() { this(10); }

/**
 * 根据指定初始容量创建键值对为空的稀疏数组,并且不会申请额外内存;指定初始容量为 0 时会创建一个轻量级的不需要任何内存分配的稀疏数组
 * capacity 容量
 */
public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
        mKeys = EmptyArray.INT; // 长度为 0 的 int 类型数组
        mValues = EmptyArray.OBJECT; // 长度为 0 的 Object 类型数组
    } else {
        mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
        mKeys = new int[mValues.length];
    }
    mSize = 0;
}

SparseArray 有两个构造函数,默认时创建初始容量为 10 数组,另外一个时可以使用者指定出事容量的数量

三、添加/修改 操作

public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 使用二分法查找对应的 key 在数组中的下标

    if (i >= 0) { // 索引大于等于 0 说明原数组中有对应 key
        mValues[i] = value; // 则直接 Value 数组中的 value 值为最新的 value
    } else { // 索引小于 0 说明原数组中不存在对应的 key
        i = ~i; // 取反后得到当前 key 应该在的位置

        if (i < mSize && mValues[i] == DELETED) { // 如果数组长度够,并且当前位置已被回收则直接对该位置赋值
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        if (mGarbage && mSize >= mKeys.length) { // 回收状态为 true 并且内容长度大于等于 key 数组长度
            gc(); // 回收,整理数组

            // Search again because indices may have changed.
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); // 再次使用二分法查找位置
        }

        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); // 执行 key 插入到 key 数组对应位置
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); // 执行 value 插入到 value 数组对应位置
        mSize++; // 键值对数量加 1
    }
}

上面的 put 方法中用到了一个 ContainerHelpers 的 binarySearch 函数,我们先来看一下这个函数的操作,主要是使用二分法查找对应的位置

// ContainerHelpers 
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; // 带符号右移,也就是做除以 2,这里是找到中间位置索引的操作
            final int midVal = array[mid]; 

            // 下面是正常的二分法操作
            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;  // value found
            }
        }
        return ~lo;  // 当数组中不存在对应 value 的时候,这里是将如果数组中存在 value 时应该在的位置取反后返回
    }

接着我们看一下 gc() 方法的操作

// 
private void gc() {
    int n = mSize; // 键值对数量
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) { // 通过循环将 value 数组中的 DELETED 值移除,并且 DELETED 以后的键跟值都往前补
        Object val = values[i];

        if (val != DELETED) {
            if (i != o) { // 循环第一次执行时 i 和 o 都是 0 ,这种情况不需要处理
                keys[o] = keys[i];
                values[o] = val;
                values[i] = null; // 原位置置空
            }
            o++;
        }
    }

    mGarbage = false; // 回收状态置为 false
    mSize = o; // 将键值对的值更新为实际的键值对数量
}

/**
 * GrowingArrayUtils 中定义了 泛型/int/long/boolean 等类型数组在指定位置插入数据的方法
 */
public static int[] insert(int[] array, int currentSize, int index, int element) {
    assert currentSize <= array.length;

    if (currentSize + 1 <= array.length) { // 不需要扩容
        System.arraycopy(array, index, array, index + 1, currentSize - index); // 将对应位置后的内容右移
        array[index] = element;
        return array;
    }

    // 需要扩容,
    int[] newArray = new int[growSize(currentSize)];
    System.arraycopy(array, 0, newArray, 0, index); // 将对应位置前的内容插入
    newArray[index] = element; // 将对应位置内容插入
    System.arraycopy(array, index, newArray, index + 1, array.length - index); // 将对应位置后的内容插入
    return newArray;
}

/**
 * GrowingArrayUtils 中定义了 泛型/int/long/boolean 等类型数组在指定位置插入数据的方法,这个方法的作用为,在位置超出数组大小时,计算扩容后数组的新长度
 * 旧数组长度小于 4 则设置为 8,否则都是在当前长度基础上扩容一被
 */
public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
}

小结一下,插入操作的工作是,首先在原 key 数组中查找是否有对应的 key,如果找到则直接替换 value 数组中对应下标的值;如果 key 不存在之前的 key 数组,则需要根据是否回收状态进行无用数据回收,然后执行插入,插入过程中如果数组需要扩容还需要执行扩容操作。

由插入操作可以看出,keys 数组中的值为从小到大排列,是一个有序数组

上面分析了插入方法的主要逻辑,接下来继续看 查找/删除 等操作,如果明白了插入操作,下面的就都简单了

四、查找方法 get(int key)

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

/**
 * 根据 key 查找 value ,如果 key 不存在则返回指定的默认值
 */
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];
    }
}

可以看到 get() 方法比较简单,首先通过 二分法 找到当前 key 在 key 数组中的位置,如果位置不小于 0 且 value 数组中对应位置的值部位 DELETED,说明找到对应值,直接返回,否则就返回 null。get() 操作是有一个重载方法的,调用者可以传入一个默认值,在查不到对应 key 时则返回默认值。

五、删除方法 delete(int key)

/**
 * 删除操作
 */
public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        if (mValues[i] != DELETED) {
            mValues[i] = DELETED;
            mGarbage = true;
        }
    }
}

public E removeReturnOld(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        if (mValues[i] != DELETED) {
            final E old = (E) mValues[i];
            mValues[i] = DELETED;
            mGarbage = true;
            return old;
        }
    }
    return null;
}

删除操作就更简单了,首先通过二分法查找 key 所在位置,找到就将 value 中对应位置的值设置为 DELETED,在其他操作时通过 gc() 操作执行该位置的回收。removeReturnOld 方法则是会返回删除的 value 值。

同时,SparseArray 也提供了移除指定位置的键值对的方法

/**
 * 删除指定位置的值
 */
public void removeAt(int index) {
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}

/**
 * 以 index 开始,删除之后 size 个值,包含 index 位置,不包含 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);
    }
}

六、其他操作

克隆

SparseArray 重写了 clone 方法,科隆时其 keys,values 数组都会克隆成新的数组

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

size() 返回键值对的数量

首先执行 gc() 操作,然后返回正确的数量

public int size() {
    if (mGarbage) {
        gc();
    }

    return mSize;
}

keyAt() valueAt() setValueAt() indexOfKey() indexOfValue()

/**
 * 返回指定位置的 key
 */
public int keyAt(int index) {
    if (mGarbage) {
        gc();
    }
    return mKeys[index];
}

/**
 * 返回指定位置的 value
 */
public E valueAt(int index) {
    if (mGarbage) {
        gc();
    }

    return (E) mValues[index];
}

/**
 * 将对应位置的值设置为指定 value
 */
public void setValueAt(int index, E value) {
    if (mGarbage) {
        gc();
    }

    mValues[index] = value;
}

/**
 * 返回指定位置的 key
 */
public int indexOfKey(int key) {
    if (mGarbage) {
        gc();
    }

    return ContainerHelpers.binarySearch(mKeys, mSize, key);
}

/**
 * 返回指定位置的 value
 */
public int indexOfValue(E value) {
    if (mGarbage) {
        gc();
    }

    for (int i = 0; i < mSize; i++) {
        if (mValues[i] == value) {
            return i;
        }
    }

    return -1;
}

/**
 * 返回指定 value 所在位置,只不过 value 相等的判断使用 equals 方法
 */
public int indexOfValueByValue(E value) {
    if (mGarbage) {
        gc();
    }

    for (int i = 0; i < mSize; i++) {
        if (value == null) {
            if (mValues[i] == null) {
                return i;
            }
        } else {
            if (value.equals(mValues[i])) {
                return i;
            }
        }
    }
    return -1;
}

/**
 * 移除所有键值对
 */
public void clear() {
    int n = mSize;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
        values[i] = null;
    }

    mSize = 0;
    mGarbage = false;
}

/**
 * 插入键值对,优化插入的 key 大于所有现在已有 key 的情况,由于 key 数组是从大到小的有序数组,所以这种情况下不需要二分法查找位置,优化了性能
 */
public void append(int key, E value) {
    if (mSize != 0 && key <= mKeys[mSize - 1]) { // 如果不是大于现在已有的 key ,则按照正常方式插入
        put(key, value);
        return;
    }

    if (mGarbage && mSize >= mKeys.length) { // 执行回收 DELETED 的 value
        gc();
    }

    mKeys = GrowingArrayUtils.append(mKeys, mSize, key); // 直接向后插入
    mValues = GrowingArrayUtils.append(mValues, mSize, value); // 直接向后插入
    mSize++;
}

/**
 * 打印所有的 key value
 */
public String toString() {
    if (size() <= 0) {
        return "{}";
    }

    StringBuilder buffer = new StringBuilder(mSize * 28);
    buffer.append('{');
    for (int i=0; i<mSize; i++) {
        if (i > 0) {
            buffer.append(", ");
        }
        int key = keyAt(i);
        buffer.append(key);
        buffer.append('=');
        Object value = valueAt(i);
        if (value != this) {
            buffer.append(value);
        } else {
            buffer.append("(this Map)");
        }
    }
    buffer.append('}');
    return buffer.toString();
}

七、总结

SparseArray 的代码非常少,只有 450 行左右,并且特别易于理解。但 SparseArray 要比 HashMap 更加高效,在 Android 手机中,如果 key 为 int 类型的 Map 数据,最好使用 SparseArray 来实现。

推荐阅读更多精彩内容

  • pdf下载地址:Java面试宝典 第一章内容介绍 20 第二章JavaSE基础 21 一、Java面向对象 21 ...
    王震阳阅读 70,194评论 26 501
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 1,974评论 0 16
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 67,769评论 12 114
  • 内存优化前我们先了解一些和内存相关的概念: 垃圾回收 内存抖动 四种引用 内存泄露 下面我们回到正题, 讲一下如何...
    MZzF2HC阅读 184评论 0 3
  • 黑客,是生活在互联网里神秘而又让人羡慕的群体,谈何羡慕?我想大家都知道黑客的意思,不懂可以百度,黑客是完全的依靠技...
    geekape阅读 245评论 1 0