SparseArray源码简单分析

433 阅读3分钟
1.定义
  • SparseArray是一个以基本类型int为键,Object为值的键值映射对。类似的还有SparseIntArray、SparseLongArray、SparseBooleanArray。
  • 由于不需要自动装箱拆箱,并且不需要创建其他实体(例如HashMap中的Entry),因而它拥有更好的内存效率。
  • 它使用二分查找进行查找、插入和删除,因而不适用于数据量大的情况,在查找效率方面,一般也是不如HashMap的,数据量大时更为突出。
2.应用场景

SparseArray适用于数据量小,并且存取的值为指定类型,例如boolean, long, int, 可以避免自动装箱和拆箱问题。

3.源码解析
    private static final Object DELETED = new Object();
    private boolean mGarbage = false;

    @UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
    private int[] mKeys;
    @UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, E)
    private Object[] mValues;
    @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
    private int mSize;

首先来看一下SparseArray的属性定义,DELETED这个对象先放一下,感觉是和删除相关的。然后mGarbage这个属性是一个bool值,garbage翻译为垃圾感觉也是和删除相关的,也放到分析删除的代码时再看。然后就是mKeys、mValues和mSize,通过属性名可以看出:

  • mKeys为存储键的数组。
  • mValues为存储值的数组。
  • mSize应该是存入键值的数量。
   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;
   }

这里看到构造函数需要传入一个初始的大小,如果初始大小为0.,则创建一个int空数组和object空数组赋值。否则的话,这里newUnpaddedObjectArray调用了native方法VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen),这个方法会new一个不填充的数组,返回一个至少为minLen长度的数组,但有可能会更大。因此mKeys使用Object数组的length进行初始化,而不是使用initialCapacity。

    public E get(int key) {
      	//默认值为null
        return get(key, null);
    }

    @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];
        }
    }

查找对应两个方法,一个为传默认值的,另一个为默认值返回null的。

    /**
     * Removes the mapping from the specified key, if there was any.
     */
    public void delete(int key) {
      	 //通过二分查找获取索引,若不存在为最接近的小值取反
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

      	//i>=0 说明查到了
        if (i >= 0) {
          	//若果对应的value不为DELETED
            if (mValues[i] != DELETED) {
              	//将对应的value置为DELTED,mGarbage置为true
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

果然上面提到的DELETE和mGarbage是与删除相关的,删除只是做标记,不进行数组压缩,那么什么时候进行数组压缩呢?

    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];
						
          	//若果值不为DELETED
            if (val != DELETED) {
              	//并且新下标不等于旧下标
                if (i != o) {
                    //将旧位置的key和value移到新位置
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
								//新下标向后移一位
                o++;
            }
        }
				
      	//标记置为false,标明已进行数组压缩
        mGarbage = false;
        //大小置为新数组键值对的数量
        mSize = o;

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

可以看到gc这个方法中对数组进行了压缩,移除了值为NULL的键值,那么什么时候会调用gc呢?

以上是所有调用gc方法的地方,可以将以上函数总结一下可以归纳为向数组中添加元素,修改mValues数组,获取SparseArray属性(键、值、size)。方法中都是通过判读mGarbage属性是否为true来判断是否需要gc。

    /**
     * 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 {
          	//i取反获取数组索引
            i = ~i;
						
          	//如果所查找的位置对应的值已标记为要回收
            if (i < mSize && mValues[i] == DELETED) {
              	//覆盖要回收的值
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }

          	//如果数组需要被压缩并且键值对数量超过或者等于key数组的数量,这里比较这个大小,说明key数组已经满了,添加需要扩容,							因此回收之后可以不用进行数组扩容。
            if (mGarbage && mSize >= mKeys.length) {
              	//回收
                gc();

                // 重新查找索引由于数组已变化
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }

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

可以看到put方法整体流程:

查找索引 -> 若key存在则直接替换 -> 若key对应的value被标记为Delete,则替换key、value -> 若需要回收,则先回收重新计算索引 -> 添加键值

    /**
     * Primitive int version of {@link #insert(Object[], int, int, Object)}.
     */
    public static int[] insert(int[] array, int currentSize, int index, int element) {
        assert currentSize <= array.length;

        if (currentSize + 1 <= array.length) {
        		//若增加一个数据的大小不超过数组的长度时,直接将index之后的数据整体向后移一位
            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);
        //index赋值
        newArray[index] = element;
        复制旧数组index后的数据到新数组index+1的位置,即将index之后数据整体向后移一位
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }
    
    public static int growSize(int currentSize) {
    		//若不超过4则返回8,否则返回原大小乘2
        return currentSize <= 4 ? 8 : currentSize * 2;
    }

这里通过查看key的insert方法,可以看出如果添加后的大小不超过数组大小,则直接进行添加,否则需要先扩容在添加,value的添加方法完全一致。

添加还有一个方法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();
        }
				
      	//直接将数据放入mSize的位置,这里可以看到调用的GrowingArrayUtils的append方法
        mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
        mValues = GrowingArrayUtils.append(mValues, mSize, value);
        mSize++;
    }

如果已知键大于数组中的所有键,则只需要将数据按顺序加到数组的最后位置即可。