SparseArray源码分析

Android有一组自己的集合类,原因是使用java的集合太占内存。这里主要介绍下SparseXXX系列的容器。

SparseArray将int映射成object,类似Map,但是由于不需要自动装箱,因此会更节约内存。

另外,与HashMap的区别是,由于key是int的,因此其查找使用的是二分查询,而不是hashMap的哈希查询,因此SparseArray不适合大数据的存储,二分查找毕竟比不上哈希查询的效率哈。对于删除key而言,SparseArray不会删除key后立即收缩数据,而会先在该位置做个标记,如果后面再插入相同key,那么是可以复用的。

另外SparseXXX系列还有:

  • SparseIntArray:int映射到int
  • SparseBooleanArray:int映射到boolean
  • SparseLongArray:int映射到long
  • LongSparseArray:long映射到object

SparseArray使用二分查找key,意味着key是有序的。

API说明

SparseArray的接口和Map的接口很类似,put、get、indexOfKey、indexOfValue、clear、size等等。

唯一多的一个是append(int key,E value),等同于put,但是如果key大于目前所有的key,那么会得到优化。至此为什么?下面来细细看一看。

源码解释

字段

public class SparseArray implements Cloneable {
    private static final Object DELETED = new Object();
    private boolean mGarbage = false;

    private int[] mKeys;
    private Object[] mValues;
    private int mSize;
}    

可以看到由于键是int的,又为了避免自动装箱,因此用了int数组。mGarbage字段表示是否要进行删除标记点整理。DELETED字段就是上面所讲的删除标记。

插入方法

put方法

put方法如下所示:

public void put(int key, E value) {
        //二分查找得到该key的index,i>=0表示存在该值;负数表示不存在
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        
        //已经存在该key了,替换值即可
        if (i >= 0) {
            mValues[i] = value;
        }
        //没找到该key,执行插入操作
        else {
            //取反,得到小于key的第一个索引位置
            i = ~i;
            
            //如果这个位置之前做了删除标记,那么回收该位置,这种case不需要扩展数组
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            
            //执行gc,
            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的位置,这个位置可能是一个已经做了删除标记的位置,那么直接复用好了;如果不是,那么可能需要先做一次gc,然后在找到新的位置,最后是插入数据。
上面的方法有一点需要注意:重用删除标记时,没有增加mSize,看来这个mSize的计算另有门道。

其中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) {
                //i和o不相同,执行复制操作,从mKeys、mValues引用切换到keys、values上
                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);
    }

gc方法主要是整理现有数据。可以发现gc并没有去动态减小之前申请的数组大小,因此如果一开始不断插入,将数组扩展的很大,后面删除了不用,那空间还是放在那里的。

触发gc方法的条件是:mGarbage=true&&mSize>=mKeys.Length

真正插入一个数据,mSize会+1,当当前分配的数组都插入过了,即mSize==mKeys.Length,且mGarbage=true,那么需要执行因此gc。

看到这里,是不是想到了虚拟机的gc算法中的:标记清除-标记整理。

删除操作是标记清除,添加操作是标记整理。整理数据,解决碎片

append

append是SparseArray与Map区分的一个方法,其源码如下:

public void append(int key, E value) {
        //如果key的范围已经包含了,那么调用put
        if (mSize != 0 && key <= mKeys[mSize - 1]) {
            put(key, value);
            return;
        }
        
        //如果key大于已有key的范围,那么理论上,该key的位置应该是最后一个
        if (mGarbage && mSize >= mKeys.length) {
            gc();
        }
        
        //追加一个数据
        mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
        mValues = GrowingArrayUtils.append(mValues, mSize, value);
        mSize++;
    }

查询方法

get

查询方法大同小异,看一个就好。

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

二分查找得到索引,如果不存在key或者key已经标记清除了,返回默认值;否则返回值。

size

public int size() {
        if (mGarbage) {
            gc();
        }

        return mSize;
    }

可以看到,如果需要执行标记清理,那么首先要执行标记清理,然后才能返回size。在put中,如果重用删除的位置,size不递增,可以发现这个size如果不先gc,那么是不准确的。

删除操作

remove

public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

这里,可以看到如果找到了key,将其标记清除,且将mGarbage置为true,该值初始化时为false。也意味着只要有成功删除的操作,mGarbage就会为true,也就表示当前有碎片存在,那么在下次插入,如果需要扩展数组时,首先应该先去执行一把整理操作。

总结

SparseArray是Android特有的数据结构,目的是代替Map这种情况,内部使用了二分查找进行查找。同时利用了gc算法:标记清除-标记整理的思路,只要理解了这个,SparseArray的核心思想迎刃而解。

关注我的技术公众号,与君共同学习。微信扫一扫下方二维码即可关注:
SparseArray源码分析_第1张图片

你可能感兴趣的:(深入浅出Android,SparseArray,源码)