Java容器解析系列(16) android内存优化之SparseArray
HashMap的缺点:
- 自动装箱导致的性能损失;
- 使用拉链法来解决hash冲突,如果hash冲突较多,需要遍历链表,导致性能下降,在Java 8 中,如果链表长度>8,会使用红黑树来代替链表;
- 由于loadFactor的存在,导致(1 - loadFactor) * capacity 的空间会浪费,capacity越大,浪费空间更多;
- 扩容时需要重新计算hash,浪费性能;
- 每一个value都由一个Node保存,Node除了保存本身的数据外,还需要其他冗余数据,如hash值,下一个节点的指针等;
SparseArray
的特点:
key
只能为int
类型,避免了自动装箱;- 使用二分法查找key,在查询指定key时需要进行二分查找,查询的时间复杂度为O(logN),添加删除亦如是;所以不适合大量数据,在数据量为1000以下(up to hundreds of items)时,其与HashMap之间的性能差别(difference)低于50%;
- 没有冗余数据;
- 为了提升性能,在删除数据时,并不马上调整压缩数组(压缩数组需要移动数组的元素),而是将被删除位置的value标记为DELETED,之后该位置可能被重用,在一次压缩中,清除掉所有的DELETED;
- 可以通过
keyAt(int)
和valueAt(int)
来遍历SparseArray
中的key
和value
; - 扩容时只是复制数组值,不需要进行hash计算;
总体来看,相对于HashMap
,SparseArray
是以时间换空间
;
SparseArray源码分析:
数据结构:
SparseArray
使用两个数组来分别存储key和value:
private int[] mKeys;// 存储key值的数组
private Object[] mValues;// 存储value值的数组 mKeys[i]---mValues[i]为一个键值对
private int mSize;// 存储键值对个数,确切地说,是当前存储的key值个数,key-value键值对均存储在数组中下标为 [0,mSize-1]之间的位置
private static final Object DELETED = new Object();// DELETED标记
private boolean mGarbage = false;// 用于判断是否需要进行压缩
构造方法:
public SparseArray() {
this(10);// 默认初始容量:10
}
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];// key value数组大小一致
}
mSize = 0;
}
添加key-value:
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);// 通过二分法查找key在mKeys中的索引
if (i >= 0) {
mValues[i] = value;// 覆盖已有值
} else {
// 如果没有找到,那么i值为 ~(0) 或 ~(mSize - 1),这里再取反,值就成了 0 或 (mSize - 1),参考源码 ContainerHelpers.binarySearch(mKeys, mSize, key)
i = ~i;
if (i < mSize && mValues[i] == DELETED) {// 刚好是一个被删除的值直接替换
mKeys[i] = key;
mValues[i] = value;
return;
}
// 需要通过gc(),腾出新的空间
if (mGarbage && mSize >= mKeys.length) {
gc();
// gc() 后索引变了,需要重新找个位置,重新二分查找,确定key/value在数组中的索引
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);// 插入新key
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);// 插入新值
mSize++;
}
}
上述部分引用方法如下:
// # ContainerHelpers.binarySearch(int[], int, int)
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
// # SparseArray.gc()
// 对 mKeys 和 mValues 进行重新排列,将mValues中标记为DELETED的值清除掉
private void gc() {
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;// 更新mSize
}
// # GrowingArrayUtils.insert(int[], int, int, int);
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;// 扩容为之前的两倍
}
public static int[] insert(int[] array, int currentSize, int index, int element) {
// 不需要扩容
if (currentSize + 1 <= array.length) {
// 将之后的所有元素向后移一位 这里的时间复杂度为O(N)
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;
}
查询:
public E get(int key) {
return get(key, null);
}
// 获得指定key的值,如果没有,返回 valueIfKeyNotFound
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];
}
}
删除:
public void remove(int key) {
delete(key);
}
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);// 二分法找到位置
if (i >= 0) {
// 将值修改为 DELETED
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;// 需要垃圾回收,必要时检查该变量,并对 mKeys 和 mValues 进行重新排序
}
}
// 这里并没有将 mSize - 1,因为如果将 mSize - 1,需要重新对 mKeys 和 mValues 进行重新移动
}
综上,SparseArray的时间复杂度:
- 增:O(N);因为需要移动数组元素;
- 查:O(logN).使用二分法查找;
- 删:O(logN);查找到要删除的key之后,将其value标记为DELETED;
- 改:O(logN);同删除,只是将value改为新值而已;
如果key为long类型的话,可以使用LongParseArray,其实现与ParseArray相同,只是mKeys为long[]
如果key为int类型:
- value为int类型,可以使用SparseIntArray
- value为long类型,可以使用SparseLongArray
- value为boolean类型,可以使用SparseBooleanArray
跟多关于Android的SparseArray相关,参考视频:
Java容器解析系列(16) android内存优化之SparseArray的更多相关文章
- Java容器解析系列(0) 开篇
最近刚好学习完成数据结构与算法相关内容: Data-Structures-and-Algorithm-Analysis 想结合Java中的容器类加深一下理解,因为之前对Java的容器类理解不是很深刻, ...
- Java容器解析系列(11) HashMap 详解
本篇我们来介绍一个最常用的Map结构--HashMap 关于HashMap,关于其基本原理,网上对其进行讲解的博客非常多,且很多都写的比较好,所以.... 这里直接贴上地址: 关于hash算法: Ha ...
- Java容器解析系列(1) 迭代的进化——从Enumeration到Iterator
在Java中,对于所有的Collection,都有一个特性,可以通过迭代器来遍历和删除其中的元素,因为Collection接口继承自Iterable接口. public interface Colle ...
- Java容器解析系列(13) WeakHashMap详解
关于WeakHashMap其实没有太多可说的,其与HashMap大致相同,区别就在于: 对每个key的引用方式为弱引用; 关于java4种引用方式,参考java Reference 网上很多说 弱引用 ...
- Java容器解析系列(7) ArrayDeque 详解
ArrayDeque,从名字上就可以看出来,其是通过数组实现的双端队列,我们先来看其源码: /** 有自动扩容机制; 不是线程安全的; 不允许添加null; 作为栈使用时比java.util.Stac ...
- Java容器解析系列(10) Map AbstractMap 详解
前面介绍了List和Queue相关源码,这篇开始,我们先来学习一种java集合中的除Collection外的另一个分支------Map,这一分支的类图结构如下: 这里为什么不先介绍Set相关:因为很 ...
- Java容器解析系列(8) Comparable Comparator
Comparable和Comparator接口是两个用于对对象进行大小比较的接口,在java集合相关类中,也被经常地使用到. 关于其使用,可以参考网络上的其他博客(没什么好说的);这里阐述关于这两个接 ...
- Java容器解析系列(9) PrioriyQueue详解
PriorityQueue:优先级队列; 在介绍该类之前,我们需要先了解一种数据结构--堆,在有些书上也直接称之为优先队列: 堆(Heap)是是具有下列性质的完全二叉树:每个结点的值都 >= 其 ...
- Java容器解析系列(6) Queue Deque AbstractQueue 详解
首先我们来看一下Queue接口: /** * @since 1.5 */ public interface Queue<E> extends Collection<E> { / ...
随机推荐
- LeetCode150_Evaluate Reverse Polish Notation评估逆波兰表达式(栈相关问题)
题目: Evaluate the value of an arithmetic expression in Reverse Polish Notation. Valid operators are+, ...
- vue新增属性响应式更新的问题
根据官方文档定义: 如果在实例创建之后添加新的属性到实例上,它不会触发视图更新. 受现代 JavaScript 的限制 (以及废弃 Object.observe),Vue 不能检测到对象属性的添加或删 ...
- IntelliJ IDEA+springboot+jdbctemplet+easyui+maven+oracle搭建简易开发框架(一)
前言: 这两天为了巩固easyui的各个控件用法,搭建了一个简易的框架用于开发,大家可以用来参考,如果发现文章中有哪些不正确不合理的地方,也请各位不吝赐教,感激不尽.文章最下面有源码,可以用于参考.整 ...
- world 文档中表格旋转180°
一个好朋友给我打电话,说是有个wps操作把他难住了,他常年跟wps 形影不离,你都搞不定,我都不怎么用.听完他说的以后,我才明白他要的效果是怎么样的,贴图来看: 其实像直接转化成这种效果没有办法,但是 ...
- 20191121-4 Final发布用户使用报告
此作业要求参见:https://edu.cnblogs.com/campus/nenu/2019fall/homework/10064 队名:组长 组长:杨天宇 组员:罗杨美慧,王歆瑶,魏鑫,梅小雨 ...
- NET Core 3.1 PATCH HTTP 的使用注意事项
使用Postman请求示例: 一.在Headers要声明请求类型Content-Type 二.body提交要使用raw,且声明为json格式传输 三.如果有authorization验证还需要带上(如 ...
- 洛谷$P2570\ [ZJOI2010]$贪吃的老鼠 网络流+二分
正解:网络流+二分 解题报告: 传送门$QwQ$ 和上一题有点儿像,,,?$QwQ$但是比上一题要有趣很多$QwQ$ 首先把大致思路捋下?依然是.二分出每个奶酪的开始和结束时间,然后check下最大流 ...
- DataX-MysqlWriter 插件文档
:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdow ...
- 洛谷P2858 奶牛零食 题解 区间DP入门题
题目大意: 约翰经常给产奶量高的奶牛发特殊津贴,于是很快奶牛们拥有了大笔不知该怎么花的钱.为此,约翰购置了 \(N(1 \le N \le 2000)\) 份美味的零食来卖给奶牛们.每天约翰售出一份零 ...
- 【Python3爬虫】突破反爬之应对前端反调试手段
一.前言 在我们爬取某些网站的时候,会想要打开 DevTools 查看元素或者抓包分析,但按下 F12 的时候,却出现了下面这一幕: 此时网页暂停加载,自动跳转到 Source 页面并打开了一个 ...