Java&Android 基础知识梳理(10) - SparseArray 源码解析

一、基本概念

SparseArray的用法和keyint类型,valueObject类型的HashMap相同,和HashMap相比,先简要介绍一下它的两点优势。

内存占用

Java&Android 基础知识梳理(8) - 容器类 我们已经学习过HashMap的内部实现,它内部是采用数组的形式保存每个Entry,并采用链地址法来解决Hash冲突的问题。但是采用数组会遇到扩容的问题,默认情况下当数组内的元素达到loadFactor的时候,会将其扩大为目前大小的两倍,那么就有可能造成空间的浪费。

SparseArray虽然也是采用数组的方式来保存Key/Value

private int[] mKeys;
private Object[] mValues;

但是与HashMap使用普通数组不同,它对存放ValuemValues数组进行了优化,其创建方式为:

    public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            //默认情况下,创建的 initialCapacity 大小为 10。
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;
    }

其中ArrayUtils.newUnpaddedObjectArray(initialCapacity)用于创建优化后的数组,该方法实际上是一个Native方法,它解决了当数组中的元素没有填满时造成的空间浪费。

SparseArray 浅析 一文中介绍了SparseArray对于数组的优化方式,假设有一个9 x 7的数组,在一般情况下它的存储模型可以表示如下:

数组存储的一般模型

可以看到这种模型下的数组当中存在大量无用的0值,内存利用率很低。而优化后的方案用两个部分来表示数组:

  • 第一部分:存放的是数组的行数、列数、当前数组中有效元素的个数
  • 第二部分:存放的是所有有效元素的行、列数、元素的值

数组存储的优化模型

mKeys则是用普通数组实现的,通过查找Key值所在的位置,再根据mValues数组的属性找到对应元素的行、列值,从而得到对应的元素值。

避免自动装箱

对于HashMap来说,当我们采用put(1, Object)这样的形式来放入一个元素时,会进行自动装箱,即创建一个Integer对象放入到Entry当中。

SparseArray则不会存在这一问题,因为我们声明的就是int[]类型的mKeys数组。

二、源码解析

2.1 存放过程

    
    public void put(int key, E value) {
        //通过二分查找法进行查找插入元素所在位置。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //如果大于0,那么直接插入。
        if (i >= 0) { 
            mValues[i] = value;
        } else {
            //找到插入的位置。
            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);
            }
            //重新分配数组,并插入新的 Key,Value。
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }

2.2 读取过程

    public E get(int key, E valueIfKeyNotFound) {
        //通过二分查找,在 Key 数组中得到对应 Value 的下标。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //取出下标对应的元素。
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }

2.3 删除过程

    public void delete(int key) {
        //二分查找所在位置。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //将该位置的元素置为 DELETED,它是内部预先定义好的一个对象。
        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

可以看到,在删除元素的时候,它是用一个空的Object来标记该位置。在合适的时候(例如上面的put方法),才通过下面的gc()方法对mKeysmValues数组 重新排列

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

2.4 二分查找

    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;  // value found
            }
        }
        //如果没有找到,那么返回的是低位指针的取反坐标。
        return ~lo;  // value not present
    }

这里用的是~,由于lo>=0,所以当无法查找到对应元素的时候,返回值~lo一定<0。(~lo=-(lo+1)

这也是我们在2.1中看到,为什么在i>=0时就可以直接替换的原因,因为只要i>=0,就说明之前已经存在一个Key相同的元素了。

而在返回值小于0时,对它再一次取~,就刚好可以得到 要插入的位置

三、SparseArray 的效率问题

了解了SparseArray的原理之后,我们可以分析出有以下几方面有可能会影响SparseArray插入的效率:

  • 插入的效率。插入的效率其实主要跟Key值插入的先后顺序有关,假如Key值是按 递减顺序 插入的,那么每次我们都是在mValues[0]位置插入元素,这就要求把原来ValuesmKeys数组中[0, xxx]位置元素复制到[1, xxx+1]的位置,而如果是 递增插入 的则不会存在该问题,直接扩大数组数组的范围之后再插入即可。
  • 查找的效率。这点很明显,因为采用了二分查找,如果查找的Key值位于折半处,那么将会更快地找到对应的元素。

也就是说SparseArray在插入和查找上,相对于HashMap并不存在明显的优势,甚至在某些情况下,效率还要更差一些。

Google之所以推荐我们使用SparseArray来替换HashMap,是因为在移动端我们的数据集往往都是比较小的,而在这种情况下,这两者效率的差别几乎可以忽略。但是在内存利用率上,由于采用了优化的数组结构,并且避免了自动装箱,SparseArray明显更高,因此更推荐我们使用SparseArray

四、SparseArray 的衍生

SparseArray还有几个衍生的类,它们的基本思想都是一样的,即:

  • 用两个数组分别存储keyvalue,通过下标管理映射关系。
  • 采用二分查找法查找现在mKeys数组中对应找到所在元素的下标,再去mValues数组中取出元素。

我们在平时使用的时候,可以根据实际的应用场景选取相应的集合类型。

Key 类型不同

假如keylong型:

  • LongSparseArraykeylongvalueObject

Value 类型不同

假如keyint,而value为下面三种基本数据类型之一,那么可以采用以下三种集合来避免value的自动装箱来进一步优化。

  • SparseLongArraykeyintvaluelong
  • SparseBooleanArraykeyintvalueboolean
  • SparseIntArraykeyintvalueint

Key 和 Value 类型都不同

假如keyvalue都不为基本数据类型,那么可以采用:

  • ArrayMapkeyObjectvalueObject

更多文章,欢迎访问我的 Android 知识梳理系列:

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

推荐阅读更多精彩内容