SparseArray详解及源码简析

一、前言

SparseArray 是 Android 在 Android SdK 为我们提供的一个基础的数据结构,其功能类似于 HashMap。与 HashMap 不同的是它的 Key 只能是 int 值,不能是其他的类型。

二、代码分析

1. demo 及其简析

首先也还是先通过 demo 来看一看 SparseArray 的基本使用方法,主要就是插入方法以及遍历方法。这也会后面的代码分析打下一个基础。

        SparseArray<Object> sparseArray = new SparseArray<>();
        sparseArray.put(0,null);
        sparseArray.put(1,"fsdfd");
        sparseArray.put(2,new String("fjdslfjdk"));
        sparseArray.put(3,1);
        sparseArray.put(4,new Boolean(true));
        sparseArray.put(5,new Object());
        sparseArray.put(8,new String("42fsjfldk"));
        sparseArray.put(20,"jfslfjdkfj");
        sparseArray.put(0,"chongfude");

        int size = sparseArray.size();
        for (int i = 0;i < size;i++) {
            Log.d(TAG, "sparseArraySample: i = " + i + ";value = " + sparseArray.get(sparseArray.keyAt(i)) );
        }

上面代码先是 new 了一个 SparseArray,注意声明时只能指定 value 的类型,而 key 是固定为 int 的。然后再往里面添加 key 以及 value。这里注意一下的是 key 为 0 的情况插入了 2 次。遍历时,是先通过顺序的下标取出 key ,再通过 keyAt 来 get 出 value。当然也可以一步到位通过 valueAt() 直接获取到 value。然后这个 demo 的执行结果如下。

sparseArraySample: i = 0;value = chongfude
sparseArraySample: i = 1;value = fsdfd
sparseArraySample: i = 2;value = fjdslfjdk
sparseArraySample: i = 3;value = 1
sparseArraySample: i = 4;value = true
sparseArraySample: i = 5;value = java.lang.Object@b67a0fa
sparseArraySample: i = 6;value = 42fsjfldk
sparseArraySample: i = 7;value = jfslfjdkfj

然后通过 Debug 来看一看在内存中,SparseArray 实际是如何存储的。如下图分别是 key 与 value 在内存中的形式。可以看出 keys 和 values 的大小都为 13,而且 keys 的值是按从小到大顺序排列的。

keys
values

2.源码分析

下面是 SparseArray 的类图结构,可以看到其属性非常的少,也可以看出其分别用了数组 int[] 和 object[] 来存储 key 以及 value。


SparseArray.jpg
  • SparseArray 初始化
    SparseArray 的初始化也就是它的构造方法。
    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 个构造方法,带参与不带参。当然,这个参数就是指定数组初始大小,也就是 SparseArray 的初始容量。而不带参数则默认指定数组大小为 10 个。

  • 插入数据 put() 方法
public void put(int key, E value) {
        // 1.先进行二分查找
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 2. 如果找到了,则 i 必大于等于 0
        if (i >= 0) {
            mValues[i] = value;
        } else {
        // 3. 没找到,则找一个正确的位置再插入
            i = ~i;
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            if (mGarbage && mSize >= mKeys.length) {
                gc();
                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }

这里调用了很多外部的方法以及内部的方法。首先是 ContainerHelpers#binarySearch() 的二分查找算法。

    //This is Arrays.binarySearch(), but doesn't do any argument validation.
    static int binarySearch(int[] array, int size, int value) {
        int lo = 0;
        int hi = size - 1;
        while (lo <= hi) {
            // 高位+低位之各除以 2,写成右移,即通过位运算替代除法以提高运算效率
            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;  // value found
            }
        }
        //若没找到,则lo是value应该插入的位置,是一个正数。对这个正数去反,返回负数回去
        return ~lo;  // value not present
    }

二分查找的分析属于基础内容,在注释中了。回到 put() 方法首先通过二分查找算法从当前 keys 中查找是否已经存在相同的 key 了,如果存在则会返回大于等于 0 的下标,然后接下来就会将原下标下的 values 中的旧value 替换成新的 value 值,即发生了覆盖。

那如果没有找到,那么将 i 取反就是要插入的位置了,这一结论正好来自 binarySearch() 的返回结果。可以看到其最后如果没有找到的话,就会返回 lo 的取反数。那么这里再把它取反过来那就是 lo 了。

这里如果 i 是在大小 mSizes 的范围内的,且其对应的 values[i] 又刚是被标记为删除的对象,那么就可以复用这个对象,否则就还是要依当前的 i 值进一步寻找要插入的位置,再插入相应的 value。

在插入之前,如果由于之前进行过 delete(),remoeAt() 以及 removeReturnOld() 中的某一个方法,那就可能要进行 gc() 操作。当然,这里不是指的内存的 gc()。

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;
        mSize = o;
    }

通过代码很容易分析得出,这里的 gc ,实际就是压缩存储,简单点说就是让元素挨得紧一点。

而 gc() 完之后,下标 i 可能会发生变化,因此需要重新查找一次,以得到一个新的下标 i。

最后就是通过 GrowingArrayUtils.insert() 来进行 key 和 value 的插入。这个 insert() 根据数组类型重载了多个,这里只分析 int[] 类型的即可。

public static int[] insert(int[] array, int currentSize, int index, int element) {
        //确认 当前集合长度 小于等于 array数组长度
        assert currentSize <= array.length;
        //不需要扩容
        if (currentSize + 1 <= array.length) {
            //将array数组内从 index 移到 index + 1,共移了 currentSize - index 个,即从index开始后移一位,那么就留出 index 的位置来插入新的值。
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            //在index处插入新的值
            array[index] = element;
            return array;
        }
        //需要扩容,构建新的数组,新的数组大小由growSize() 计算得到
        int[] newArray = new int[growSize(currentSize)];
        //这里再分 3 段赋值。首先将原数组中 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;
    }

上面的算法中,如果不需要扩容则直接进行移位以留出空位来插入新的值,如果需要扩容则先扩容,然后根据要插入的位置 index,分三段数据复制到新的数组中。这里再看看 growSize() 是如何进行扩容 size 的计算的。

    public static int growSize(int currentSize) {
        //如果当前size 小于等于4,则返回8, 否则返回当前size的两倍
        return currentSize <= 4 ? 8 : currentSize * 2;
    }

代码相对简单,当前 size 小于等于 4 则为 8 ,否则为 2 倍大小。

  • get() 方法
    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];
        }
    }

get() 方法就是通过 key 来返回对应的 value,前面在分析 put() 的时候已经分析过了二分查找。那么这里如果找到了,就会通过下标直接从 mValues[] 中返回。

  • delete() 方法
public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

delete() 也非常简单,通过二分查找算法定位到下标,然后将对应的 value 标记为 DELETE,并且标记需要进行 gc 了。这里需要注意的是被标记为 DELETE 的 value 不会在 gc 中被移除掉,而只会被覆盖掉,从而提高了插入的效率。

三、总结

文章对 SparseArray 进行了简要的分析,文章也只对主要的几个方法进行了分析,其他没有分析到的方法在这个基础上再进行分析相信也是很简单的。而总结下来几点是:

  • 其内部主要通过 2 个数组来存储 key 和 value,分别是 int[] 和 Object[]。这也限定了其 key 只能为 int 类型,且 key 不能重复,否则会发生覆盖。
  • 一切操作都是基于二分查找算法,将 key 以升序的方法 “紧凑” 的排列在一起,从而提高内存的利用率以及访问的效率。相比较 HashMap 而言,这是典型的时间换空间的策略。
  • 删除操作并不是真的删除,而只是标记为 DELETE,以便下次能够直接复用。

最后,感谢你能读到并读完此文章。受限于作者水平有限,如果存在错误或者疑问都欢迎留言讨论。如果我的分享能够帮助到你,也请记得帮忙点个赞吧,鼓励我继续写下去,谢谢。

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

推荐阅读更多精彩内容

  • 本系列出于AWeiLoveAndroid的分享,在此感谢,再结合自身经验查漏补缺,完善答案。以成系统。 Java基...
    济公大将阅读 1,528评论 1 6
  • 引言 SparseArray是在API level 1就已经添加的适用于Android的集合类,而ArrayMap...
    horseLai阅读 709评论 0 0
  • 分支结构。 一种条件,一种结果:条件||结果, 两种条件,两种结果:当结构复杂时,使用if。。。else语句:if...
    5e34228c9e5d阅读 146评论 0 0
  • 兹夜, 阑珊光影的一角, 小贩于摊位吆喝属于自己张罗。 锅里的蒸汽翻滚着空白, 昏黄的广场上。 喧嚣里仿佛嘶声裂肺...
    挚城阅读 162评论 0 2