SparseArray源码解读

前言

当我们在安卓开发过程中需要使用到键值对映射的时候,自然而然的会想到使用HashMap,但是当我们指定key的泛型为Int型时,IDE却会提示我们使用SparseArray来替换HashMap,这是为什么呢?难道SparseArray相较于HashMap有什么优势吗?带着疑问我们去探寻一下SparseArray的源码。

官方简介

SparseArray类位于android.util这个包下,官方对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.

翻译过来就是:SparseArray用于将整形映射到对象,对于这样的键值对映射,它有着比HashMap更高的内存效率,因为其避免了整形自动拆装箱的过程,并且不需要额外的Entry结构。

大致存储结构

分析源码之前,大家先根据图大概看一下SparseArray是怎么存储键值对的。 其大致存储结构如下图:

可以根据图归纳出以下几点:

  • 使用int数组mKeys 存储所有的key
  • 使用Obeject数组mValues 存储所有的value
  • keyvalue根据下标一一对应,也就是说想得到某个键值对,只需要根据某个下标index利用mKeys[index]中取出key值,再利用mValues[index]取出value,就得到了一个key->value键值对了。

还有几个比较关键的点图中没有展示:

  • mKeys是有序数组,按照key的大小由小到大排列
  • 某个键值对的value值可能为DELETE(SparseArray中的一个静态Object变量),方便起见,下文中我会称这种键值对为脏数据。出现原因是,当你想删除某个键值对时,SparseArray不会立即删除该键值对,而是先将要删除的key对应的value值替换成DELETE,然后在必要的时候才会删除。 下面开始分析源码。

    属性

SparseArray内部的属性很少,如下:

  // 当移除一个元素时,并不是立即删除数据,而是将key->value 变成key->DELETED
  private static final Object DELETED = new Object(); 
  // 用来标记当前内部是否有 key->DELETED 这样的脏数据
  private boolean mGarbage = false; 

  private int[] mKeys; // 用于存放keys的数组
  private Object[] mValues; // 用于存放values数组
  private int mSize; // 键值对的数量
复制代码

一共就五个属性,后面三个比较好理解,就不说了。而前面两个主要是用于协助 SparseArray的删除操作,当调用delete(int key)方法删除某个元素的时候,它不会立即删除元素,只是将原来的key->value 映射变成key->DELETED,并用mGarbage置为true,表示当前数组中存在脏数据,然后在某种必要的时间才清除这些键值对。

构造函数

  public SparseArray(int initialCapacity) {
      if (initialCapacity == 0) {
          mKeys = EmptyArray.INT;
          mValues = EmptyArray.OBJECT;
      } else {
      // initialCapacity不为0时,根据initialCapacity初始化mKeys和mValues
          mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
          mKeys = new int[mValues.length];
      }
      mSize = 0;
  }
  
  public SparseArray() {
      this(10); // 内部调用了上面的方法,设置初始数组长度为10
  }
复制代码

SparseArray有两个构造函数,无参构造函数内部调用了带初始容量的构造函数,该构造函数内部进行了key,value数组的初始化。我们得知如果使用无参构造函数,key,value数组的初始容量均为10,比较简单. 接下来看一看关键的put方法。

put(int key,int value) 增加(刷新)键值对

 public void put(int key, E value) {
 
    // 先尝试着找一下是否mKeys中已经存在目标key了,如果找到了,只需要替换value的就好了,而如果没找到,则需要新增一组键值对
       int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
 
       if (i >= 0) {  // 找到了
           mValues[i] = value; // 替换对应的value值
       } else {
       // 没找到,先将i的值取反得到应该插入的下标
           i = ~i;
           
       // 如果不是插在最后一个,并且要插入的地方已经被标记删除了,则直接将key,value替换成新的键值对,插入完成
           if (i < mSize && mValues[i] == DELETED) {
               mKeys[i] = key;
               mValues[i] = value;
               return;
           }
       
       // 当数组中存在key->DELETE键值对,并且数组已经不能容下新的键值对时,进行gc()操作,清除无意义键值对
           if (mGarbage && mSize >= mKeys.length) {
               gc();

       // 由于清除了键值对,所以需要插入的下标的位置可能有变化,所以重新获取一下
               // Search again because indices may have changed.
               i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
           }
           
        // 在i处利用工具类插入 key和value,从GrowingArrayUtils名字可以看出,当key和value数组容量不足时,会先对数组扩容,在插入。
           mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
           mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
           mSize++; // 插入成功 刷新容量
       }
   }
   
   // ------------------binarySearch(int[] array, int size, int value)-----
     static int binarySearch(int[] array, int size, int value) {
       int lo = 0; // 左边起始位
       int hi = size - 1; // 右边起始位

   // 二分查找key对应的下标
       while (lo <= hi) {
           final int mid = (lo + hi) >>> 1; // 类似于 mid = (lo+hi)/2,不过这里使用了位运算提升了运算效率 
           final int midVal = array[mid];

           if (midVal < value) {
               lo = mid + 1;
           } else if (midVal > value) {
               hi = mid - 1;
           } else {
               return mid;  // value found
           }
       }
       // 到了这一步,说明未查到key。这里记住一个规律,当二分查找在数组中未找到某个值,则此时lo的值为最适合插入数组的下标,这里进行取反然后再返回,这样方便让外层根据正负判断是否找到对应key的下标。
       return ~lo;  // value not present
   }
   
   //---------------- gc() --------------------- 
    private void gc() {
       // Log.e("SparseArray", "gc start with " + mSize);

       int n = mSize; // 键值对数量
       int o = 0; // 用于记录有意义(不是key->DELETE)的键值对的数量
       int[] keys = mKeys; // 获取key数组引用
       Object[] values = mValues; // 获取value数组引用
      
      // 开始清除
       for (int i = 0; i < n; i++) {
           Object val = values[i];
           
     // 每遇到一个有意义的键值对,将他存入到key和value数组中,并且更新键值对个数
           if (val != DELETED) { 
               if (i != o) {
                   keys[o] = keys[i];
                   values[o] = val;
                   values[i] = null;
               }

               o++;
           }
       }
       
       mGarbage = false; // 数组中不再有key->DELETE 键值对
       mSize = o; // 恢复正确的键值对容量

       // Log.e("SparseArray", "gc end with " + mSize);
   }
复制代码

put方法是SparseArray中比较难的方法了,流程如下:

比较关键的地方:

  • binarySearch(int[] array, int size, int value) 方法比较巧妙,当在mkeys数组中未找到要插入的key时,返回值是一个负数,当我们对这个负数进行取反便能得到最合适的插入位置index
  • 当要插入的地方的value值为DELETE时,我们可以采用直接替换掉脏数据而不是插入,提升了效率。
  • 采用二分法获取下标插入,因此,mKeys是按照key的大小从小到大排列的。
  • 插入key,value的方式为mkeys和mValues在同一下标存入对应的key值和value值,并且GrowingArrayUtils会智能的对数组进行扩容。

下面看get()方法

get(int key) & get(int key,E valueIfKeyNotFound) 根据键获取值

 public E get(int key) {
   // 重载了下面的方法
        return get(key, null);
    }
    
 public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

  // 还是使用二分查找查找key的下标,当未找到key值或者找到的key值对应的value值为DELETE时,返回传入的valueIfKeyNotFound
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
        // 找到key的下标,返回value值
            return (E) mValues[i];
        }
    }
复制代码

get方法比较简单,第二个get方法类似于HashMap中的getOrDefault()方法,找不到对应的value时会返回一个传入的默认值。 下面看我们的delete方法。

delete(int key) & remove(int key) 删除某个键值对

  public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 当key存在于mKeys中时,将对应的value赋值为DELETE,并用mGarbage标记当前存在脏数据
        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }
    
    // 内部调用了delete方法
     public void remove(int key) {
        delete(key);
    }
复制代码

我们看到,调用delete或者remove方法删除数据时并不是真正的删除了数据,只是将key->value变成了key->DELETE的形式,看过put方法之后,不难发现,在某种情况下,比如这种:

SparseArray sparseArray = new SparseArray();
sparseArray.put(5,obj1); // 存入5 ->obj1
sparseArray.delete(5); // 删除 key 为 5的键值对,根据删除机制,此时键值对情况为 5 ->DELETE
sparseArray.put(1,obj2); // 此时,欲存入1->obj2 键值对,根据二分查找,应该插入下标为0 ,此时mValues[0] = DELETE,因此,直接替换mKeys[0]为1,mValues[0]为obj2,键值对情况更新为 1->obj2。
复制代码

可以发现,SparseArray的删除机制在某些情况下能免去一次删除操作和一次插入操作,提升了效率。

size() 获取键值对的个数

 public int size() {
        if (mGarbage) {
            gc();
        }

        return mSize;
    }
复制代码

因为可能存在脏数据,所以return之前进行了gc()判断。

indexOfValue(E value) & indexOfValueByValue(E value) 获取某个值得下标

  public int indexOfValue(E value) {
  // 如果存在脏数据,进行gc(),防止下标返回有误
        if (mGarbage) {
            gc();
        }

        for (int i = 0; i < mSize; i++) {
            if (mValues[i] == value) { // 使用 “==” 条件判断
                return i;
            }
        }

        return -1;
    }
    
      public int indexOfValueByValue(E value) {
      // 如果存在脏数据,进行gc(),防止下标返回有误
        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])) {// 使用 “equals方法” 条件判断
                    return i;
                }
            }
        }
        return -1;
    }

复制代码

两个方法的作用都是获取某个valuevalues数组的下标,但是有一点小区别。indexOfValue(E value) 使用 “==” 来进行条件判断,表示查找的值和容器中的value为同一对象时,才返回下标,而indexOfValueByValue(E value) 则没有那么严格,使用equals方法进行条件判断。需要使用者根据不同的需求调用对应的方法。

还有一些零零闪闪的方法就不一一列出来了,知道了原理,都相对简单。

在什么时候我们应该使用SparseArray?

好了,以上就是SparseArray大致的源码解析了,比起HashMapSparseArray避免了自动拆装箱过程,内部也仅使用了两个数组来存储键值对,比较轻巧简单,对内存更加友好,所以在项目中,如果是遇到Key的类型为int的情况,应该尽量使用SparseArray来替代HashMap。但是值得注意的是,SparseArray使用二分查找来获取key在数组中的下标,其时间复杂度为O(log(N)),当存放的数据量达到千量级以上,存取耗时会比较明显,此时,使用时间复杂度更低的HashMap会是更好的选择,不过这种情况相对较少。

最后

好了,以上就是我对SparseArray的理解和解读,如果错误,欢迎指正。好久没写博客啦,希望自己以后能陆陆续续的分享一些东西出来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值