Android--SparseArray、ArrayMap

在Android开发时,我们使用的大部分都是Java的api。其中我们经常会用到java中的集合,比如HashMap,使用HashMap非常舒服,但是对于Android这种内存敏感的移动平台,很多时候使用这些java的api并不能达到更好的性能,相反反而更消耗内存,所以针对Android,google也推出了更符合自己的api,比如SparseArray、ArrayMap用来代替HashMap在有些情况下能带来更好的性能提升。

HashMap

我们可以先来看看HashMap的实现:

这里写图片描述

平时我们使用HashMap一般会new一个对象,使用无参的构造方法,
我们看到注释中的说明,默认容量为16,加载因子是0.75。

但是我们现在new出hashmap不会初始化这个16个容量大小的容器。
直到我们put

这里写图片描述

put数据的时候,如果会调用resize方法,然后会

这里写图片描述

将newCap设为16,newthr的值是16*0.75并赋值给了一个全局变量threshold。
这里我们看到一旦我们使用了这个hashmap,我们就需要

这里写图片描述

创建一个16个容量的数组,哪怕我们只存储1-2个数据。
而我们put数据(put函数)

这里写图片描述

如果我们的容量一旦大于threshold

这里写图片描述

我们就需要容量*2,所以我们16的容量会存储12个数据,而存储第13个数据,就需要24大小的数组。 16-12有4个不能用32的容量,我们只能存储24个数据,32-24=8.

那么如果我们需要存粗更多的数据,那么被浪费掉的容量也会越来越大。

HashMap存储数据通过他的内部类Node

这里写图片描述

Node中有4个成员,分别存储key的hash值、key、value与下一个node。
我们知道java有自动装箱。如果我们的key确定了是int,那么我们使用hashmap的时候一般会写成HashMap<Integer,Object>。Int占4个字节,但是Integer有16个。那么当我们使用int操作这个hashmap的时候,会产生一个int基础类型与一个integer对象,创建对象需要耗费更多性能,并且也会更占内存。

SparseArray

在Android中,某些情况我们可以使用SparseArray来替代hashmap,看看源码

这里写图片描述

可以看到与hashmap不同,sa使用两个数组分别保存key与value,并且key必须是int。
并且插入数据的时候

这里写图片描述

http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/java/com/android/internal/util/GrowingArrayUtils.java

这里写图片描述

而扩容也是2倍

这里写图片描述

但是没有扩容因子。
并且获取数据也是使用从中间找的二分查找,

这里写图片描述

比hashmap的遍历数组来获得对于value要更快。

虽说SparseArray性能比较好,但是由于其添加、查找、删除数据都需要先进行一次二分查找,所以在数据量大的情况下性能并不明显。

一般满足下面两个条件我们可以使用SparseArray代替HashMap:
• 数据量不大,最好在千级以内
• key必须为int类型,这中情况下的HashMap可以用SparseArray代替

分析下方法:

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

这个方法很简单,就是把元素重新排放,如果之前有删除的,就把后面的挪到前面,删除之后就会标注为DELETED,我们主要看一下put(int key, E value)方法

 /** 
   * 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) {  
          //如果找到,说明这个key是存在的,替换就行了。  
          mValues[i] = value;  
      } else {  
          //如果没找到就取反,binarySearch方法没找到返回的是大于key所在下标的取反,在这里再取反  
          //返回的正好是大于key所在下标的值  
          i = ~i;  
          //首先说明一点,是有的key值存放的时候都是排序好的,如果当前存放的key大于数组中最大的key  
           //那么这时的i肯定是大于mSize的,在这里i小于mSize说明这里的key是小于mKeys[]中的最大值的,  
           //如果mValue[i]被删除了,就把当前的key和value放入其中,在这里举个例子,比如下面的数组  
           //{1,3,7,9,13,16,22}如果key为7通过二分法查找得到的i为2,如果key为8则得到的i为-4,通过取反  
           //为3,在下标为3的位置如果被删除了就用当前的值替换掉  
          if (i < mSize && mValues[i] == DELETED) {  
              mKeys[i] = key;  
              mValues[i] = value;  
              return;  
          }  
           //如果当前下标为i的没有被删除,就会执行下面的代码。如果对数据进行了操作,就是mGarbage为true,  
           //并且当前的数据已经满了就调用gc(),然后再重新查找,因为gc之后数据的位置可能会有变化,所以要  
           //必须重新查找  
          if (mGarbage && mSize >= mKeys.length) {  
              gc();  

              // Search again because indices may have changed.  
              i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);  
          }  
            //当目前空间满了以后需要重新计算最理想的数组大小,然后再对数组进行扩容。  
          if (mSize >= mKeys.length) {  
              int n = ArrayUtils.idealIntArraySize(mSize + 1);  

              int[] nkeys = new int[n];  
              Object[] nvalues = new Object[n];  

              // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);  
              System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);  
              System.arraycopy(mValues, 0, nvalues, 0, mValues.length);  

              mKeys = nkeys;  
              mValues = nvalues;  
          }  
             //这里的i有可能是上面重新查找的i,根据上面的二分法查找如果等于mSize,说明当前的key比mKeys中的任何  
             //值都要大,肯定要按顺序放在mKeys数组中最大值的后面,如果不等于,说明当前的key应该放到mKeys数组中  
             //间下标为i的位置,需要对当前大于key的值向后移一位。  
          if (mSize - i != 0) {  
              // Log.e("SparseArray", "move " + (mSize - i));  
              System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);  
              System.arraycopy(mValues, i, mValues, i + 1, mSize - i);  
          }  
            //存放数据  
          mKeys[i] = key;  
          mValues[i] = value;  
          mSize++;  
      }  
  }  

我们再来看一下上面提到的binarySearch(int[] array, int size, int value)方法

static int binarySearch(int[] array, int size, int value) {  
    int lo = 0;  
    int hi = size - 1;  

    while (lo <= hi) {  
        int mid = (lo + hi) >>> 1;  
        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  
}  

这就是二分法查找,前提是数组必须是排序好的并且是升序排列,原理就是通过循环用当前的value和数组中间的值进行比较,如果小于就在前半部分查找,如果大于就在后半部分查找。最后如果找到就返回所在的下标,如果没有就返回一个负数。剩下的remove(int key)方法和delete(int key)方法都很简单,删除的时候只是把他的value置为DELETED就可以了,这里就不在介绍。下面我们再来介绍最后一个方法append(int key, E value)

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

    int pos = mSize;  
    if (pos >= mKeys.length) {  
        int n = ArrayUtils.idealIntArraySize(pos + 1);  

        int[] nkeys = new int[n];  
        Object[] nvalues = new Object[n];  

        // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);  
        System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);  
        System.arraycopy(mValues, 0, nvalues, 0, mValues.length);  

        mKeys = nkeys;  
        mValues = nvalues;  
    }  

    mKeys[pos] = key;  
    mValues[pos] = value;  
    mSize = pos + 1;  
}  

通过上面的注释我们知道如果当前的key比mKeys中的任何一个都大时,使用这个方法比put方法效率更好一些,这个方法和put差不多,put方法的key可以是任何值,但append方法的key值更偏向于大于mKeys的最大值,如果小于就会调用put方法。

ArrayMap

ArrayMap是一个<key,value>映射的数据结构,内部同样使用两个数组进行数据存储,一个数组记录key的hash值,另外一个数组记录Value值,它和SparseArray一样,也会对key使用二分法进行从小到大排序,在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,然后通过index来进行添加、查找、删除等操作,所以,应用场景和SparseArray的一样,不同的就是key可以是任意类型。

不过不同的是他的一个数组存储的是Hash值另一个数组存储的是key和value,其中key和value是成对出现的,key存储在数组的偶数位上,value存储在数组的奇数位上,我们先来看其中的一个构造方法

public ArrayMap(int capacity) {  
    if (capacity == 0) {  
        mHashes = ContainerHelpers.EMPTY_INTS;  
        mArray = ContainerHelpers.EMPTY_OBJECTS;  
    } else {  
        allocArrays(capacity);  
    }  
    mSize = 0;  
} 

当capacity不为0的时候调用allocArrays方法分配数组大小,在分析allocArrays源码之前,我们先来看一下freeArrays方法

private static void freeArrays(final int[] hashes, final Object[] array, final int size) {  
    if (hashes.length == (BASE_SIZE*2)) {  
        synchronized (ArrayMap.class) {  
            if (mTwiceBaseCacheSize < CACHE_SIZE) {  
                array[0] = mTwiceBaseCache;  
                array[1] = hashes;  
                for (int i=(size<<1)-1; i>=2; i--) {  
                    array[i] = null;  
                }  
                mTwiceBaseCache = array;  
                mTwiceBaseCacheSize++;  
                if (DEBUG) Log.d(TAG, "Storing 2x cache " + array  
                        + " now have " + mTwiceBaseCacheSize + " entries");  
            }  
        }  
    } else if (hashes.length == BASE_SIZE) {  
        synchronized (ArrayMap.class) {  
            if (mBaseCacheSize < CACHE_SIZE) {  
                array[0] = mBaseCache;  
                array[1] = hashes;  
                for (int i=(size<<1)-1; i>=2; i--) {  
                    array[i] = null;  
                }  
                mBaseCache = array;  
                mBaseCacheSize++;  
                if (DEBUG) Log.d(TAG, "Storing 1x cache " + array  
                        + " now have " + mBaseCacheSize + " entries");  
            }  
        }  
    }  
}  

BASE_SIZE的值为4,ArrayMap对于hashes.length为4和8的两种情况会进行缓存,上面的两种情况下原理都是一样的,我们就用下面的一种情况进行分析,缓存的数量也不是无线大的,当大于等于10(CACHE_SIZE)的时候也就不再进行缓存了,缓存的原理就是让array数组的第一个位置保存之前缓存的mBaseCache,第二个位置保存当前的hashes数组,其他的全部置为空,下面我们再来看一下之前的allocArrays方法

private void allocArrays(final int size) {
        if (mHashes == EMPTY_IMMUTABLE_INTS) {
            throw new UnsupportedOperationException("ArrayMap is immutable");
        }
        if (size == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCache != null) {
                    final Object[] array = mTwiceBaseCache;
                    mArray = array;
                    mTwiceBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mTwiceBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                            + " now have " + mTwiceBaseCacheSize + " entries");
                    return;
                }
            }
        } else if (size == BASE_SIZE) {
            synchronized (ArrayMap.class) {
                if (mBaseCache != null) {
                    final Object[] array = mBaseCache;
                    mArray = array;
                    mBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                            + " now have " + mBaseCacheSize + " entries");
                    return;
                }
            }
        }

        mHashes = new int[size];
        mArray = new Object[size<<1];
    }

如果分配的尺寸不为4或者8,就初始化,我们看到最下面两行mArray的大小是mHashes的两倍,这是因为mArray存储的是key和value两个值。

如果分配的尺寸为4或者8,就判断之前对这两种情况是否进行了缓存,如果缓存过就从缓存中取,取出来的时候会把array的值置空,在上面的freeArrays方法中我们知道array的第一个位置和第二个位置保存的有值,其他的都置为空,在这里把array[0]和array[1]也置为了空,但是有一点奇葩的地方就是mHashes的值确保留了下来,无论是在freeArrays方法中还是在allocArrays方法中,都没有把他置为默认值。

通过ArrayMap的源码发现,这里mHashes的值无论改不改变基本上都没有什么太大影响,因为put的时候如果存在就被替换了,但在indexOf的方法中如果存在还要在继续比较key的值,只有hash和key都一样才会返回。我们下面来看一下indexOf(Object key, int hash)这个方法

int indexOf(Object key, int hash) {  
    final int N = mSize;  

    // Important fast case: if nothing is in here, nothing to look for.  
    if (N == 0) {  
        return ~0;  
    }  

    int index = ContainerHelpers.binarySearch(mHashes, N, hash);  

     // If the hash code wasn't found, then we have no entry for this key.  
    if (index < 0) {  
        return index;  
    }  

     // If the key at the returned index matches, that's what we want.  
    if (key.equals(mArray[index<<1])) {  
        return index;  
    }  

    // Search for a matching key after the index.  
    int end;  
    for (end = index + 1; end < N && mHashes[end] == hash; end++) {  
        if (key.equals(mArray[end << 1])) return end;  
    }  

    // Search for a matching key before the index.  
    for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {  
        if (key.equals(mArray[i << 1])) return i;  
    }  

    // Key not found -- return negative value indicating where a  
    // new entry for this key should go.  We use the end of the  
    // hash chain to reduce the number of array entries that will  
    // need to be copied when inserting.  
    return ~end;  
}  

这个方法很简单,就是根据二分法查找来确定hash值在数组中的位置,如果没找到就返回一个负数,注意下面还有两个循环,这是因为mHashes数组中的hash值不是唯一的,只有hash值相同并且key也相同才会返回所在的位置,否则就返回一个负数。下面就来看一下put(K key, V value)这个方法。

@Override  
  public V put(K key, V value) {  
      final int hash;  
      int index;  
      if (key == null) {  
          hash = 0;  
          index = indexOfNull();  
      } else {  
          hash = key.hashCode();  
          index = indexOf(key, hash);  
      }  
      //通过查找,如果找到就把原来的替换,  
      if (index >= 0) {  
          index = (index<<1) + 1;  
          final V old = (V)mArray[index];  
          mArray[index] = value;  
          return old;  
      }  
        //上面讲过,根据二分法查找,如果没有找到就会返回一个负数,这里进行取反  
      index = ~index;  
        //如果满了就扩容  
      if (mSize >= mHashes.length) {  
            //扩容的尺寸,三目运算符  
          final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))  
                  : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);  

          if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);  

          final int[] ohashes = mHashes;  
          final Object[] oarray = mArray;  
            //扩容  
          allocArrays(n);  
            //如果原来有数据就把原来的数据拷贝到扩容后的数组中  
          if (mHashes.length > 0) {  
              if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");  
              System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);  
              System.arraycopy(oarray, 0, mArray, 0, oarray.length);  
          }  

          freeArrays(ohashes, oarray, mSize);  
      }  
        //根据上面的二分法查找,如果index小于mSize,说明新的数据是插入到数组之间index位置,插入之前需要把后面的移位  
      if (index < mSize) {  
          if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)  
                  + " to " + (index+1));  
          System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);  
          System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);  
      }  
        //数据保存,mHashes只有hash值,mArray即保存key值又保存value值,  
      mHashes[index] = hash;  
      mArray[index<<1] = key;  
      mArray[(index<<1)+1] = value;  
      mSize++;  
      return null;  
  }  

还有clear()方法和erase()方法,这两个区别就是clear()把所有的数据清空,并释放空间,erase()清空数据但没有释放空间,并且erase()只清mArray数据,mHashes数据并没有清空,这就是上面讲到的mHashes即使没清空也不会有影响,代码比较少就不在看了。在看一下和put类似的一个方法append(K key, V value)

/** 
 * Special fast path for appending items to the end of the array without validation. 
 * The array must already be large enough to contain the item. 
 * @hide 
 */  
public void append(K key, V value) {  
    int index = mSize;  
    final int hash = key == null ? 0 : key.hashCode();  
    if (index >= mHashes.length) {  
        throw new IllegalStateException("Array is full");  
    }  
    if (index > 0 && mHashes[index-1] > hash) {  
        RuntimeException e = new RuntimeException("here");  
        e.fillInStackTrace();  
        Log.w(TAG, "New hash " + hash  
                + " is before end of array hash " + mHashes[index-1]  
                + " at index " + index + " key " + key, e);  
        put(key, value);  
        return;  
    }  
    mSize = index+1;  
    mHashes[index] = hash;  
    index <<= 1;  
    mArray[index] = key;  
    mArray[index+1] = value;  
}  

我们看注释这个方法是隐藏的,没有开放,因为这个方法不稳定,如果调用可能就会出现问题,看上面的注释,意思是说这个方法存储数据的时候没有验证,因为在最后存储的时候,是直接存进去的,这就会有一个问题,如果之前存过相同的key和value,再调用这个方法,很可能会再次存入,就可能会有两个key和value完全一样的,我个人认为如果把上面的if (index > 0 && mHashes[index-1] > hash)改为if (index > 0 && mHashes[index-1] >= hash)应该就没问题了,因为如果有相同的就调用put方法把原来的替换,不明白他为什么要这样写

public V removeAt(int index) {  
      final Object old = mArray[(index << 1) + 1];  
        //如果小于等于1就全部清空  
      if (mSize <= 1) {  
          // Now empty.  
          if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");  
          freeArrays(mHashes, mArray, mSize);  
          mHashes = EmptyArray.INT;  
          mArray = EmptyArray.OBJECT;  
          mSize = 0;  
      } else {  
             // 如果数组比较大,但使用的比较少,就会重新分配空间  
          if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {  
              // Shrunk enough to reduce size of arrays.  We don't allow it to  
              // shrink smaller than (BASE_SIZE*2) to avoid flapping between  
              // that and BASE_SIZE.  
                //重新计算空间,当大于8的时候会1.5倍增长  
              final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2);  

              if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);  

              final int[] ohashes = mHashes;  
              final Object[] oarray = mArray;  
                // 重新分配空间  
              allocArrays(n);  

              mSize--;  
              if (index > 0) {  
                    //如果删除的位置大于0,拷贝前半部分到新数组中  
                  if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");  
                  System.arraycopy(ohashes, 0, mHashes, 0, index);  
                  System.arraycopy(oarray, 0, mArray, 0, index << 1);  
              }  
              if (index < mSize) {  
                    // 如果删除的位置小于mSize,把index位置以后的数据拷贝到新数组中  
                  if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize  
                          + " to " + index);  
                  System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index);  
                  System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,  
                          (mSize - index) << 1);  
              }  
          } else {  
              mSize--;  
              if (index < mSize) {  
            //同上  
                  if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize  
                          + " to " + index);  
                  System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index);  
                  System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,  
                          (mSize - index) << 1);  
              }  
                // 把移除的位置置空,上面的为什么没有置空,是因为上面的数据拷贝到一个新的数组中,而删除的就没有  
                //拷贝,这里要置空是因为这里数组没有扩容,还是在原来的数组操作,所以必须置空  
              mArray[mSize << 1] = null;  
              mArray[(mSize << 1) + 1] = null;  
          }  
      }  
      return (V)old;  
  }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值