Android内存优化之SparseArray源码解析

Android内存优化之SparseArray源码解析

一.概述

当遇到性能问题时,可以使用SparseArray来代替HashMap进行一些操作。
在源代码中(API26)中有如下一段描述:

It is intended to be more memory efficient than using a HashMap to map
Integers to Objects, both because it avoids auto-boxing keys and its
data structure doesn’t rely on an extra entry object for each mapping.

大致意思就是:目的是在进行数字到对象映射时,会比使用HashMap内存效率更高。
原因有两点:第一是避免了自动装箱;第二是数据结构不依赖于额外的Entry(对比HashMap)

It is generally slower than a traditional HashMap, since lookups require a binary search and adds and removes require inserting and deleting entries in the array. For containers holding up to hundreds of items, the performance difference is not significant, less than 50%.

该数据结构内部使用数组实现,使用二分查找来寻找keys。一般来讲,它的速度比HashMap要慢一点(因为使用二分查找O(logN));当数据量增加时,性能变化不明显,小于50%

the container includes an optimization when removing keys: instead of compacting its array immediately, it leaves the removed entry marked as deleted. The entry can then be re-used for the same key, or compacted later in a single garbage collection step of all removed entries.

可以看到文档里还写到延迟删除这一功能,稍后在源码中进行解析

二.源码


public class SparseArray<E> implements Cloneable{
    private static final Object DELETED = new Object();//标记删除位
    private boolean mGarbage = false;//是否回收

    private int[] mKeys;//键使用int数组存储,直接避免了Integer装箱
    private Object[] mValues;//value存储数组
    private int mSize;//大小
    ……
}

先看最基本的几个成员变量,都已经在注释中标明了

//构造函数,显然默认大小为10
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大小进行了特殊处理:初始化时不需要额外的数组分配;其他情况下正常分配空间大小。
将size初始化为0

接下来是主要使用的get、put方法
先看一下put方法

public void put(int key, E value) {
        //通过一个工具类进行二分查找,如果返回一个非负数则说明查找到,
        //返回负数则是没有找到
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //说明命中,直接更新value值
        if (i >= 0) {
            mValues[i] = value;
        } else {
        //因为负数表示未命中,二分查找代码中返回的是二分查找使用的lo值取反,
        //所以再取反就得到了lo值
            i = ~i;
        //如果lo小于数组大小并且此时对应的value值是已移除状态(此时还没有调用gc方法)
        //那么就更新key和value并返回,结束此次put
        //所以从源码中可以看出,如果移除了某一个值,并在之后又有新的添加动作
        //那么原来的key就可能被覆盖掉
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
        //如果还没有进行gc操作并且数组大小不能再增加时
        //先进行gc方法
        //然后获取要插入的位置并增加数组大小
            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);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }

刚才提到了一些诸如DELETED、gc方法等等
先看一下DELETED的存在

public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //如果要删除的键存在,那么把对应的value标记位DELETED
        //注意garbage参数被设置为true,即意味着可以进行gc方法
        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

接着看一下gc方法的实现

private void gc() {
        // Log.e("SparseArray", "gc start with " + mSize);
        //保存原来的数组大小
        int n = mSize;
        //用来计算剩下的大小
        int o = 0;
        //复制key
        int[] keys = mKeys;
        //复制value
        Object[] values = mValues;
        //循环,更新数组
        for (int i = 0; i < n; i++) {
            Object val = values[i];
            //如果不是DELETED,那么就保存下来
            if (val != DELETED) {
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }

                o++;
            }
        }
        //至此数组更新结束
        mGarbage = false;
        //大小更新为o指示的大小
        mSize = o;

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

到了这里应该就明白put方法了
接下来是比较简单的get方法

public E get(int key) {
        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];
        }
    }

不用多说,根据前面的讲解,就知道当二分查找为负数或者值为DELETED时,就是未找到,否则就返回value数组中对应的数据

剩下的方法就比较简单了,都是在这几个方法基础上的调用

三.使用

SparseArray<String> sparseArray = new SparseArray<>();
sparseArray.put(1,"hello");
sparseArray.put(2,"hello");
sparseArray.put(3,"hello");
Log.i(TAG, "onCreate: " + sparseArray.get(4));
Log.i(TAG, "onCreate: " + sparseArray.get(2));

和HashMap基本一样的调用,非常简单,但是只能使用int作为key值

四.总结

相比于HashMap,SparseArray有如下优缺点
优点

  • 使用int类型作为key而不是Integer,避免了自动装箱和拆箱的操作,提高了效率
  • 避免了HashMap会出现的哈希冲突(退化成单链表,虽然JDK8变为红黑树),避免重哈希带来的性能问题

缺点
- 只能使用int作为key,对于其他类型无能为力
- 由于使用二分查找,所以查找时间复杂度为O(lgn),相比于HashMap的查找O(1)有劣势

对于HashMap有疑问的同学,可以参考别人的源码解析,这里不再赘述

猜你喜欢

转载自blog.csdn.net/u011955067/article/details/79837731