Android中HashMap内存优化之ArrayMap和SparseArray

本文探讨了在Android中,当面临内存优化时,如何选择使用HashMap、ArrayMap和SparseArray。文章指出,HashMap在元素较少时可能导致内存浪费,因为其自动装箱产生额外对象。相比之下,SparseArray避免了key的自动装箱,适用于key为int类型且数据量较小的情况。此外,文章还介绍了LongSparseArray等特殊类型的SparseArray,以及ArrayMap的内部实现和优势,如减少了HashMapEntry对象的创建。
摘要由CSDN通过智能技术生成
ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的。用于在一定情况下取代HashMap而达到节省内存的目的。

在Android开发中HashMap使用频率相当高,回顾一下 HashMap的结构,其是以array存储链表的头结点,找到头结点后在进行遍历查找,如下图:


时间效率方面,利用hash算法,插入和查找等操作都很快,且一般情况下,每一个数组值后面不会存在很长的链表(因为出现hash冲突毕竟占比较小的比例),所以不考虑空间利用率的话,HashMap的效率非常高。

但是HashMap 有如下特点:

1: HashMap中默认的存储大小是一个容量为16的数组,所以当我们创建出一个HashMap对象时,即使里面没有任何元素,也要分别一块内存空间给它,而且当我们不断的向HashMap里put数据时,达到一定的容量限制时HashMap的空间将会扩大,从图示中可以看出当HashMap扩容容量过多,元素较少时会产生内存使用不平衡,即浪费不少内存。

2: 还有使用HashMap会涉及一个要求:key与value必须为对象类型,而不能为基本类型, 这就导致了本可以使用基本类型的数据必须转换为其对象包装类型(int->Integer,long->Long......)这就涉及到需要占用更多内存以及拆箱装箱频繁转换问题。
在jvm中一个Integer类型占用内存大小 > 12byte, 一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。 Object ob = new Object(); 这样在程序中完成了一个空Java对象的声明,但是它所占的空间为:4byte+8byte,4byte是Java statck中保存引用所需要的空间,而8byte则是Java heap中对象的信息。 所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。 包装类型已经成为对象类型,因此需要按照对象类型的大小计算至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,Java进行对对象的内存分配时其大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。

3:HashMap本质上是一个HashMapEntry构成的数组。每个HashMapEntry的实体都包括:
  • 一个非基本类型的key
  • 一个非基本类型的value
  • 一个key的Hashcode
  • 指向下一个Entry的指针
            final K key;
            V value;
            HashMapEntry<K,V> next;
            int hash;
所以总结下HashMap的缺点:
  • 当HashMap扩容容量过多,元素较少时会产生内存使用不平衡,即浪费不少内存
  • 基本数据类型自动装箱意味着需要产生额外的对象。
  • HashMapEntry对象的创建,并且HashMapEntry自己本身也会产生额外的对象。
  • 哈希是很好的实现方案,但是如果实现的不好将会让我们的开销回到O(N)


所以下面开始介绍Android为我们提供的各种优化的类: ArrayMap, SimpleArrayMap SparseArray, LongSparseArray, SparseIntArray, SparseLongArray,SparseBooleanArray。

先从如何使用它们开始吧:

java.util.HashMap<String, String> hashMap = new java.util.HashMap<String, String>(16);
hashMap.put("key", "value");
hashMap.get("key");
hashMap.entrySet().iterator();

android.util.ArrayMap<String, String> arrayMap = new android.util.ArrayMap<String, String>(16);
arrayMap.put("key", "value");
arrayMap.get("key");
arrayMap.entrySet().iterator();

android.support.v4.util.ArrayMap<String, String> supportArrayMap =
        new android.support.v4.util.ArrayMap<String, String>(16);
supportArrayMap.put("key", "value");
supportArrayMap.get("key");
supportArrayMap.entrySet().iterator();

android.support.v4.util.SimpleArrayMap<String, String> simpleArrayMap =
        new android.support.v4.util.SimpleArrayMap<String, String>(16);
simpleArrayMap.put("key", "value");
simpleArrayMap.get("key");
//simpleArrayMap.entrySet().iterator();      <- will not compile

android.util.SparseArray<String> sparseArray = new android.util.SparseArray<String>(16);
sparseArray.put(10, "value");
sparseArray.get(10);

android.util.LongSparseArray<String> longSparseArray = new android.util.LongSparseArray<String>(16);
longSparseArray.put(10L, "value");
longSparseArray.get(10L);

android.util.SparseLongArray sparseLongArray = new android.util.SparseLongArray(16);
sparseLongArray.put(10, 100L);
sparseLongArray.get(10);



SparseArray

先看看 SparseArray的结构如下图:


SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型),还避免了HashMapEntry对象的创建,它内部则是通过两个数组来进行数据存储的,一个存储key(有序),另外一个存储value,为了优化性能,对key使用了二分查找的方法,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间,我们从源码中可以看到key和value分别是用数组表示:
    private int [] mKeys;
    private Object[] mValues;
因为key为int也就不需要什么hash值了,只要int值相等,那就是同一个对象,简单粗暴。
空间上对比:与HashMap,去掉了Hash值的存储空间,没有next的指针占用,还有 HashMapEntry 的内存占用。
时间上对比:因为需要有序,可能存在大量的数组搬移。但是它避免了装箱的环节,装箱过程还是很费时的。

  • 它里面也用了两个数组。一个int[] mKeys和Object[] mValues。从名字都可以看得出来一个用来存储key一个用来保存value的。
  • key(不是它的hashcode)保存在mKeys[]的下一个可用的位置上。所以不会再对key自动装箱了。
  • value保存在mValues[]的下一个位置上,value还是要自动装箱的,如果它是基本类型。
  • 查找key还是用的二分法查找。也就是说它的时间复杂度还是O(logN)
  • 知道了key的index,也就可以用key的index来从mValues中检索出value。
SparseArray只能存储key为int类型的数据,同时SparseArray在存储和读取数据时候,对key使用的是二分查找法,所以key存储是有序的并不是按插入的顺序,可以看看:
public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        ...
        }
public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        ...
        }
也就是在put添加数据的时候,会使用二分查找法查找添加的元素的key,然后按照从小到大的顺序排列好,所以,SparseArray存储的元素都是按元素的key值从小到大排列好的。 而在获取数据的时候,也是使用二分查找法判断元素的位置,所以,在获取数据的时候非常快。

满足下面两个条件我们可以使用SparseArray代替HashMap:
  • 数据量不大,最好在千级以内
  • key必须为int类型

LongSparseArray,SparseIntArray,SparseLongArray,SparseBooleanArray

SparseArray只接受int类型作为key,而LongSparseArray可以用long作为key,实现原理和SparseArray一致。
其次对于key是int类型而value也是基本数据类型,我们可以对应使用SparseIntArray,SparseLongArray ,SparseBooleanArray 。它们使用方式是和SparseArray一样的。它的好处是mValues[]是基本类型的数组。也就意味着无论是key还是value都不用装箱。并且相对于HashMap来说我们节约了3个对象的初始化(Entry,Key和Value),但是我们将查询复杂度从O(1)上升到了O(logN)

SparseArray实现
  1. public class SparseArray<E> implements Cloneable {  
  2.     private static final Object DELETED = new Object();  
  3.     private boolean mGarbage = false;  
  4.   
  5.     private int[] mKeys;//使用array存储int key  
  6.     private Object[] mValues;//使用array存储泛型Value  
  7.     private int mSize;  
  8.   
  9.     /** 
  10.      * Creates a new SparseArray containing no mappings. 
  11.      */  
  12.     public SparseArray() {  
  13.         this(10);//默认容量10  
  14.     }  
  15.   
  16.     public SparseArray(int initialCapacity) {  
  17.         if (initialCapacity == 0) {  
  18.             mKeys = EmptyArray.INT;  
  19.             mValues = EmptyArray.OBJECT;  
  20.         } else {  
  21.             mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);  
  22.             mKeys = new int[mValues.length];  
  23.         }  
  24.         mSize = 0;  
  25.     }  
  26.       
  27.         /** 
  28.      * Gets the Object mapped from the specified key, or <code>null</code> 
  29.      * if no such mapping has been made. 
  30.      */  
  31.     public E get(int key) {  
  32.         return get(key, null);  
  33.     }  
  34.   
  35.     /** 
  36.      * Gets the Object mapped from the specified key, or the specified Object 
  37.      * if no such mapping has been made. 
  38.      */  
  39.     @SuppressWarnings("unchecked")  
  40.     public E get(int key, E valueIfKeyNotFound) {  
  41.         int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//使用二分查找查找key  
  42.   
  43.         if (i < 0 || mValues[i] == DELETED) { //没有找到key或者Value已经被deleted  
  44.             return valueIfKeyNotFound;  
  45.         } else {  
  46.             return (E) mValues[i];  
  47.         }  
  48.     }  
  49.   
  50.      public void put(int key, E value) {  
  51.         int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//二分查找key是否存在,如果没有找到时,这里返回的是-low,也就是待插入位置取反  
  52.   
  53.         if (i >= 0) { //存在直接替换value  
  54.             mValues[i] = value;  
  55.         } else {  
  56.             i = ~i;//待插入位置  
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值