Android SparseArray源码分析

一、概述

最近看一些关于Android性能优化方面的书,有讲到了使用Android提供的SparseArray代替Java的HashMap来存储key-value键值对,一定程度上能够提升性能,但是SparseArray也有一定的局限性,比如key的类型固定为int,存储的元素个数过大时对性能有较大的影响等。平时使用过SparseArray,但是未进行深入研究,藉此机会来学习一下。


二、使用SparseArray示例

SparseArray采用泛型(SparseArray<E>)来支持存储任意引用类型的元素,它的使用是比较简单的,因为它的key固定为int类型,所以只需指定value的类型即可。下面为了演示,采用String作为value的类型。实际使用时指定我们需要的类型即可,最终存储的key-value的类型分别为<int,E>

示例如下,列举了常用的几个方法(更多方法可以查看其api):

 // 创建SparseArray,指定存储的值的类型为String
 SparseArray<String> sparseArray = new SparseArray<>();
   for (int i = 0; i < 20; i++) {
       sparseArray.put(i, "element" + i);  // put插入元素
   }

   // get获取元素值
   System.out.println("element value:"+sparseArray.get(10)); 
   // 删除元素
   sparseArray.delete(10);  
   // 获取key对应的索引
   int index = sparseArray.indexOfKey(15); 
   // valueAt根据索引获取元素值
   System.out.println("element value:"+sparseArray.valueAt(index));  

   // 删除一个范围的值,从index起始,删除6个
   sparseArray.removeAtRange(index, 6);

三、SparseArray源码分析

SparseArray是一种存储key-value键值对的容器,前面使用时我们就发现,只需指定value的类型即可,那是因为它内部对key的类型已经做了严格限定(只允许为int类型),因此只需指定value的类型即可,并且容器存储的value只允许为引用类型。

SparseArray将key和value分别使用数组存储,使用数组位置的索引作为key-value的映射纽带,通过二分查找key对应的索引,进而通过索引找到value的值。对元素的存储也有一套管理机制(gc调节),处理已经处于DELETED 状态的元素达到复用,使存储空间达到最优利用。

下面我们通过SparseArray的使用流程,来一步一步解剖它。

1. 创建SparseArray

创建SparseArray只需要一句话,如下:

SparseArray<String> sparseArray = new SparseArray<>();

实际上这是创建一个默认的SparseArray,它初始大小为10。查看源码可知,它是调用了另一个构造方法,初始化了一个大小为10的容器。

/**
     * Creates a new SparseArray containing no mappings.
     */
    public SparseArray() {
        this(10);
    }

    /**
     * Creates a new SparseArray containing no mappings that will not
     * require any additional memory allocation to store the specified
     * number of mappings.  If you supply an initial capacity of 0, the
     * sparse array will be initialized with a light-weight representation
     * not requiring any additional array allocations.
     */
    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;
    }

看到这里,出现了好几个全局变量:mKeys ,mValues ,mSize ,它们的定义如下:

  private static final Object DELETED = new Object();
    private boolean mGarbage = false;

    private int[] mKeys;  // 存储key的数组容器,类型为int
    private Object[] mValues; // 存储value的数组容器,类型为Object,可存储任意引用类型
    private int mSize; //实际存储的元素个数

可以发现key、value是单独存储的,对于上面的mKeys ,mValues ,mSize 都是比较好理解的,但是DELETED 常量是什么意思?见名知意,它确实表示一种删除的对象,当对mValues中的元进行删除时,实际上是赋值为DELETED 的,表示当前元素被删除了,对DELETED 的理解可以认为当前元素是无意义的或者是已经删除的。

那么mGarbage 又是什么?从名字上看,似乎与垃圾回收有关系,诚然它对SparseArray存储空间的动态管理起着重要的作用,在后面后详细分析它。

2. SparseArray存储元素

SparseArray使用put方法来存储元素,该方法定义如下:

 /**
     * 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) {// 插入 , key是有序存储的,从小到大

           int i = ContainerHelpers.binarySearch(mKeys, mSize, key);  // 二分查找key是否存在,返回key的索引

        if (i >= 0) {  // key存在则直接覆盖value
            mValues[i] = value;
        } else { // key不存在
            i = ~i;    // i= -i-1 ; 如i=-15 , 则i = ~i后i=14

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

            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);  // 将key插入指定位置i
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);  // 将value插入指定位置i
            mSize++; 
        }
    }

可以发现对key进行二分查找,获取key在mKeys中的索引,然后根据这个索引来存储value。因此,对mKeys来说是有序的(二分查找的保证),并且根据实现来看是从小到大的。

搜索key的索引可能不存在,此时二分查找返回的i是实际应该插入的位置index取负数-1(-index-1,即~i),此时进行 i = ~i;操作,即可得到应该插入的位置index,当然这个位置上的value有2中可能情况:

  • 1.value可能是DELETED,此时直接进行覆盖就可以了
  • 2.value的值是有意义的,此时需要腾出位置来以供插入

对于第二种情况,实现是由GrowingArrayUtils的insert方法来完成的,如下:

public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
       assert currentSize <= array.length;
        // 插入索引在数组范围内,即数组长度够,不需重新开辟空间
       if (currentSize + 1 <= array.length) { 
           // 将从index起的数组元素都后移一位,腾出位置
           System.arraycopy(array, index, array, index + 1, currentSize - index);            
            // 插入到腾出的index位置
            array[index] = element;           
            return array;
         }

        // 数组空间不够,重新开辟空间
       T[] newArray = (T[]) Array.newInstance(array.getClass().getComponentType(),
               growSize(currentSize));
       System.arraycopy(array, 0, newArray, 0, index); // 拷贝0~index-1 到新数组
       newArray[index] = element; // 插入到index位置
       // 拷贝index 开始的原数组元素到新数组的index + 1起始,拷贝剩余的元素
        System.arraycopy(array, index, newArray, index + 1, array.length - index); 
        return newArray;  // 最后返回新创建的数组
   }

特别说明
arraycopy函数是System类的一个静态函数,用于将一个数组的内容拷贝到另一个数组。

函数原型

public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)

  • 从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。
  • 源数组中位置在 srcPos 到 srcPos+length-1 之间的组件被分别复制到目标数组中的 destPos 到 destPos+length-1 位置。

参数

  • src - 源数组。
  • srcPos - 源数组中的起始位置。
  • dest - 目标数组。
  • destPos - 目标数据中的起始位置。
  • length - 要复制的数组元素的数量。

当数组长度不大于4时,指定增长为8,否则增长原来长度的两倍,增长规律通过如下函数控制:

public static int growSize(int currentSize) {
        return currentSize <= 4 ? 8 : currentSize * 2;
}

使用ContainerHelpers的binarySearch方法进行二分查找,对于其中的二分算法如下,没有什么好说的:

// This is Arrays.binarySearch(), but doesn't do any argument validation.
    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;   // mid + 1 ~ hi
            } else if (midVal > value) {
                hi = mid - 1;  // lo ~ mid - 1
            } else {
                return mid;  // value found
            }
        }
        return ~lo;  // value not present
    }

2. SparseArray获取元素

SparseArray获取元素使用 get方法,定义如下:

    /**
     * Gets the Object mapped from the specified key, or <code>null</code>
     * if no such mapping has been made.
     */
    public E get(int key) {
        return get(key, null);
    }

 /**
     * Gets the Object mapped from the specified key, or the specified Object
     * if no such mapping has been made.
     */
public E get(int key, E valueIfKeyNotFound) {
         // 二分查找key是否存在,返回key的索引
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
         // 不存在key或key对应的值为DELETED,则返回指定的默认值valueIfKeyNotFound
        if (i < 0 || mValues[i] == DELETED) {             return valueIfKeyNotFound;
        } else { 
            return (E) mValues[i];  // 存在key则返回mValues中相同索引的value
        }
    }

可以发现,获取元素是非常简单的,两个方法,要么不指定默认值,不存在则返回null;要么指定默认值valueIfKeyNotFound,不存在则返回之。和存储元素类似,使用二分查找获得索引,然后得到value。

还有类似的valueAt、keyAt等就不一一分析了,原理大致相同。

3. SparseArray删除元素

SparseArray使用delete方法,通过key来删除一组key-value映射,该方法定义如下:

/**
    * Removes the mapping from the specified key, if there was any.
    */
public void delete(int key) {
       int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 二分查找key是否存在,返回key的索引

       if (i >= 0) {  // 存在key
           if (mValues[i] != DELETED) { // value有意义
               mValues[i] = DELETED; // 赋值为DELETED
               mGarbage = true; // 垃圾回收flag置为true
           }
       }
}

delete方法实现比较简单,主要有几个关键点:

  • 1.使用二分查找得到索引,和之前一样
  • 2.对要删除的value赋值为DELETED,即将此处的value置为无意义的
  • 3.置mGarbage 为true,为内存空间的管理做准备

4. SparseArray内存管理

上面多次见过gc方法,那它是干什么的呢?对于一个容器而言,增删是很平常的事,因此如何管理内存是一个关键点。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++) {  // 循环遍历,将数组后面有意义的值,移动到前面为DELETED的位置
            Object val = values[i];

            if (val != DELETED) {
                if (i != o) {  // 将i处的key-value映射前移到o处
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }

                o++;
            }
        }

        mGarbage = false;
        mSize = o; // 调整后的数组的元素的真实个数

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

这个方法比较关键,对我们掌握SparseArray原理至关重要。通常有两个条件同时满足时,才触发gc操作(某些方法只需要mGarbage为true,如size()方法):

  • mGarbage
  • mSize >= mKeys.length

mGarbage 标志着是否有元素存在垃圾状态,即被置为DELETED
mSize >= mKeys.length 表示此时存储的元素个数已满,看看是否需要调整

对于gc的原理是这样的:当满足gc条件后,执行gc,遍历所有mValues的元素,发现DELETED状态,则记录下来(o),在后面的遍历发现不为DELETED时,则将当前的元素key-value映射移动到之前记录的o处,这样使mValues数组的前面x项都被填充有意义的值,后面的空间都腾出来,使数组能够充分利用空间,而不是再开辟新空间来容纳元素。

举个栗子:
gc前的mValues假设是这样子的,当前mSize=7 :

element1, element2,element3,DELETED,element5,DELETED,element7
注:DELETED元素一般是删除了元素造成的,会使mGarbage=true

gc后的mValues变成了如下,当前mSize=5:

element1, element2,element3,element5,element7,null,null

gc后使得mValues的空间得以较好的利用,而不必从新开辟空间造成浪费。

5. SparseArray 使用index获取key与value

在理解上面的内容的基础上,看懂下面代码也比较容易了:

 /**
     * Given an index in the range <code>0...size()-1</code>, returns
     * the key from the <code>index</code>th key-value mapping that this
     * SparseArray stores.
     *
     * <p>The keys corresponding to indices in ascending order are guaranteed to
     * be in ascending order, e.g., <code>keyAt(0)</code> will return the
     * smallest key and <code>keyAt(size()-1)</code> will return the largest
     * key.</p>
     *
     * <p>For indices outside of the range <code>0...size()-1</code>,
     * the behavior is undefined.</p>
     */
     public int keyAt(int index) { // 未做index检查,可能导致异常产生
        if (mGarbage) {
            gc();
        }

        return mKeys[index];
    }

  /**
     * Given an index in the range <code>0...size()-1</code>, returns
     * the value from the <code>index</code>th key-value mapping that this
     * SparseArray stores.
     *
     * <p>The values corresponding to indices in ascending order are guaranteed
     * to be associated with keys in ascending order, e.g.,
     * <code>valueAt(0)</code> will return the value associated with the
     * smallest key and <code>valueAt(size()-1)</code> will return the value
     * associated with the largest key.</p>
     *
     * <p>For indices outside of the range <code>0...size()-1</code>,
     * the behavior is undefined.</p>
     */
    public E valueAt(int index) {
        if (mGarbage) {
            gc();
        }

        return (E) mValues[index];
    }

从上面的注释可以得到:

  • 在得到key或value时,可能会触发gc操作
  • keyAt(0)获得最小的key,keyAt(size()-1)可获得最大的key
  • valueAt(0)返回的是最小key对应的value,valueAt(size()-1)返回的是最大key对应的位置
  • index的范围在0…size()-1其他值是不被支持的

6. SparseArray 删除指定索引、指定范围以及清空

删除指定索引处

    /**
     * Removes the mapping at the specified index.
     *
     * <p>For indices outside of the range <code>0...size()-1</code>,
     * the behavior is undefined.</p>
     */
    public void removeAt(int index) {
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;  // 直接赋值为DELETED
            mGarbage = true;
        }
    }

删除范围,利用removeAt来完成


    /**
     * Remove a range of mappings as a batch.
     *
     * @param index Index to begin at
     * @param size Number of mappings to remove
     *
     * <p>For indices outside of the range <code>0...size()-1</code>,
     * the behavior is undefined.</p>
     */
    public void removeAtRange(int index, int size) {
        final int end = Math.min(mSize, index + size);
        for (int i = index; i < end; i++) {
            removeAt(i);
        }
    }

清空 容器

    /**
     * Removes all key-value mappings from this SparseArray.
     */
    public void clear() {
        int n = mSize;
        Object[] values = mValues;

        for (int i = 0; i < n; i++) {
            values[i] = null;  // 清空时会将元素的值赋值为null
        }

        mSize = 0;
        mGarbage = false;
    }

注意:

  • removeAt、removeAtRange删除元素会导致mGarbage 被置为true,但不会使mSize变化
  • clear操作会将所有元素的值赋值为null,并且置mSize为0

四、总结

  • 1.对于SparseArray而言,对其key-value映射方式(index,二分查找),插入元素的处理(直接覆盖还是需要拓展空间)、以及内存管理(gc)的理解是较为关键的。
  • 2.对于SparseArray 而言,适用于存在数量不多的数据(通常推荐是1000以内),这样才能达到性能较优。毕竟当数量大了以后,每当有删除操作,接下来的操作(比如keyAt)都可能导致gc,会比较消耗内存,影响性能。
  • 3.由于key被限定为int类型,因此在选择时需要考虑存入的元素的key的类型,这需要结合具体的情况综合考虑与运用。
  • 4.SparseArray 为Android提供的容器,用来替代HashMap等Java类容器,因此使用范围有一定的平台局限性。
  • 5.为了达到Android前后版本的兼容性,可以考虑使用v4包的SparseArrayCompat来代替SparseArray。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值