SparseArray 与 HashMap

  之前我们分析过HashMap的源码,了解了它的扩容机制。不了解的朋友可以看一下我的另一篇文章:HashMap、HashTable与ConcurrentHashMap源码解析.HashMap总体来说还是很好用的,但是我们可以发现,当数量一达到扩容条件时就开始扩容,即使我们只需要使用一点点内存,这样就会使得内存大大的浪费,当容量越大时浪费的越明显。在android这种内存特别敏感的平台,我们应该尽量避免这样的事情发生。因此,google推出了更适合自己的api,SparseArray和ArrayMap。今天我们就来简单分析一下SparseArray的源码。

  首先我们看一下官方介绍:

/**
 * SparseArrays map integers to Objects.  Unlike a normal array of Objects,
 * there can be gaps in the indices.  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.
 *
 * 

Note that this container keeps its mappings in an array data structure, * using a binary search to find keys. The implementation is not intended to be appropriate for * data structures * that may contain large numbers of items. 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%.

* *

To help with performance, 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. This garbage collection will * need to be performed at any time the array needs to be grown or the the map size or * entry values are retrieved.

* *

It is possible to iterate over the items in this container using * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using * keyAt(int) with ascending values of the index will return the * keys in ascending order, or the values corresponding to the keys in ascending * order in the case of valueAt(int).

  简单翻译一下:大概意思是说,SparseArray比Hashmap内存使用效率更高,主要体现在两个方面,一方面它允许key的自动装箱,另一方面它不需依赖额外的Entry。需要注意的是,这个容器使得键值对一直保持数组的数据结构,并且用二分法查找key,所以它并不适合存储大量数据。SparseArray在增删改查的效率要比传统的Hashmap慢,因为它每次处理之前都需要进行二分法的查找。当数据量小于100的时候,这个性能差距不会很明显,会低于50%。为了提高性能,SparseArray在删除key的时候做了一项优化,他并不会立即将key从数组中删除,而是将其标记成删除状态,后续可以复用它,或者后续再将其移入一个专门收集的容器中,以便数组需要扩容时使用。

  废话不多说,我们先来看一下它的数据结构吧!

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

  正如注释当中说到的,SparseArray主要用两个数组分别来存储key和value,而且key只能为int。

  看完了数据结构,我们看一下增删改查。
  首先看增:增有两个方法:一个是append,另一个put;我们分别来看一下
  首先看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;
            }

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

  可以看出首先进行二分查找,判断key数组当中是否存在对应的key,如果存在命中if条件,直接将value值复制给对应索引位置。如果不存在则根据二分查找所返回的索引的位置判断对应位置的value是否是被标记是deleted,如果是则将key和value分别赋值给对应的索引位置。如果返回的索引位置比当前的size还要大,则判断是否有垃圾回收,如果有的话先进行gc,再二分法重新查找获得对应索引,最后将对应的key和value插入到对应的索引位置。

  再看append方法:

public void append(int key, E value) {
        if (mSize != 0 && key <= mKeys[mSize - 1]) {
            put(key, value);
            return;
        }

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

        mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
        mValues = GrowingArrayUtils.append(mValues, mSize, value);
        mSize++;
    }

  可以看出当size>0并且key小于最后一个元素的key时,则调用put方法。否则当之前还存在garbage并且size大于等于数组的长度的时候,先进行gc,然后再将对应的key,value追加到数组后面。那么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) {
                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);
    }

可以看出,其实就是将已经被标记为DELETE的对应索引位置的key,value重新赋值,也就是对数组进行了压缩。

看完了增,我们继续看删除方法:
删除系列方法一共有六个:

 public void remove(int key) {
        delete(key);
    }

public void removeAt(int index) {
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;
            mGarbage = true;
        }
    }

  public void removeAtRange(int index, int size) {
        final int end = Math.min(mSize, index + size);
        for (int i = index; i < end; i++) {
            removeAt(i);
        }
    }

public E removeReturnOld(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                final E old = (E) mValues[i];
                mValues[i] = DELETED;
                mGarbage = true;
                return old;
            }
        }
        return null;
    }

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

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

 public void clear() {
        int n = mSize;
        Object[] values = mValues;

        for (int i = 0; i < n; i++) {
            values[i] = null;
        }

        mSize = 0;
        mGarbage = false;
    }

  可以看出,其实大部分是相同的,主要就是将对应索引或者key对应value的值设为Delete。clear方法是将所有value的值清空并将size设置为0.

  再来看修改:setValueAt方法

public void setValueAt(int index, E value) {
        if (mGarbage) {
            gc();
        }

        mValues[index] = value;
    }

  很简单,就是判断是否需要gc,然后再对values对应的索引赋值。

  最后就是查询方法了:
  查询方法比较多:

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

  这两个方法主要是通过key来获取到value。

 */
    public int indexOfKey(int key) {
        if (mGarbage) {
            gc();
        }

        return ContainerHelpers.binarySearch(mKeys, mSize, key);
    }

    public int indexOfValue(E value) {
        if (mGarbage) {
            gc();
        }

        for (int i = 0; i < mSize; i++) {
            if (mValues[i] == value) {
                return i;
            }
        }

        return -1;
    }

 
    public int indexOfValueByValue(E value) {
        if (mGarbage) {
            gc();
        }

        for (int i = 0; i < mSize; i++) {
            if (value == null) {
                if (mValues[i] == null) {
                    return i;
                }
            } else {
                if (value.equals(mValues[i])) {
                    return i;
                }
            }
        }
        return -1;
    }

  第一个方法是获取key对应的索引,后两个方法是获取到对应value的索引,两者的不同在于一个是用equals判断另一个是通过==来判断。

  还有最后两个方法:keyAt和valueAt

public int keyAt(int index) {
        if (mGarbage) {
            gc();
        }

        return mKeys[index];
    }
 public E valueAt(int index) {
        if (mGarbage) {
            gc();
        }

        return (E) mValues[index];
    }

  分别返回索引所对应的key和value。

  总的看起来,SparseArray要比HashMap的数据结构简单的多。没有hashmap的node结点,也不需要迭代器来遍历。二分法查找在查找上更有优势。缺点就是它的key只支持int类型。显然在我们实际开发当中是不够的,因此也就有了ArrayMap了,ArrayMap的源码我们就不再分析了。实际上它的用法以及对数据的存储都和SparseArray一样,都是由两个数组来完成的,并且也是基于二分查找。不同的地方在于,ArrayMap的key可以是任意类型的。

你可能感兴趣的:(SparseArray 与 HashMap)