• 安卓性能优化—使用ArrayMap与SparseArray


    性能优化是我们做开发的必须要熟练掌握的技能,所以我打算写一个性能优化专题,把平时用到的一些优化方法记录下来,以便忘记的时候可以快速查找,同时也给给其他开发者提供微薄之力吧:

    这篇文章讲述的是在一些特定的场景使用使用ArrayMap与SparseArray代替HashMap,提高对数据的操作;
    先看看官方文档的描述:

    1. ArrayMap is a generic key->value mapping data structure that is designed to be more memory efficient than a traditional HashMap, this implementation is a version of the platform's ArrayMap that can be used on older versions of the platform.
    2. 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.
    3. Note that for containers holding up to hundreds of items, the performance difference is not significant, less than 50%.
    • 1

    从官方文档可以看出使用ArrayMap与SparseArray都要比传统的HashMap 更有效率;但是最后有一个note,也就是当数据量达到千级以上的时候,ArrayMap与SparseArray都要比传统的HashMap 效率更低50%;

    HashMap
    HashMap允许使用 null 值和 null 键,是基于hashing原理,我们通过put()和get()方法储存和获取对象。
    HashMap的结构:

    HashMap 有两个参数影响其性能:初始容量 和加载因子。容量是哈希表中桶的数量(默认16组),初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。加载因子默认值为0.75 。
    当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用LinkedList来解决碰撞问题,当发生碰撞了,对象将会储存在LinkedList的下一个节点中。 HashMap在每个LinkedList节点中储存键值对对象。
    put()方法

    1. @Override public V put(K key, V value) {
    2. if (key == null) {//允许空值
    3. return putValueForNullKey(value);
    4. }
    5. //使得hash过后的值的分布更加均匀,尽可能地避免冲突
    6. int hash = Collections.secondaryHash(key);
    7. HashMapEntry<K, V>[] tab = table;
    8. //当tab.length2^n-1的时候,可以保证结果不大于tab.length
    9. int index = hash & (tab.length - 1);
    10. for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
    11. if (e.hash == hash && key.equals(e.key)) {
    12. preModify(e);
    13. V oldValue = e.value;
    14. e.value = value;
    15. return oldValue;
    16. }
    17. }
    18. // No entry for (non-null) key is present; create one
    19. modCount++;
    20. //扩容
    21. if (size++ > threshold) {
    22. tab = doubleCapacity();
    23. index = hash & (tab.length - 1);
    24. }
    25. addNewEntry(key, value, hash, index);
    26. return null;
    27. }
    • 1

    当两个Key同时hash到一个值时,就会出现这样的冲突。这个冲突主要有2种解决方法。

    1. 1、开放地址,亦即如果hash冲突,则在空闲的位置进行插入
    2. 2、hash复用,同一个hash值,链式地加入多个value
    • 1

    get()方法

    1. public V get(Object key) {
    2. if (key == null) {//允许value为空
    3. HashMapEntry<K, V> e = entryForNullKey;
    4. return e == null ? null : e.value;
    5. }
    6. //使得hash过后的值的分布更加均匀,尽可能地避免冲突
    7. int hash = Collections.secondaryHash(key);
    8. HashMapEntry<K, V>[] tab = table;
    9. for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
    10. e != null; e = e.next) {
    11. K eKey = e.key;
    12. if (eKey == key || (e.hash == hash && key.equals(eKey))) {
    13. return e.value;
    14. }
    15. }
    16. return null;
    17. }
    • 1

    HashMap还有很多方法,这里就不一一例举了;

    通过get与put的源码,可以看出HashMap获取数据是通过遍历Entry[]数组来得到对应的元素,在数据量很大时候会比较慢,所以在Android中,HashMap是比较费内存的,我们在一些情况下可以使用SparseArray和ArrayMap来代替HashMap。
    SparseArray

    1. /**
    2. * A copy of the current platform (currently {@link android.os.Build.VERSION_CODES#KITKAT}
    3. * version of {@link android.util.SparseArray}; provides a removeAt() method and other things.
    4. */
    5. public class SparseArrayCompat<E> implements Cloneable {
    6. private static final Object DELETED = new Object();
    7. private boolean mGarbage = false;
    8. private int[] mKeys;
    9. private Object[] mValues;
    10. private int mSize;
    • 1

    可以看出SparseArray由两个数组mKeys和mValues存放数据;其中key的类型为int型,这就显得SparseArray比HashMap更省内存一些,SparseArray在存储和读取数据时候,使用的是二分查找法,那何为二分法呢?
    先看一下put()方法:

    1. public void put(int key, E value) {
    2. int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    3. if (i >= 0) {
    4. mValues[i] = value;
    5. } else {
    6. i = ~i;
    7. if (i < mSize && mValues[i] == DELETED) {
    8. mKeys[i] = key;
    9. mValues[i] = value;
    10. return;
    11. }
    12. if (mGarbage && mSize >= mKeys.length) {
    13. gc();
    14. // Search again because indices may have changed.
    15. i = ~ ContainerHelpers.binarySearch(mKeys, mSize, key);
    16. }
    17. //如果容量不够
    18. if (mSize >= mKeys.length) {
    19. int n = ContainerHelpers.idealIntArraySize(mSize + 1);
    20. int[] nkeys = new int[n];
    21. Object[] nvalues = new Object[n];
    22. // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
    23. //特别注意这,是copy,而不是new,效率提升
    24. System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
    25. System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
    26. mKeys = nkeys;
    27. mValues = nvalues;
    28. }
    29. if (mSize - i != 0) {
    30. // Log.e("SparseArray", "move " + (mSize - i));
    31. System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
    32. System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
    33. }
    34. mKeys[i] = key;
    35. mValues[i] = value;
    36. mSize++;
    37. }
    38. }
    • 1

    在上面代码中有一个ContainerHelpers.binarySearch(mKeys, mSize, key)方法,这是Arrays提供了一个方便查询的方法,也就是我们所说的“二分法”;

    1. // This is Arrays.binarySearch(), but doesn't do any argument validation.
    2. static int binarySearch(int[] array, int size, int value) {
    3. int lo = 0;
    4. int hi = size - 1;
    5. while (lo <= hi) {
    6. int mid = (lo + hi) >>> 1;//无符号右移
    7. int midVal = array[mid];
    8. if (midVal < value) {
    9. lo = mid + 1;
    10. } else if (midVal > value) {
    11. hi = mid - 1;
    12. } else {
    13. return mid; // value found
    14. }
    15. }
    16. return ~lo; // value not present
    17. }
    • 1

    get()方法:

    1. public E get(int key, E valueIfKeyNotFound) {
    2. //二分法
    3. int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    4. if (i < 0 || mValues[i] == DELETED) {
    5. return valueIfKeyNotFound;
    6. } else {
    7. return (E) mValues[i];
    8. }
    9. }
    • 1

    使用二分查找法和之前的key比较当前我们添加的元素的key的大小,然后按照从小到大的顺序排列好,所以,SparseArray存储的元素都是按元素的key值从小到大排列好的。
    而在获取数据的时候,也是使用二分查找法判断元素的位置,所以,在获取数据的时候非常快,比HashMap快的多,因为HashMap获取数据是通过遍历Entry[]数组来得到对应的元素。
    其他的一些方法:
    转存失败重新上传取消
    ArrayMap
    ArrayMap是一个键值对映射的数据结构,它设计上更多的是考虑内存的优化,内部是使用两个数组进行数据存储,一个数组记录key的hash值,另外一个数组记录Value值,它和SparseArray一样,也会对key使用二分法进行从小到大排序,区别是ArrayMap的key是hash值,

    1. public class ArrayMap, V> extends SimpleArrayMap, V> implements Map, V> {
    2. MapCollections<K, V> mCollections;
    3. public ArrayMap() {
    4. super();
    5. }
    • 1

    可以看出ArrayMap的构造方法直接调用的父类的构造方法:

    1. int[] mHashes;
    2. Object[] mArray;
    3. public SimpleArrayMap() {
    4. mHashes = ContainerHelpers.EMPTY_INTS;
    5. mArray = ContainerHelpers.EMPTY_OBJECTS;
    6. mSize = 0;
    7. }
    • 1

    put()方法:

    1. public V put(K key, V value) {
    2. final int hash;
    3. int index;
    4. if (key == null) {
    5. hash = 0;
    6. index = indexOfNull();
    7. } else {
    8. hash = key.hashCode();
    9. index = indexOf(key, hash);
    10. }
    11. if (index >= 0) {
    12. index = (index<<1) + 1;
    13. final V old = (V)mArray[index];
    14. mArray[index] = value;
    15. return old;
    16. }
    17. index = ~index;
    18. if (mSize >= mHashes.length) {
    19. final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
    20. : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
    21. if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
    22. final int[] ohashes = mHashes;
    23. final Object[] oarray = mArray;
    24. allocArrays(n);
    25. if (mHashes.length > 0) {
    26. if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
    27. System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
    28. System.arraycopy(oarray, 0, mArray, 0, oarray.length);
    29. }
    30. //释放资源
    31. freeArrays(ohashes, oarray, mSize);
    32. }
    33. if (index < mSize) {
    34. if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
    35. + " to " + (index+1));
    36. System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
    37. System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
    38. }
    39. mHashes[index] = hash;
    40. mArray[index<<1] = key;
    41. mArray[(index<<1)+1] = value;
    42. mSize++;
    43. return null;
    44. }
  • 相关阅读:
    JavaScript——关于JavaScript、在HTML中嵌入JS代码的三种方式、变量
    HTML与CSS
    网络安全--对称加密
    vue3状态管理工具pinia的插件书写,pinia全局错误处理插件安排
    Spring(存储Bean对象五大类注解,获取Bean对象三种注入方式)
    已知三角形三条边求面积
    负载均衡原理及算法
    spring源码-spring启动(待完善)
    使用重建大师进行重建时,为什么引擎信息中显示只有一台主机能运行?
    HDMI 输出实验
  • 原文地址:https://blog.csdn.net/cqn2bd2b/article/details/127129702