在深入分析SparseAray前,我们先说一下SparseArray的特点 ,同时介绍一下其使用场景;

Sparserray是Android中特有的数据结构,他的几个重要的特点;

  1. 以键值对形式进行存储,基于分查找,因此查找的时间复杂度为0(LogN);
  2. .由于SparseArray中Key存储的是数组形式,因此可以直接以int作为Key。避免了HashMap的装箱拆箱操作,性能更高且int的存储开销远远小于Integer;
  3. 采用了延迟删除的机制(针对数组的删除扩容开销大的问题的优化, 具体稍后分析) ;

SparseArray小巧但是精悍,主类代码加上注释也只有不到500行,但是其中蕴含的思想却很值得学习。下面我们一起深入源码去学习一下其中的设计思想;

重要属性

public class SparseArray<E> implements Cloneable {private static final Object DELETED = new Object();private boolean mGarbage = false;private int[] mKeys;private Object[] mValues;private int mSize;

SparseArray中的元素较少,下面具体介绍:

  • DELETED ,static final 的一个静态Object实例,当一个键值对被remove后,会在对应key的value下放置该对象,标记该元素已经被删除(延迟删除,等下具体介绍);
  • mGarbage  , 当值为true,标志数据结构中有元素被删除,可以触发gc对无效数据进行回收(真正删除);
  • mKeys数组, 用于存放Key的数组,通过int[] 进行存储,与HashMap相比减少了装箱拆箱的操作,同时一个int只占4字节;一个重要特点,mKeys的元素是升序排列的,也是基于此,我们才能使用二分查找;
  • mValues数组,用于存放与Key对应的Value,通过数组的position 进行映射;如果存放的是int型等,可以用SparseIntArray ,存放的Values也是int数组,性能更高;
  • mSize,mSize的大小等于数组中mValues的值等于非DELETED的元素个数;

Remove方法(Delete)

    public void delete(int key) {//查找对应key在数组中的下标,如果存在,返回下标,不存在,返回下标的取反;int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//key存在于mKeys数组中,将元素删除,用DELETED替换原value,起标记作用;if (i >= 0) {if (mValues[i] != DELETED) {mValues[i] = DELETED;mGarbage = true;}}}/*** @hide* Removes the mapping from the specified key, if there was any, returning the old value.*/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;}/*** Alias for {@link #delete(int)}.*/public void remove(int key) {delete(key);}

吐槽:写文章前根据源码顺序写,先写get再写put最后写remove,发现很多东西没有讲清楚会很麻烦。于是重新整理,把思想最丰富,贯彻全局的remove方法前移。。。。。。。我太难了。。

首先我们主要是通过ContainerHelpers.binarySearch来进行查找对应的key,返回的i就是对应数组的下标;下面我们去看看该方法的实现原理;

其实ContainerHelpers方法只有这一个方法(准确说还有一个,输入的第一个参数array的参数为long[],而不是int[])

主要做的就是二分查找,并返回下标。下面我们仔细分析其中的设计;请对着下述源码中的注释;

class ContainerHelpers {// This is Arrays.binarySearch(), but doesn't do any argument validation.//第一个参数array为keys的数组,第二个为数组中元素个数(与keys的length不一定相等),第三个value为目标的keystatic int binarySearch(int[] array, int size, int value) {//lo为二分查找的左边界int lo = 0;//hi为二分查找的右边界int hi = size - 1;//还没找到,继续查找while (lo <= hi) {//左边界+右边界处以2,获取到mid 的indexfinal int mid = (lo + hi) >>> 1;//获取中间元素final int midVal = array[mid];// 目标key在右部分  。。。。感觉这部分太简单了if (midVal < value) {lo = mid + 1;} else if (midVal > value) {hi = mid - 1;} else {//相等,找到了,返回key对应在array的下标;return mid;  // value found}}//没有找到该元素,对lo取反!!!!!很重要return ~lo;  // value not present}

这部分代码本来就简单,注释也写的非清楚,重点就在于最后的return,可能往往二分查找没有找到都是返回-1。但是这里返回了~lo,取反导致下标小于0,用于判断没有找到;这个主要用在Put方法中,稍后再讲。我们现在只要知道,该方法是通过二分查找返回了当前key的对应于mKeys数组的下标,如果没有找到,就返回一个特殊的负数;

之后下一步,我们得到了下标i,如果非负数,我们则对其所对应的value进行替换成DELETED,用于标记该key已经被删除,同时,我们将garbage赋值true,代表数组中可能存在垃圾;

总结:remove方法主要做的就是这些,找到需要删除的key,并将对应的value用DELETED替换;但是key仍然存在于mKeys数组,因此删除是一个伪删除。这就是所谓的延迟删除机制;

接下来,我们就去put方法中切身体会一下延迟删除的作用和好处;

Put方法

    public void put(int key, E value) {int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//原来已经有key,可能是remove后,value存放着DELETED,也可能是存放旧值,那么就替换if (i >= 0) {mValues[i] = value;} else {//没有找到,对i取反,得到i= lo(ContainerHelpers.binarySearch)i = ~i;//如果i小于数组长度,且mValues==DELETED(i对应的Key被延迟删除了)if (i < mSize && mValues[i] == DELETED) {//直接取代,实现真实删除原键值对mKeys[i] = key;mValues[i] = value;return;}//数组中可能存在延迟删除元素且当前数组长度满,无法添加if (mGarbage && mSize >= mKeys.length) {//真实删除,将所有延迟删除的元素从数组中清除;gc();//清除后重新确定当前key在数组中的目标位置;// Search again because indices may have changed.i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);}//不存在垃圾或者当前数组仍然可以继续添加元素,不需要扩容,则将i之后的元素全部后移,数组中仍然存在被DELETED的垃圾key;mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);//新元素添加成功,潜在可用元素数量+1mSize++;}}

可以看到,put方法也调用了ContainerHelpers.binarySearch方法先进行查找,查找到大于0,则在数组中找到了对应的key,此时,直接将value进行替换即可;

但是,如果没有找到,返回的是~lo,那么,将i赋值~~lo,即i=lo,,此时i就是我们需要插入的位置;这个可能对二分查找不熟悉的话难以理解,下面我们用个例子展示一下,如果我们查找Key=2;

此时,lo大于hi,退出循环,lo对应的下标为2,且是插入Key=2的理想位置;因此,这个lo取反,有两个重要的作用:

  • 代表没有找到对应的key
  • 对返回值重新取反后,得到的就是lo,就是应该插入的元素。此时将key下标为lo及之后的元素后移,再将当前元素插入该位置。就完成了一次有序插入;

此刻,我们找到了i,就是目标位置,如果没有设置延迟删除(DELETED)。那么由于数组的特点,我们需要将i序号之后的数组后移,这样就会产生一个较大的性能损耗;,但是如果我们设置了延迟删除且mValue[i]上当前的元素恰巧为DELETED,那么此时我们可以用当前的key替换原来mKeys的key,且用当前value替换DELETED;这样就成功避免了一次数组的迁移操作;

但是事情不可能永远凑巧,如果,i上的元素并非恰好被删除呢;

那么此时我们会判断mGarbage,如果为true那么我们执行一次gc,将无效数据移除,再进行一次二分查找,然后将i之后的数据全部后移,将当前key插入;

如果mGarbage为false,那么证明其中的数据全部存在,因此不需要gc,直接进行元素插入并将数组后移;

其中GrowingArrayUtils.insert主要做的就是调用System.arraycopy将数组后移,如果需要扩容则扩容;

    public static int[] insert(int[] array, int currentSize, int index, int element) {assert currentSize <= array.length;if (currentSize + 1 <= array.length) {System.arraycopy(array, index, array, index + 1, currentSize - index);array[index] = element;return array;}int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));System.arraycopy(array, 0, newArray, 0, index);newArray[index] = element;System.arraycopy(array, index, newArray, index + 1, array.length - index);return newArray;}

既然遇到了gc,那我们再进入gc方法看看SparseArray是如何gc回收数据的:

GC

    private void gc() {// Log.e("SparseArray", "gc start with " + mSize);//n代表gc前数组的长度;int n = mSize;int o = 0;int[] keys = mKeys;Object[] values = mValues;for (int i = 0; i < n; i++) {Object val = values[i];//遍历元素,如果value不为DELETED,则用前数据放在o上,o的序号表示当前的有效元素下标。//每遇到一次DELETED,则i-o的大小+1;if (val != DELETED) {//之后遇到非DELETED数据,则将后续元素的key和value往前挪if (i != o) {keys[o] = keys[i];values[o] = val;values[i] = null;}o++;}}//此时无垃圾数据,o的序号表示mSize的大小mGarbage = false;mSize = o;// Log.e("SparseArray", "gc end with " + mSize);}

这里要注意一个非常非常重要的点:

我们可以看到在循环遍历中,我们做的是将数组前移。因此会存在一个问题,即gc后有效数组长度为o,但是此时,keys.length可能会大于o,那么此时,最后的keys.length-o 个数组元素中仍然存在着key和value且不会消失;但是,由于mSize等于o,此时并不会访问到最后的多个废弃元素。只有在mSize数组范围内的DELETED数据才被称为延迟删除元素,mSize范围外的不会作为 被gc删除,只会被之后的put数组后移覆盖;

下面来一个例子说明一下gc的特点:

Get方法

    public E get(int key) {return get(key, null);}/*** Gets the Object mapped from the specified key, or the specified Object* if no such mapping has been made.*/@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];}}

这一步非常简单,也没有什么特殊的设计,看明白了之前的ContainerHelpers.binarySearch这里没有任何重点。。

总结

  1. SparseArray采用了延迟删除的机制,通过将删除KEY的Value设置DELETED,方便之后对该下标的存储进行复用;
  2. 使用二分查找,时间复杂度为O(LogN),如果没有查找到,那么取反返回左边界,再取反后,左边界即为应该插入的数组下标;
  3. 如果无法直接插入,则根据mGarbage标识(是否有潜在延迟删除的无效数据),进行数据清除,再通过System.arraycopy进行数组后移,将目标元素插入二分查找左边界对应的下标;
  4. mSize 小于等于keys.length,小于的部分为空数据或者是gc后前移的数据的原数据(也是无效数据),因此二分查找的右边界以mSize为准;mSize包含了延迟删除后的元素个数;
  5. 如果遇到频繁删除,不会触发gc机制,导致mSize 远大于有效数组长度,造成性能损耗;
  6. 根据源码,可能触发gc操作的方法有(1、put;2、与index有关的所有操作,setValueAt()等;3、size()方法;)
  7. mGarbage为true不一定有无效元素,因为可能被删除的元素恰好被新添加的元素覆盖;

根据SparseArray的这些特点。我们能分析出其使用场景

  • key为整型;
  • 不需要频繁的删除;
  • 元素个数相对较少;

Android轻量级数据SparseArray详解相关推荐

  1. Android NFC卡实例详解

    Android NFC卡实例详解 公司最近在做一个NFC卡片的工程,经过几天的时间,终于写了一个Demo出来,在此记录下在此过程中遇到的问题.由于之前本人是做iOS的,Android写起来并不是那么的 ...

  2. Android Gradle 自定义Task 详解

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/76408024 本文出自[赵彦军的博客] 系列目录 Android Gradle使用 ...

  3. android ------- 开发者的 RxJava 详解

    在正文开始之前的最后,放上 GitHub 链接和引入依赖的 gradle 代码: Github:  https://github.com/ReactiveX/RxJava  https://githu ...

  4. 宏锦软件 Android 的 ListView 使用详解

     宏锦软件爱好者在开发Android软件时,对ListView的使用有点陌生,于是翻了许多资料,这里给大家一份比较好的教程,希望有用. 在android开发中ListView是比较常用的组件,它以 ...

  5. Android开发入门一之Android应用程序架构详解

    Android应用程序架构详解如下: src/ java源代码存放目录 gen/自动生成目录 gen 目录中存放所有由Android开发工具自动生成的文件.目录中最重要的就是R.java文件.这个文件 ...

  6. android调webview的方法,Android中的WebView详解

    Android中的WebView详解 WebView详解 基本用法 布局文件配置WebView android:id="@+id/wv_news_detail" android:l ...

  7. android room 教程,Android Room的使用详解

    Room 是在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库. Room 包含 3 个重要部分: 数据库:包含数据库持有者,并作为应用已保留的 ...

  8. Android多点触控详解

    本文转载自GcsSloop的 安卓自定义View进阶-多点触控详解 的文章 Android 多点触控详解,在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案,本次带 ...

  9. Android面试Hash原理详解二

    Hash系列目录 Android面试Hash原理详解一 Android面试Hash原理详解二 Android面试Hash常见算法 Android面试Hash算法案例 Android面试Hash原理详解 ...

最新文章

  1. 将php数组存取到本地文件
  2. 「功能笔记」Spacemacs+Evil备忘录
  3. FBV(function base views) 顾名思义基于函数的视图类 CBV(class base views)基于类的视图类
  4. 90后,是时候想想你的副业了
  5. “我觉得,这个项目只需要 2 个小时”
  6. 【免费毕设】asp.net电子书城系统设计与实现(源代码+lunwen)
  7. golang使用Nsq
  8. 将图片url转为base64的方法
  9. matlab导出高分辨率图片
  10. r语言和python培训_Python 和R语言
  11. python3执行js之pyexecjs
  12. 数据分析报告入门(3)
  13. contiki学习笔记(六)contiki程序加载器和多线程库
  14. Python第三方库巧用,制作图片验证码只需三行代码
  15. Jmeter入门实战(二)如何使用Jmeter的BeanShell断言,把响应数据中的JSON跟数据库中的记录对比
  16. 2020-2021年 元宇宙发展研究报告
  17. 2023西安建筑科技大学考研介绍
  18. 研究学习之java使用selenium教程
  19. 计算机技术要求低的工作,成绩一般的同学,可以考虑这3个专业,学历要求低,还很好找工作...
  20. quasar ssr 开发网站

热门文章

  1. Node.js 学习笔记day005
  2. 静下心来,慢慢改变一切.
  3. markdown数据转换,处理html2canvas+jsPDF下载后文字截断问题(记录)
  4. 巅峰之战,2021年中国移动创客马拉松大赛移动云专题赛决赛落幕!
  5. git 命令行合并代码分支
  6. 一个和二维泊松求和有关的公式(推导Ewald级数中有用,运用了2D泊松求和公式,傅里叶变换的位移性质)
  7. 网络安全学习(十七)VlAN
  8. 对接支付宝服务商当面付手机网页支付
  9. P2P直播、点播技术学习经验
  10. CF786C Till I Collapse