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 来实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,213评论 0 16
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 内存优化前我们先了解一些和内存相关的概念: 垃圾回收 内存抖动 四种引用 内存泄露 下面我们回到正题, 讲一下如何...
    MZzF2HC阅读 1,621评论 0 6
  • 也有日子没动手撕烤人参了,前天听一哥叹道每一两天就要抓一个节点向前推事儿,今天突然悟到:一哥别无选择,惟有把个人乃...
    冰小寒阅读 503评论 0 3
  • 001 增加自由支配的时间。 很多时候我们觉得时间不够用,还有许多事情没办。如果每天早起哪怕一个小时,一年就多留出...
    我心已许阅读 194评论 1 1