Android为什么推荐使用SparseArray来替代HashMap?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/woshizisezise/article/details/79361458

SparseArray也许你没听过,那正好今天就来学习一下咯,这也是Android官方推荐使用的,所以我们需要了解一下他的优势和劣势在哪些地方。

首先SparseArray用来和HashMap做比较,在安卓项目中,你新建一个HashMap对象,注意下面会有下划线,里面有提示
内容
翻译成白话文的意思就是建议使用SparseArray替代HashMap来获得更好的表现。我们都知道HashMap在java中很常用,并且也很吃香,面试官经常会问到HashMap源码实现,既然HashMap的表现都这么牛逼了,那么为什么Android官方会推荐使用SparseArray呢?不要急,慢慢来~

首先来了解一下SparseArray,翻译过来称为“稀疏数组”,所谓稀疏数组就是数组中大部分的内容值都未被使用(或都为零),在数组中仅有少部分的空间使用。因此造成内存空间的浪费,为了节省内存空间,并且不影响数组中原有的内容值,我们可以采用一种压缩的方式来表示稀疏数组的内容。

下面有网上举例子的两张图,我挪过来了,如果涉及侵权问题,请联系我删除哈,毕竟自己照猫画虎重新画一个,浪费时间,没有必要:
这里写图片描述
可以看出来这是一个9*7的数组,它所占用的空间为63个,但是如图只有5个空间使用了,其他的都是浪费,浪费是可耻的,所以按照稀疏数组的整理,就变成了下面这张图所示
这里写图片描述
大家应该都能看懂吧,二维数组对应的值,那么这么一来所占空间仅仅为3*6=18个了,相比起来是否节省很多,所以这就是为什么Android官方推荐使用SparseArray的原因了,我们稍后来验证。

下面我们来写一段程序来看看SparseArray和HashMap在处理数据上面的性能情况。

  • HashMap正向插入数据
HashMap<Integer, String> map = new HashMap<Integer, String>();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
    map.put(i, String.valueOf(i));
}
long memory = Runtime.getRuntime().totalMemory();
long end = System.currentTimeMillis();
long usedTime = end - start;
System.out.println("HashMap消耗的时间是:" + usedTime + ",HashMap占用的内存是:" + memory);

输出结果:
这里写图片描述

  • SparseArray正向插入数据
SparseArray<String> sparse = new SparseArray<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
    sparse.put(i, String.valueOf(i));
}
long memory = Runtime.getRuntime().totalMemory();
long end = System.currentTimeMillis();
long usedTime = end - start;
System.out.println("SparseArray消耗的时间是:" + usedTime + ",SparseArray占用的内存是:" + memory);

输出结果:
这里写图片描述

公平起见,这两段代码我都是分别运行在Android程序中的,一次说明不了什么结果,所以我重复启动了几次项目,这个时候再做对比就很明显了,这四次结果SpaseArray的内存占用都是明显低于HashMap的内存占用的,消耗的时间好像也是这样的,但是Android官方并没有提到效率这个问题,我们之前分析SpaseArray的内存结果也没有发现这个现象,那么这是为什么呢?不要急,下面我们反向来插入一次数据再做一次比较。

  • HashMap反向插入数据
HashMap<Integer, String> map = new HashMap<Integer, String>();
long start = System.currentTimeMillis();
for (int i=100000-1;i>=0;i--) {
    map.put(i, String.valueOf(100000-i-1));
}
long memory = Runtime.getRuntime().totalMemory();
long end = System.currentTimeMillis();
long usedTime = end - start;
System.out.println("HashMap消耗的时间是:" + usedTime + ",HashMap占用的内存是:" + memory);

输出结果:
这里写图片描述

  • SparseArray反向插入数据
SparseArray<String> sparse = new SparseArray<>();
long start = System.currentTimeMillis();
for (int i=100000-1;i>=0;i--) {
    sparse.put(i, String.valueOf(100000-i-1));
}
long memory = Runtime.getRuntime().totalMemory();
long end = System.currentTimeMillis();
long usedTime = end - start;
System.out.println("SparseArray消耗的时间是:" + usedTime + ",SparseArray占用的内存是:" + memory);

输出结果:
这里写图片描述

运行了三次以后我都不好意思再运行了,可以看到倒入的情况下,消耗时间表现非常糟糕,并且页面这个时候处于卡顿状态,但是仍然有一点,那就是SparseArray占用的内存依然比HashMap优秀的多,由于SparseArray每次在插入的时候都要使用二分查找判断是否有相同的值被插入.因此这种倒序的情况是SparseArray效率最差的时候。


源码分析

打开SparseArray源码,我们先看看左边的结构视图有哪些方法:
这里写图片描述

  • 看看开头定义的变量
private static final Object DELETED = new Object();
private boolean mGarbage = false;

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

很直接的可以看到有两个数组,并且他们的命名都那么的直接,一个存放key,一个存放value,还有一个大小。

  • 看看构造函数
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;
}

默认的构造函数也会调用重载方法并给初始容量设为10。

  • 看看插入方法put
/**
 * Adds a mapping from the specified key to the specified value,
 * replacing the previous mapping from the specified key if there
 * was one.
 */
 //注释已经说明了如果key不存在,则插入key和value,如果key存在,则替换原来的value
 //所以这个方法也可以当作修改
public void put(int key, E value) {
    //通过二分法查找key
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        //如果i>0,说明之前已经存在了这个key,所以直接覆盖原来的value
        mValues[i] = value;
    } else {
        //如果i<0,说明key不存在,所以i肯定是负数,所以我们要取相反数
        i = ~i;

        //如果i在mSize范围之内并且之前的值被删除了,那么直接进行赋值即可
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        //如果大小超过了keys的长度,说明存在无效的数据,这个时候需要回收了
        if (mGarbage && mSize >= mKeys.length) {
            gc();

            // Search again because indices may have changed.
            //回收完后引用可能改变了,所以需要对key重新二分法查找并取反
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }

        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}
  • 看看插入方法append
/**
 * Puts a key/value pair into the array, optimizing for the case where
 * the key is greater than all existing keys in the array.
 */
public void append(int key, E value) {
    if (mSize != 0 && key <= mKeys[mSize - 1]) {
        put(key, value);
        return;
    }

    if (mGarbage && mSize >= mKeys.length) {
        gc();
    }

    mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
    mValues = GrowingArrayUtils.append(mValues, mSize, value);
    mSize++;
}

可以看出来,append方法其实很简单,它也是调用了put方法完成键值对的插入,并且调用回收的逻辑也是一样的。

  • 看看删除的4种方法

1.delete(int key)

/**
 * Removes the mapping from the specified key, if there was any.
 */
public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

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

如果通过二分法能找到key,就将对应的索引对应的value设置为DELETED常量,并且标识mGarbage = true。

2.remove(int key)

public void remove(int key) {
   delete(key);
}

直接调用delete方法

3.removeAt(int index)

public void removeAt(int index) {
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}

原理和delete方法一样

4.clear()

public void clear() {
    int n = mSize;
    Object[] values = mValues;

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

    mSize = 0;
    mGarbage = false;
}

这个方法顾名思义,将数组内容全部清空置为null。

  • 看看修改的方法setValueAt(int index, E value)
public void setValueAt(int index, E value) {
    if (mGarbage) {
        gc();
    }

    mValues[index] = value;
}

如果标记为垃圾的话,先进行一次垃圾回收,然后再将指定的值设置到对应的索引上。

  • 看看获取方法get()、get(int key, E valueIfKeyNotFound)
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(int key, E valueIfKeyNotFound)方法,只不过第一个找不到key对应的value时返回的是null,第二个方法则可以由我们自己指定的默认值。

总结

大家一定要注意,我们插入的key并不是索引的角标,不要以为是int型就是角标了,那就错了,其实和map一样,都是通过key来查找value,我们传入key通过二分法生成一个index,这个index才是角标。

注意内存二字很重要,因为它仅仅提高内存效率,而不是提高执行效率,所以也决定它只适用于android系统(内存对android项目有多重要,地球人都知道)

最后引用别人总结的比较好的一段话来结束这篇文章:

SparseArray有两个优点:1.避免了自动装箱(auto-boxing),2.数据结构不会依赖于外部对象映射。我们知道HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置,存放的都是数组元素的引用,通过每个对象的hash值来映射对象。而SparseArray则是用数组数据结构来保存映射,然后通过折半查找来找到对象。但其实一般来说,SparseArray执行效率比HashMap要慢一点,因为查找需要折半查找,而添加删除则需要在数组中执行,而HashMap都是通过外部映射。但相对来说影响不大,最主要是SparseArray不需要开辟内存空间来额外存储外部映射,从而节省内存。

猜你喜欢

转载自blog.csdn.net/woshizisezise/article/details/79361458