Android内存优化之取代HashMap(SparseArray和ArrayMap解析)

看过了内存优化的文章,了解到在Android里为了节约内存,应该尽量避免使用HashMap.
今天的主角是SparseArray和ArrayMap.下面我打算从源码的角度去分析为什么我们要取代HashMap.主要是从三个方法put(),remove(),get()进行分析.

1.SpareArray:

SparseArray仅使用于用int值做key的键值对,他主要是通过两个数组去存储数据

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

mKeys[]用于存储key,mValues[]用于存储对应的value.
首先看下他的put方法

    public void put(int key, E value) {
        //通过二分法找到下标
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //如果已存在就直接赋值取代
        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i;
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }

            //省略清理数组的代码

            mKeys[i] = key;
            mValues[i] = 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];
        }
    }

用二分查找法找下标,没什么好说的

看看remove()方法,直接调用了delete():

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

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

删除的时候用一个叫DELETED的Object对象替换原对象

private static final Object DELETED = new Object();

然后会通过清理数组的代码去整合数组,代码如下

    private void gc() {
        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) {
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
                o++;
            }
        }

        mGarbage = false;
        mSize = o;
    }

SparseArray总结一下,他的核心使用二分算法去查找,效率很高,需要的内存也不多,就两个数组,因此这个是最节约内存的(后面会有测试数据).

2.ArrayMap

如果key不能是int的,那就可以考虑用ArrayMap去存储键值对
put()方法:

    public V put(K key, V value) {
        final int hash;
        int index;
        if (key == null) {
            hash = 0;
            index = indexOfNull();
        } else {
        //查找index里面是用二分查找法
            hash = key.hashCode();
            index = indexOf(key, hash);
        }
        //
        if (index >= 0) {
            index = (index<<1) + 1;
            final V old = (V)mArray[index];
            mArray[index] = value;
            return old;
        }

        //省略清理数组的代码

        mHashes[index] = hash;
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;
        mSize++;
        return null;
    }

可以看出ArrayMap主要由两个数组来存储键值对,分别是mHashes[]用存储key对应的hashcode,mArray[]用来存储key和value(通过移位操作找到下标,由于要存储两个东西,所以就让index左移一位,就是乘以2嘛,从而达到用一个数组同时存储key和value).
下面是get()方法:

    public V get(Object key) {
        final int index = key == null ? indexOfNull() : indexOf(key, key.hashCode());
        return index >= 0 ? (V)mArray[(index<<1)+1] : null;
    }

用二分算法找到下标然后找出值,很简单.

remove()同理,用二分算法找到index然后移除对应的值,代码如下

    public V remove(Object key) {
        int index = key == null ? indexOfNull() : indexOf(key, key.hashCode());
        if (index >= 0) {
            return removeAt(index);
        }
        return null;
    }

总结ArrayMap,他也是用了二分算法(谷歌为什么这么喜欢二分算法),存数据用了两个数组

     int[] mHashes;
    Object[] mArray;

效率应该跟SparseArray比较接近,但他存储了3个东西,一个是key,一个是key对应的hashcode,还有一个value,所以内存占用应该会比SparseArray多点.

3.HashMap:

一张图看懂HashMap
HashMap
HashMap其实就是一个数组,这个数组存储着HashMapEntry对象,而HashMapEntry是个链表结构,所以HashMap其实是数组和链表的结合体.
HashMap的成员主要有:

//存储空key的HashMapEntry
transient HashMapEntry<K, V> entryForNullKey;
//核心数组,用于存储对应的HashMapEntry链表
transient HashMapEntry<K, V>[] table;
//用来存储key的集合
private transient Set<K> keySet;
//存储Entry的集合
private transient Set<Entry<K, V>> entrySet;
//存储值的集合
private transient Collection<V> values;

可以看出HashMap的东西还挺多的.

来看put()方法:

        @Override 
        public V put(K key, V value) {
        if (key == null) {
        //把空的key用专门的一个链表去存储
            return putValueForNullKey(value);
        }
        //得到转换后的hash值
        int hash = secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        //找到index后对对应的HashMapEntry做遍历如果已经存在就返回
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

addNewEntry():

table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);

在看看get方法():

    public V get(Object key) {
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            return e == null ? null : e.value;
        }

        int hash = key.hashCode();
        hash ^= (hash >>> 20) ^ (hash >>> 12);
        hash ^= (hash >>> 7) ^ (hash >>> 4);

        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }

get()方法就是通过key的hash值拿到index,然后遍历那个位置的HashMapEntry.

最后看看remove方法():

        @Override
        public V remove(Object key) {
        if (key == null) {
            //删除空key的方法
            return removeNullKey();
        }
        int hash = secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        //同样是通过key的hash值找出index,然后遍历那个位置的HashMapEntry
        for (HashMapEntry<K, V> e = tab[index], prev = null;
                e != null; prev = e, e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                if (prev == null) {
                    tab[index] = e.next;
                } else {
                    prev.next = e.next;
                }
                modCount++;
                size--;
                postRemove(e);
                return e.value;
            }
        }
        return null;
    }

4.总结

分析完了3个类,我们总结下为什么HashMap比较耗内存.
对比下就会发现,HashMap里面用于存储数据的数组是最多的.其次,性能优化的文章(传送门:http://hukai.me/android-performance-patterns-season-3/)说HashMap会做自动装箱(AutoBoxing boolean,int,float等转换成Boolean,Integer,Float等对象)处理.而SparseArray和ArrayMap都能避免AutoBoxing从而达到节约内存.那篇文章还说,满足两个条件才考虑用ArrayMap:
1.对象个数的数量级最好是千以内
2.数据组织形式包含Map结构
原因是超过一定数量ArrayMap插入和删除效率不高.

光瞎说没有说服力,我们用实际数据测下他们所占的内存大小
首先我们分别为三个类创建一个对象,然后向他们添加1000个Object对象

     for (int i = 0 ; i < 10000 ; i++){
            mHashMap.put(i,new Object());
        }

        for (int i = 0 ; i < 10000 ; i++){
            mArrayMap.put(i,new Object());
        }

        for (int i = 0 ; i < 10000 ; i++){
            mSparseArray.put(i,new Object());
        }

然后用Android Studio自带的内存分析工具看下他们所占内存大小
图1是加1000个的大小,图2是10000个数据的大小
图1

图2

可以看出HashMap,ArrayMap,SparseArray所占内存大小为5:3:2左右.
所以以后我们应该尽量考虑内存优化根据情况尽量不要使用HashMap了.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值