谈谈源码中的SparseArray

橙三吉。 提交于 2019-12-10 17:43:23

谈谈源码中的SparseArray

在Andorid的源码和第三方库中,偶尔能看到该类,我们先来看一下官方文档的说明如下:

SparseArray map integers to Objects. Unlike a normal array of Objects,there can be gaps in the indices. It is intended to be more memory efficient than using a HashMap to map Integers to Objects, both because it avoids auto-boxing keys and its data structure doesn't rely on an extra entry object for each mapping.

上面的意思是SparseArray 用来替代HashMap Int到Object的这种关系。 它设计的目的是为了比HashMap更加节省内存,这是因为:

  1. 它避免了键值的自动装箱
  2. 他的数据结构不需要依赖额外的对象来完成映射。

Note that this container keeps its mappings in an array data structure,using a binary search to find keys. The implementation is not intended to be appropriate for data structures that may contain large numbers of items. It is generally slower than a traditional HashMap, since lookups require a binary search and adds and removes require inserting and deleting entries in the array. For containers holding up to hundreds of items,the performance difference is not significant, less than 50%.

上面说了,SparseArray通过二分查找键值,这种实现方式不太适合太多的item。通常情况他是比HashMap慢的,但是如果容器只有数百的item,这个性能损失不太重要,不超过50%。

实际在Android环境中,我们的键值也很少有超过上千的,所以SparseArray我们能在项目中用到的地方还是不少,如果在Android Stuido出现一个黄色警告叫你替换的话,你就可以考虑替换了。因为对于Android 来说,内存往往比较重要一点。

稀疏数组

下面我们分析SparseArray的实现方式,从字面意思上翻译过来就是稀疏数组,首先我们打开源码看一下SparseArray的结构是怎么样的:

public class SparseArray<E> implements Cloneable {
    private static final Object DELETED = new Object();
    private boolean mGarbage = false;

    private int[] mKeys;
    private Object[] mValues;
    private int mSize;
    
    ........我是省略的代码哟..........   
}

看的出代码中有一个 int[] mKeys 数组, 和一个 Object[] mValues 数组,这两个就是存放键值和对象的地方,mSize是我们存入了多少键值对。下面我们将要用一个非常土的办法来看看这个结构是怎么样的,我们写一段测试代码如下,断点调试。

1 SparseArray<Object> sparseArray = new SparseArray<>();
2 sparseArray.put(1, "11");
3 sparseArray.put(8, "13");
4 sparseArray.put(4, "12");
5 sparseArray.put(0, "30");

我们执行第二行代码以后:

mKey中的结构: {1,0,0,0,0,0,0,0,0,0,0,0} 后面的0是初始化mKey的长度 mValues中的结构: {"11"}
mSize: 1

我们执行第三行代码以后:

mKey中的结构: {1,8,0,0,0,0,0,0,0,0,0,0} 后面的0是初始化mKey的长度 mValues中的结构: {"11","13"}
mSize: 2

我们执行第三行代码以后:

mKey中的结构: {1,4,8,0,0,0,0,0,0,0,0,0} 后面的0是初始化mKey的长度 mValues中的结构: {"11","12","13"}
mSize: 3

我们执行第三行代码以后:

mKey中的结构: {0,1,4,8,0,0,0,0,0,0,0,0,0} 后面的0是初始化mKey的长度 mValues中的结构: {"30","11","12","13"}
mSize: 4

从以上的结构中我们可以判断出,如果我们想查找一个键值为4的key,那么首先第一步找到key对应在mKey数组中的index,那么对应的value就在对应mValues中的index位置。再仔细观察上面的mKey中的结构,你会发现mKey中的数字是递增的,那么这保证了我们就可以通过二分查找去找到某一个值在mkey中的位置。ok这个结构分析完毕之后,我们接下来看看,增,删,查,找功能。

ADD

   /**
     * Adds a mapping from the specified key to the specified value,
     * replacing the previous mapping from the specified key if there
     * was one.
     */
    public void put(int key, E value) {
        //通过二分查找键值
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        //如果找到了 就直接替换
        if (i >= 0) {
            mValues[i] = value;
        } else {
            //如果没有找到,这个返回的值就是没有找到的mid+1 那么再取反 正好是当前mKey的存有值      
            //的下一个值,只能说好巧妙~~
            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);
            }

            //GrowingArrayUtils 这个源码看不到,google下源码,就是他会动态增加数组的size,    
            //通过System.arraycopy 实现的,具体自己去google咯
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }

Delete

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

Find

    /**
     * Gets the Object mapped from the specified key, or the specified Object
     * if no such mapping has been made.
     */
    @SuppressWarnings("unchecked")
    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];
        }
    }

看了以上的操作,都是通过二分查找来操作的,所以和HashMap相比, 性能还是有所损失的,最后看看GC的操作

GC

 private void gc() {
        // Log.e("SparseArray", "gc start with " + mSize);

        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;

        // Log.e("SparseArray", "gc end with " + mSize);
    }

总结一下, 就是SpareArray 比HashMap更节约内存,但是性能不如HashMap,在Android系统这内存似金的年代,我们还是应该想尽各种办法去节约内存的。 SpareArray还有一个兄弟 叫LongSparsArray 实现原理也是一样的。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!