android sparseArray源码解析,比较hashmap

来源:互联网 发布:学生男鞋品牌 知乎 编辑:程序博客网 时间:2024/06/05 17:17


step1:背景

今天偶然看到了sparseArray,发现其主要是针对<(Integer,obj>的类型进行了优化,何为sparse?是稀疏的意思。指的是对稀疏数组情况的讨论

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

step2:上源码

  • 类结构图 
    类结构
  • 构造函数 
    SparseArray(int)可以指定容量
 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;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 它有两个方法可以添加键值对 
    • 1.put(int key, E value)
/**     * 朝map里面加入key,value的数据,如果key存在,替换操作     */    public void put(int key, E value) {        //二分查找,具体看step3        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);        if (i >= 0) {            mValues[i] = value;//找到,说明之前存在        } else {            i = ~i;//为负说明没找到            if (i < mSize && mValues[i] == DELETED) {                mKeys[i] = key;                mValues[i] = value;//添加key位置的value被删除了                return;            }            //需要gc或者是当前数组大小越界了            if (mGarbage && mSize >= mKeys.length) {                gc();//gc()                // gc后index变化了,需要重新查找                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);            }            //插入key和value到指定位置            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);//            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);            mSize++;//当前大小+1        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
*  2.append(int key, E value) 
 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++;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 有四个方法可以执行删除操作: 
    • 1.delete(int key)
/**     * 移除指定key的value     */    public void delete(int key) {        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);        if (i >= 0) {            if (mValues[i] != DELETED) {                mValues[i] = DELETED;//标记为DELETED                mGarbage = true;            }        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
*  2.remove(int key)
public void remove(int key) {        delete(key);    }
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
* 3. removeAt(int index)
 /**     * Removes the mapping at the specified index.     */    public void removeAt(int index) {        if (mValues[index] != DELETED) {            mValues[index] = DELETED;            mGarbage = true;        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
*   4.removeAtRange(int index, int size)
//移除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);        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

step3:Util函数支持

put(int key, E value) 中出现了3个函数,现在我们一一解析 
1)ContainerHelpers.binarySearch(mKeys, mSize, key); 
2)gc(); 
3)GrowingArrayUtils.insert(mKeys, mSize, i, key); 
* ContainerHelpers.binarySearch(mKeys, mSize, key);

    static int binarySearch(int[] array, int size, int value) {        int lo = 0;        int hi = size - 1;    //说明array是排号序的,那么他们在哪里排序,在insert的时候排的        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    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • GrowingArrayUtils.insert(mKeys, mSize, i, key);
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 = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));//数组扩容        System.arraycopy(array, 0, newArray, 0, index);        newArray[index] = element;        System.arraycopy(array, index, newArray, index + 1, array.length - index);        return newArray;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 3)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;//value置空                }                o++;            }        }        mGarbage = false;        mSize = o;        // Log.e("SparseArray", "gc end with " + mSize);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27



总结:

SparseArray是Android里为(Interger,Object)这样的Hashmap而专门写的类,目的是提高内存效率,其核心是折半查找函数(binarySearch)。注意内存二字很重要,因为它仅仅提高内存效率,而不是提高执行效率,所以也决定它只适用于android系统(内存对android项目有多重要,地球人都知道)。SparseArray有两个优点:1.避免了自动装箱(auto-boxing),2.数据结构不会依赖于外部对象映射。我们知道HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置,存放的都是数组元素的引用,通过每个对象的hash值来映射对象。而SparseArray则是用数组数据结构来保存映射,然后通过折半查找来找到对象。但其实一般来说,SparseArray执行效率比HashMap要慢一点,因为查找需要折半查找,而添加删除则需要在数组中执行,而HashMap都是通过外部映射。但相对来说影响不大,最主要是SparseArray不需要开辟内存空间来额外存储外部映射,从而节省内存。





这就是二者的结构,我们需要看一下二者到底有什么差异...

  首先是插入:

  HashMap的正序插入:

复制代码
 HashMap<Integer, String>map = new HashMap<Integer, String>(); long start_map = System.currentTimeMillis(); for(int i=0;i<MAX;i++){     map.put(i, String.valueOf(i)); } long map_memory = Runtime.getRuntime().totalMemory(); long end_map = System.currentTimeMillis()-start_map; System.out.println("<---Map的插入时间--->"+end_map+"<---Map占用的内存--->"+map_memory);

执行后的结果:
<---Map的插入时间--->914
<---Map占用的内存--->28598272
复制代码

   SparseArray的正序插入:

复制代码
 SparseArray<String>sparse = new SparseArray<String>(); long start_sparse = System.currentTimeMillis(); for(int i=0;i<MAX;i++){        sparse.put(i, String.valueOf(i)); } long sparse_memory = Runtime.getRuntime().totalMemory(); long end_sparse = System.currentTimeMillis()-start_sparse; System.out.println("<---Sparse的插入时间--->"+end_sparse+"<---Sparse占用的内存--->"+sparse_memory);//执行后的结果:<---Sparse的插入时间--->611<---Sparse占用的内存--->23281664
复制代码

   我们可以看到100000条数据量正序插入时SparseArray的效率要比HashMap的效率要高.并且占用的内存也比HashMap要小一些..这里的正序插入表示的是i的值是从小到大进行的一个递增..序列取决于i的值,而不是for循环内部如何执行...

  通过运行后的结果我们可以发现,SparseArray在正序插入的时候,效率要比HashMap要快得多,并且还节省了一部分内存。网上有很多的说法关于二者的效率问题,很多人都会误认为SparseArray要比HashMap的插入和查找的效率要快,还有人则是认为Hash查找当然要比SparseArray中的二分查找要快得多.

  其实我认为Android中在保存<Integer,Value>的时候推荐使用SparseArray的本质目的不是由于效率的原因,而是内存的原因.我们确实看到了插入的时候SparseArray要比HashMap要快.但是这仅仅是正序插入.我们来看看倒序插入的情况.

  HashMap倒序插入:

复制代码
  System.out.println("<------------- 数据量100000 散列程度小 Map 倒序插入--------------->");  HashMap<Integer, String>map_2 = new HashMap<Integer, String>();  long start_map_2 = System.currentTimeMillis();  for(int i=MAX-1;i>=0;i--){      map_2.put(MAX-i-1, String.valueOf(MAX-i-1));  }  long map_memory_2 = Runtime.getRuntime().totalMemory();  long end_map_2 = System.currentTimeMillis()-start_map_2;  System.out.println("<---Map的插入时间--->"+end_map_2+"<---Map占用的内存--->"+map_memory_2);    //执行后的结果:  <------------- 数据量100000 Map 倒序插入--------------->  <---Map的插入时间--->836<---Map占用的内存--->28598272
复制代码

  SparseArray倒序插入:

复制代码
System.out.println("<------------- 数据量100000 散列程度小 SparseArray 倒序插入--------------->");SparseArray<String>sparse_2 = new SparseArray<String>();long start_sparse_2 = System.currentTimeMillis();for(int i=MAX-1;i>=0;i--){    sparse_2.put(i, String.valueOf(MAX-i-1));}long sparse_memory_2 = Runtime.getRuntime().totalMemory();long end_sparse_2 = System.currentTimeMillis()-start_sparse_2;System.out.println("<---Sparse的插入时间--->"+end_sparse_2+"<---Sparse占用的内存--->"+sparse_memory_2);//执行后的结果<------------- 数据量100000 SparseArray 倒序插入---------------><---Sparse的插入时间--->20222<---Sparse占用的内存--->23281664
复制代码

 通过上面的运行结果,我们仍然可以看到,SparseArray与HashMap无论是怎样进行插入,数据量相同时,前者都要比后者要省下一部分内存,但是效率呢?我们可以看到,在倒序插入的时候,SparseArray的插入时间和HashMap的插入时间远远不是一个数量级.由于SparseArray每次在插入的时候都要使用二分查找判断是否有相同的值被插入.因此这种倒序的情况是SparseArray效率最差的时候.

 这就是SparseArray插入函数的源码.每次的插入方式都需要调用二分查找.因此这样在倒序插入的时候会导致情况非常的糟糕,效率上绝对输给了HashMap学过数据结构的大家都知道.Map在插入的时候会对冲突因子做出相应的决策.有非常好的处理冲突的方式.不需要遍历每一个值.因此无论是倒序还是正序插入的效率取决于处理冲突的方式,因此插入时牺牲的时间基本是相同的.

  通过插入.我们还是可以看出二者的差异的.



  我们再来看一下查找首先是HashMap的查找.

复制代码
  System.out.println("<------------- 数据量100000 Map查找--------------->");  HashMap<Integer, String>map = new HashMap<Integer, String>();         for(int i=0;i<MAX;i++){        map.put(i, String.valueOf(i));  }  long start_time =System.currentTimeMillis();  for(int i=0;i<MAX;i+=100){           map.get(i);  }  long end_time =System.currentTimeMillis()-start_time;  System.out.println(end_time);    //执行后的结果  <!---------查找的时间:175------------>  
复制代码

   SparseArray的查找:

复制代码
  System.out.println("<------------- 数据量100000  SparseArray 查找--------------->");  SparseArray<String>sparse = new SparseArray<String>();  for(int i=0;i<10000;i++){        sparse.put(i, String.valueOf(i));  }  long start_time =System.currentTimeMillis();          for(int i=0;i<MAX;i+=10){        sparse.get(i);  }  long end_time =System.currentTimeMillis()-start_time;  System.out.println(end_time);  //执行后的结果  <!-----------查找的时间:239---------------->
复制代码

  我这里也简单的对查找的效率进行了测试.对一个数据或者是几个数据的查询.二者的差异还是非常小的.当数据量是100000条.查100000条的效率还是Map要快一点.数据量为10000的时候.这就差异性就更小.但是Map的查找的效率确实还是赢了一筹.


  其实在我看来.在保存<Integer,E>时使用SparseArray去替换HashMap的主要原因还是因为内存的关系.我们可以看到.保存的数据量无论是大还是小,Map所占用的内存始终是大于SparseArray的.数据量100000条时SparseArray要比HashMap要节约27%的内存.也就是以牺牲效率的代价去节约内存空间.我们知道Android对内存的使用是极为苛刻的.堆区允许使用的最大内存仅仅16M.很容易出现OOM现象的发生.因此在Android中内存的使用是非常的重要的.因此官方才推荐去使用SparseArray<E>去替换HashMap<Integer,E>.官方也确实声明这种差异性不会超过50%.所以牺牲了部分效率换来内存其实在Android中也算是一种很好的选择吧.