(2.3.14)SparseArray稀疏数组与ArrayMap源码分析


SparseArray,通常来讲是Android中用来替代HashMap的一个数据结构。

准确来讲,是用来替换key为Integer类型,value为Object类型的HashMap。需要注意的是SparseArray仅仅实现了Cloneable接口,所以不能用Map来声明。

从内部结构来讲,SparseArray内部由两个数组组成,一个是int[]类型的mKeys,用来存放所有的键;另一个是Object[]类型的mValues,用来存放所有的值。
在这里插入图片描述
最常见的是拿SparseArray跟HashMap来做对比,由于SparseArray内部组成是两个数组,所以占用内存比HashMap要小。我们都知道,增删改查等操作都首先需要找到相应的键值对,而SparseArray内部是通过二分查找来寻址的,效率很明显要低于HashMap的常数级别的时间复杂度。提到二分查找,这里还需要提一下的是二分查找的前提是数组已经是排好序的,没错,SparseArray中就是按照key进行升序排列的。

综合起来来说,SparseArray所占空间优于HashMap,而效率低于HashMap,是典型的时间换空间,适合较小容量的存储。

一、稀疏数组的由来

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

假设有一个9*7的数组,其内容如下:
  在这里插入图片描述
   图 1 二维数组示例

在此数组中,共有63个空间,但却只使用了5个元素,造成58个元素空间的浪费。以下我们就使用稀疏数组重新来定义这个数组:
在这里插入图片描述
   图 2 使用稀疏数组进行压缩

其中在稀疏数组中第一部分所记录的是原数组的列数和行数以及元素使用的个数、第二部分所记录的是原数组中元素的位置和内容。经过压缩之后,原来需要声明大小为63的数组,而使用压缩后,只需要声明大小为6*3的数组,仅需18个存储空间。

1.1 源码解析

在这里插入图片描述

从源码角度来说,我认为需要注意的是SparseArray的remove()、put()和gc()方法。

1.2 构造方法

// 构造方法
public SparseArray() {
    this(10);
}

// 构造方法
public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
        mKeys = EmptyArray.INT;
        mValues = EmptyArray.OBJECT;
    } else {
        // key value各自为一个数组,默认长度为10
        mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
        mKeys = new int[mValues.length];
    }
    mSize = 0;
}

ps:
SparseArray构造方法中,创建了两个数组mKeys、mValues分别存放int与Object,其默认长度为10

1.3 put(int key, E value)

put()。put方法是这么一个逻辑,如果通过二分查找在mKeys数组中找到了key,那么直接覆盖value即可。如果没有找到,会拿到与数组中与要添加的key最接近的key索引,如果这个索引对应的value为DELETE,则直接把新的value覆盖DELETE即可,在这里可以避免数组元素的移动,从而提高了效率。如果value不为DELETE,会判断mGarbage,如果为true,则会调用gc()方法压缩数组,之后会找到合适的索引,将索引之后的键值对后移,插入新的键值对,这个过程中可能会触发数组的扩容。

public void put(int key, E value) {
        // 二分查找,key在mKeys列表中对应的index
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 如果找到,则直接赋值
        if (i >= 0) {
            mValues[i] = value;
        } 
        // 找不到
        else {
            // binarySearch方法中,找不到时,i取了其非,这里再次取非,则非非则正
            i = ~i;
            // 如果该位置的数据正好被删除,则赋值
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            // 如果有数据被删除了,则gc
            if (mGarbage && mSize >= mKeys.length) {
                gc();
                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            // 插入数据,增长mKeys与mValues列表
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
}

ps:

因为key为int,不存在hash冲突
mKeys为有序列表,通过二分查找,找到要插入的key对应的index (这里相对于查找hash表应该算是费时间吧,但节省了内存,所以是 时间换取了空间)
通过二分查找到的index,将Value插入到mValues数组的对应位置

1.4 get(int key)

remove()。SparseArray的remove()方法并不是直接删除之后再压缩数组,而是将要删除的value设置为DELETE这个SparseArray的静态属性,这个DELETE其实就是一个Object对象,同时会将SparseArray中的mGarbage这个属性设置为true,这个属性是便于在合适的时候调用自身的gc()方法压缩数组来避免浪费空间。这样可以提高效率,如果将来要添加的key等于删除的key,那么会将要添加的value覆盖DELETE。

// 通过key查找对应的value
public E get(int key) {
        return get(key, null);
}
// 通过key查找对应的value
public E get(int key, E valueIfKeyNotFound) {
        // mKeys数组中采用二分查找,找到key对应的index
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        // 没有找到,则返回空
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
        // 找到则返回对应的value
            return (E) mValues[i];
        }
}

// array为有序数组
// size数组中内容长度
// value要查找的值
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];
            // 如果中间元素小于要查找元素,则midIndex赋值给 lo 
            if (midVal < value) {
                lo = mid + 1;
            }
            // 如果中间元素大于要查找元素,则midIndex赋值给 hi  
            else if (midVal > value) {
                hi = mid - 1;
            }
            // 找到则返回 
            else {
                return mid;  // value found
            }
        }
        // 找不到,则lo 取非
        return ~lo;  // value not present
}

ps:
每次调用get,则需经过一次mKeys数组的二分查找,因此mKeys数组越大则二分查找的时间就越长,因此SparseArray在大量数据,千以上时,会效率较低

1.5 gc()

gc()。SparseArray中的gc()方法跟JVM的GC其实完全没有任何关系。gc()方法的内部实际上就是一个for循环,将value不为DELETE的键值对往前移动覆盖value为DELETE的键值对来实现数组的压缩,同时将mGarbage置为false,避免内存的浪费。

二、android.support.v4.util.ArrayMap

ArrayMap和SparseArray有点类似;其中含有两个数组,一个是mHashes(key的hash值数组,为一个有序数组),另一个数组存储的是key和value,其中key和value是成对出现的,key存储在数组的偶数位上,value存储在数组的奇数位上。
在这里插入图片描述

2.1 构造函数

public SimpleArrayMap() {
     // key的hash值数组,为一个有序数组
     mHashes = ContainerHelpers.EMPTY_INTS;
     // key 与 value数组
     mArray = ContainerHelpers.EMPTY_OBJECTS;
     mSize = 0;
}

ps:
构造方法中初始化了两个数组mHashes、mArray,其中mHashes为key的Hash值对应的数组

2.2 put(K key, V value)

public V put(K key, V value) {
        // key 对应的hash值
        final int hash;
        // hash对应的mHashes列表的index
        int index;
        // key为空,hash为0
        if (key == null) {
            hash = 0;
            index = indexOfNull();
        }
        //  
        else {
            // 计算key的hashcode
            hash = key.hashCode();
            // 查找key对应mHashes中的index,大于0则找到了,否则为未找到
            // 这里涉及到hash冲突,如果hash冲突,则在index的相邻位置插入数据
            index = indexOf(key, hash);
        }
        // 找到key对应mHashes中的index
        if (index >= 0) {
            // 取出基数位置原有的Value
            index = (index<<1) + 1;
            final V old = (V)mArray[index];
            // 将新数据放到基数index位置
            mArray[index] = value;
            return old;
        }
        // indexOf中取了反,这里反反则正
        index = ~index;
        // 如果满了就扩容  
        if (mSize >= mHashes.length) {
            final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
                    : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

            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[index] = hash;
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;
        mSize++;
        return null;
}

// 根据key 与key的hash,查找key对应的index
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;
        }
        // 二分查找mHashes有序数组,查找hash对应的index
        int index = ContainerHelpers.binarySearch(mHashes, N, hash);
        // 没有找到
        if (index < 0) {
            return index;
        }
        // 偶数位为对应的key,则找到了
        if (key.equals(mArray[index<<1])) {
            return index;
        }
        // index之后查找
        // 这里涉及到hash冲突,如果hash冲突,则在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;
        }
        // index之前查找
        // 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;
        }
        // 没有找到
        return ~end;
}

参考文献

猜你喜欢

转载自blog.csdn.net/fei20121106/article/details/86595351