码迷,mamicode.com
首页 > 移动开发 > 详细

关于Android中SparseArray比HashMap性能好的深入研究

时间:2015-06-02 23:23:18      阅读:248      评论:0      收藏:0      [点我收藏+]

标签:

由于网上有朋友对于这个问题已经有了很详细的研究,所以我就不班门弄斧了:

转载于:http://android-performance.com/android/2014/02/10/android-sparsearray-vs-hashmap.html

    http://liuzhichao.com/p/832.html

SparseArray是Android框架独有的类,在标准的JDK中不存在这个类。它要比 HashMap 节省内存,某些情况下比HashMap性能更好,按照官方问答的解释,主要是因为SparseArray不需要对key和value进行auto-boxing(将原始类型封装为对象类型,比如把int类型封装成Integer类型),结构比HashMap简单(SparseArray内部主要使用两个一维数组来保存数据,一个用来存key,一个用来存value)不需要额外的额外的数据结构(主要是针对HashMap中的HashMapEntry而言的)。是骡子是马得拉出来遛遛,下面我们就通过几段程序来证明SparseArray在各方面表现如何,下面的试验结果时在我的Hike X1(Android 4.2.2)手机上运行得出的。

代码1:

int MAX = 100000;
long start = System.currentTimeMillis();
HashMap<Integer, String> hash = new HashMap<Integer, String>();
for (int i = 0; i < MAX; i++) {
    hash.put(i, String.valueOf(i));
}
long ts = System.currentTimeMillis() - start;

代码2:

int MAX = 100000;
long start = System.currentTimeMillis();
SparseArray<String> sparse = new SparseArray<String>();
for (int i = 0; i < MAX; i++) {
    sparse.put(i, String.valueOf(i));
}
long ts = System.currentTimeMillis() - start;

我们分别在long start处和long ts处设置断点,然后通过DDMS工具查看内存使用情况。

代码1中,我们使用HashMap来创建100000条数据,开始创建前的系统内存情况为: 技术分享

创建HashMap之后,应用内存情况为: 技术分享可见创建HashMap用去约 13.2M内存。

再看 代码2,同样是创建100000条数据,我们用SparseArray来试试,开始创建前的内存使用情况为: 技术分享

创建SparseArray之后的情况: 技术分享创建SparseArray共用去 8.626M内存。

可见使用 SparseArray 的确比 HashMap 节省内存,大概节省 35%左右的内存。


我们再比较一下插入数据的效率如何,我们在加两段代码(主要就是把插入顺序变换一下,从大到小插入):

代码3:

int MAX = 100000;
long start = System.currentTimeMillis();
HashMap<Integer, String> hash = new HashMap<Integer, String>();
for (int i = 0; i < MAX; i++) {
    hash.put(MAX - i -1, String.valueOf(i));
}
long ts = System.currentTimeMillis() - start;

代码4:

int MAX = 100000;
long start = System.currentTimeMillis();
SparseArray<String> sparse = new SparseArray<String>();
for (int i = 0; i < MAX; i++) {
    sparse.put(MAX - i -1, String.valueOf(i));
}
long ts = System.currentTimeMillis() - start;

我们分别把这4代码分别运行5次,对比一下ts的时间(单位毫秒):

#代码1代码2代码3代码4
1 10750ms 7429ms 10862ms 90527ms
2 10718ms 7386ms 10711ms 87990ms
3 10816ms 7462ms 11033ms 88259ms
4 10943ms 7386ms 10854ms 88474ms
5 10671ms 7317ms 10786ms 90630ms

通过结果我们看出,在正序插入数据时候,SparseArray比HashMap要快一些;HashMap不管是倒序还是正序开销几乎是一样的;但是SparseArray的倒序插入要比正序插入要慢10倍以上,这时为什么呢?我们再看下面一段代码:

代码5:

SparseArray<String> sparse = new SparseArray<String>(3);

sparse.put(1, "s1");
sparse.put(3, "s3");
sparse.put(2, "s2");

我们在Eclipse的debug模式中,看Variables窗口,如图: 技术分享

及时我们是按照1,3,2的顺序排列的,但是在SparseArray内部还是按照正序排列的,这时因为SparseArray在检索数据的时候使用的是二分查找,所以每次插入新数据的时候SparseArray都需要重新排序,所以代码4中,逆序是最差情况。


下面我们在简单看下检索情况:

代码5:

long start4search = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
    hash.get(33333); //针对固定值检索
}
long end4search = System.currentTimeMillis() - start4search;

代码6:

long start4search = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
    hash.get(i); //顺序检索
}
long end4search = System.currentTimeMillis() - start4search;

代码7:

long start4search = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
    sparse.get(33333); //针对固定值检索
}
long end4search = System.currentTimeMillis() - start4search;

代码8:

long start4search = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
    sparse.get(i); //顺序检索
}
long end4search = System.currentTimeMillis() - start4search;

表1:

#代码5代码6代码7代码8
1 4072ms 4318ms 3442ms 3390ms
2 4349ms 4536ms 3402ms 3420ms
3 4599ms 4203ms 3472ms 3376ms
4 4149ms 4086ms 3429ms 3786ms
5 4207ms 4219ms 3439ms 3376ms

代码9,我们试一些离散的数据。

//使用Foo为了避免由原始类型被自动封装(auto-boxing,比如把int类型自动转存Integer对象类型)造成的干扰。
class FOO{
    Integer objKey;
    int intKey;
}
...
int MAX = 100000;

HashMap<Integer, String> hash = new HashMap<Integer, String>();
SparseArray<String> sparse = new SparseArray<String>();

for (int i = 0; i < MAX; i++) {
    hash.put(i, String.valueOf(i));
    sparse.put(i, String.valueOf(i));
}

List<FOO> keylist4search = new ArrayList<FOO>();
for (int i = 0; i < MAX; i++) {
    FOO f = new FOO();
    f.intKey = i;
    f.objKey = Integer.valueOf(i);
    keylist4search.add(f);
}

long start4search = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
    hash.get(keylist4search.get(i).objKey);
}
long end4searchHash = System.currentTimeMillis() - start4search;

long start4search2 = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
    sparse.get(keylist4search.get(i).intKey);
}
long end4searchSparse = System.currentTimeMillis() - start4search2;

代码9,运行5次之后的结果如下:

表2:

#end4searchHashend4searchSparse
1 2402ms 4577ms
2 2249ms 4188ms
3 2649ms 4821ms
4 2404ms 4598ms
5 2413ms 4547ms

从上面两个表中我们可以看出,当SparseArray中存在需要检索的下标时,SparseArray的性能略胜一筹(表1)。但是当检索的下标比较离散时,SparseArray需要使用多次二分检索,性能显然比hash检索方式要慢一些了(表2),但是按照官方文档的说法性能差异不是很大,不超过50%( For containers holding up to hundreds of items, the performance difference is not significant, less than 50%.)

总体而言,在Android这种内存比CPU更金贵的系统中,能经济地使用内存还是上策,何况SparseArray在其他方面的表现也不算差(另外,SparseArray删除数据的时候也做了优化——使用了延迟整理数组的方法,可参考官方文档SparseArray,读者可以自行把代码9中的hash.getsparse.get改成hash.removesparse.delete试试,你会发现二者的性能相差无几)。而且,使用SparseArray代替HashMap<integer,e>也是官方推荐的做法,在Eclipse中也会提示你优先使用SparseArray,如图: 技术分享

另外,我们还可以用 LongSparseArray来替代HashMap<long,e>。SparseBooleanArray来替代HashMap<integer,boolean>。

=========================================================================================

HashMap是java里比较常用的一个集合类,我比较习惯用来缓存一些处理后的结果。最近在做一个Android项目,在代码中定义这样一个变量,实例化时,Eclipse却给出了一个 performance 警告。

技术分享

 

意思就是说用SparseArray<E>来替代,以获取更好性能。老实说,对SparseArray并不熟悉,第一感觉应该是Android提供的一个类。按住Ctrl点击进入SparseArray的源码,果不其然,确定是Android提供的一个工具类。

技术分享

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

假设有一个9*7的数组,其内容如下:

 

技术分享

 

在此数组中,共有63个空间,但却只使用了5个元素,造成58个元素空间的浪费。以下我们就使用稀疏数组重新来定义这个数组:

 

技术分享

 

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

 

继续阅读SparseArray的源码,从构造方法我们可以看出,它和一般的List一样,可以预先设置容器大小,默认的大小是10:

   
    public SparseArray() {
        this(10);
    }

    public SparseArray(int initialCapacity) {
        initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);

        mKeys = new int[initialCapacity];
        mValues = new Object[initialCapacity];
        mSize = 0;
    }

再来看看它对数据的“增删改查”。

它有两个方法可以添加键值对:

 public void put(int key, E value) {}
 public void append(int key, E value){}

有四个方法可以执行删除操作:

 public void delete(int key) {}
 public void remove(int key) {} //直接调用的delete(int key)
 public void removeAt(int index){}
 public void clear(){}

修改数据起初以为只有setValueAt(int index, E value)可以修改数据,但后来发现put(int key, E value)也可以修改数据,我们查看put(int key, E value)的源码可知,在put数据之前,会先查找要put的数据是否已经存在,如果存在就是修改,不存在就添加。

    public void put(int key, E value) {
        int i = binarySearch(mKeys, 0, mSize, key);

        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i;

            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }

            if (mGarbage && mSize >= mKeys.length) {
                gc();

                // Search again because indices may have changed.
                i = ~binarySearch(mKeys, 0, mSize, key);
            }
            …………

所以,修改数据实际也有两种方法:

 public void put(int key, E value)
 public void setValueAt(int index, E value)

最后再来看看如何查找数据。有两个方法可以查询取值:

 public E get(int key)
 public E get(int key, E valueIfKeyNotFound)

其中get(int key)也只是调用了 get(int key,E valueIfKeyNotFound),最后一个从传参的变量名就能看出,传入的是找不到的时候返回的值.get(int key)当找不到的时候,默认返回null。

查看第几个位置的键:

 public int keyAt(int index)

有一点需要注意的是,查看键所在位置,由于是采用二分法查找键的位置,所以找不到时返回小于0的数值,而不是返回-1。返回的负值是表示它在找不到时所在的位置。

查看第几个位置的值:

 public E valueAt(int index)

查看值所在位置,没有的话返回-1:

 public int indexOfValue(E value)

最后,发现其核心就是折半查找函数(binarySearch),算法设计的很不错。

    private static int binarySearch(int[] a, int start, int len, int key) {
        int high = start + len, low = start - 1, guess;

        while (high - low > 1) {
            guess = (high + low) / 2;

            if (a[guess] < key)
                low = guess;
            else
                high = guess;
        }

        if (high == start + len)
            return ~(start + len);
        else if (a[high] == key)
            return high;
        else
            return ~high;
    }

相应的也有SparseBooleanArray,用来取代HashMap<Integer, Boolean>,SparseIntArray用来取代HashMap<Integer, Integer>,大家有兴趣的可以研究。

总结:SparseArray是android里为<Interger,Object>这样的Hashmap而专门写的类,目的是提高效率,其核心是折半查找函数(binarySearch)。在Android中,当我们需要定义

HashMap<Integer, E> hashMap = new HashMap<Integer, E>();

时,我们可以使用如下的方式来取得更好的性能.

SparseArray<E> sparseArray = new SparseArray<E>();

关于Android中SparseArray比HashMap性能好的深入研究

标签:

原文地址:http://www.cnblogs.com/CoolRandy/p/4547904.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!