目录
- [1. 概述](#1. 概述 "#1-%E6%A6%82%E8%BF%B0")
- [2. 设计原理](#2. 设计原理 "#2-%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86")
- [3. 基本使用](#3. 基本使用 "#3-%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8")
- [4. 源码解析](#4. 源码解析 "#4-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90")
- [5. 完整调用流程](#5. 完整调用流程 "#5-%E5%AE%8C%E6%95%B4%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B")
- [6. SparseArray vs HashMap 性能对比](#6. SparseArray vs HashMap 性能对比 "#6-sparsearray-vs-hashmap-%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94")
- [7. 性能测试](#7. 性能测试 "#7-%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95")
- [8. 内存对比详细分析](#8. 内存对比详细分析 "#8-%E5%86%85%E5%AD%98%E5%AF%B9%E6%AF%94%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90")
- [9. 何时使用 SparseArray](#9. 何时使用 SparseArray "#9-%E4%BD%95%E6%97%B6%E4%BD%BF%E7%94%A8-sparsearray")
- [10. SparseArray 家族](#10. SparseArray 家族 "#10-sparsearray-%E5%AE%B6%E6%97%8F")
- [11. 常见问题](#11. 常见问题 "#11-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98")
- [12. 总结](#12. 总结 "#12-%E6%80%BB%E7%BB%93")
1. 概述
SparseArray 是 Android 专门为 int 到 Object 映射设计的稀疏数组,相比 HashMap 在 Android 上更省内存、性能更好。
ini
SparseArray = int 键 + 值 + 自动扩容 + 二分查找
核心优势
| 优势 | 说明 |
|---|---|
| 省内存 | 相比 HashMap 内存占用减少约 80% |
| 避免装箱 | int 键无需自动装箱为 Integer |
| 性能好 | 小中数据量下性能优于 HashMap |
| Android 原生 | 专为 Android 优化,无额外依赖 |
2. 设计原理
数据结构
SparseArray 内部使用两个数组存储数据:
java
public class SparseArray<E> implements Cloneable {
// 存储 key(int)
private int[] mKeys;
// 存储 value(Object)
private Object[] mValues;
// 实际存储的元素个数
private int mSize;
}
内部存储结构
yaml
┌─────────────────────────────────────────────────────────────┐
│ SparseArray │
├─────────────────────────────────────────────────────────────┤
│ mKeys: [5, 12, 18, 23, 30] ← 有序存储 │
│ mValues: [A, B, C, D, E ] ← 对应的值 │
│ mSize: 5 ← 实际元素个数 │
└─────────────────────────────────────────────────────────────┘
↑ ↑ ↑
索引 0 索引 2 索引 4
二分查找原理
由于 mKeys 是有序的,可以使用二分查找快速定位 key,时间复杂度 O(log n)。
ini
查找 key = 18:
┌─────────────────────────────────────────────────────────────┐
│ mKeys: [5, 12, 18, 23, 30] │
│ ↑ ↑ ↑ ↑ ↑ │
│ lo mid hi │
│ │
│ 1. mid = 18 == key ← 找到了! │
│ 2. 返回 mValues[2] = "C" │
└─────────────────────────────────────────────────────────────┘
3. 基本使用
3.1 创建和操作
java
// 创建 SparseArray
SparseArray<String> sparseArray = new SparseArray<>();
// 添加元素
sparseArray.put(100, "A");
sparseArray.put(200, "B");
sparseArray.put(50, "C"); // 会自动排序
sparseArray.append(300, "D"); // 假设 key 大于所有现有 key,更快
// 获取元素
String value = sparseArray.get(200); // 返回 "B"
String value2 = sparseArray.get(999, "Default"); // key 不存在返回默认值
// 获取 key 对应的索引
int index = sparseArray.indexOfKey(200); // 返回 2
// 根据 value 查找索引
int index2 = sparseArray.indexOfValue("A"); // 返回 0
// 根据 index 获取 key 和 value
int key = sparseArray.keyAt(1); // 返回 100
String val = sparseArray.valueAt(1); // 返回 "A"
// 删除元素
sparseArray.delete(100); // 标记为删除,值为 DELETE
sparseArray.remove(200); // 等同于 delete()
sparseArray.removeAt(1); // 根据索引删除
// 清空
sparseArray.clear();
// 克隆
SparseArray<String> clone = sparseArray.clone();
3.2 遍历
java
SparseArray<String> sparseArray = new SparseArray<>();
sparseArray.put(1, "A");
sparseArray.put(5, "B");
sparseArray.put(3, "C");
// 方式 1:通过 index 遍历
for (int i = 0; i < sparseArray.size(); i++) {
int key = sparseArray.keyAt(i);
String value = sparseArray.valueAt(i);
Log.d("SparseArray", key + " = " + value);
}
// 方式 2:使用 forEach(API 24+)
sparseArray.forEach((key, value) -> {
Log.d("SparseArray", key + " = " + value);
});
3.3 高级操作
java
SparseArray<String> sparseArray = new SparseArray<>();
// 检查是否包含 key
boolean containsKey = sparseArray.indexOfKey(100) >= 0;
// 设置 value(如果 key 不存在则不操作)
sparseArray.setValueAt(0, "New Value"); // 修改索引 0 的值
// 获取大小
int size = sparseArray.size();
// 检查 key 是否有对应的有效 value
// DELETE 是一个特殊标记,表示该位置被删除
Object deleted = sparseArray.get(999, SparseArray.DELETED_OBJECT);
4. 源码解析
4.1 类结构
java
public class SparseArray<E> implements Cloneable {
// 删除标记
private static final Object DELETED = new Object();
// 是否需要 gc
private boolean mGarbage = false;
// key 数组
private int[] mKeys;
// value 数组
private Object[] mValues;
// 元素个数
private int mSize;
// 构造函数
public SparseArray() {
this(10); // 默认容量 10
}
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = new int[0];
mValues = new Object[0];
} else {
// 找到最近的 2 的幂
mKeys = new int[idealIntArraySize(initialCapacity)];
mValues = new Object[mKeys.length];
}
mSize = 0;
}
}
4.2 put 方法(核心)
java
public void put(int key, E value) {
// 使用二分查找 key 的位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
// key 已存在,直接替换 value
mValues[i] = value;
return;
}
// key 不存在,i = -(插入位置 + 1)
i = ~i; // 取反,得到插入位置
// 检查是否需要 GC(删除标记的元素)
if (mGarbage && mSize >= mKeys.length) {
gc(); // 压缩数组
// GC 后重新查找位置
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
// 插入新元素(数组需要移动)
mKeys = insert(mKeys, mSize, i, key);
mValues = insert(mValues, mSize, i, value);
mSize++;
}
4.3 get 方法
java
public E get(int key) {
return get(key, null);
}
public E get(int key, E valueIfKeyNotFound) {
// 二分查找
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0 || mValues[i] == DELETED) {
// key 不存在或已被删除
return valueIfKeyNotFound;
} else {
// 找到,返回对应 value
return (E) mValues[i];
}
}
4.4 binarySearch(二分查找)
java
class ContainerHelpers {
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; // 找到
}
}
// 未找到,返回 -(插入位置 + 1)
return ~lo;
}
}
4.5 delete/remove 方法
java
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 remove(int key) {
delete(key); // 等同于 delete()
}
4.6 gc 方法(压缩数组)
java
private void gc() {
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
// 移除标记为 DELETED 的元素,移动后面的元素
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; // 帮助 GC
}
o++;
}
}
mGarbage = false;
mSize = o;
}
4.7 insert 方法(插入数组)
java
private static int[] insert(int[] array, int currentSize, int index, int element) {
// 检查是否需要扩容
if (currentSize + 1 <= array.length) {
// 不需要扩容,移动元素
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
// 需要扩容
int[] newArray = new int[idealIntArraySize(currentSize + 1)];
// 复制前面的元素
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
// 复制后面的元素
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
4.8 容量增长规则
java
static int idealIntArraySize(int need) {
// 找到最近的 2 的幂
return idealByteArraySize(need * 4) / 4;
}
static int idealByteArraySize(int need) {
// 返回 >= need 的最小的 2 的幂
for (int i = 4; i < 32; i++) {
if (need <= (1 << i) - 12) {
return (1 << i) - 12;
}
}
return need;
}
ini
容量增长规则:
need = 10 → 返回 16
need = 17 → 返回 32
need = 33 → 返回 64
need = 65 → 返回 128
...
5. 完整调用流程
5.1 put 流程
scss
put(100, "A")
↓
binarySearch(mKeys, mSize, 100)
↓
key 不存在,i = -(插入位置 + 1)
↓
i = ~i ← 取反得到插入位置
↓
检查 mGarbage && mSize >= mKeys.length
↓
如果需要,gc() 压缩数组
↓
insert(mKeys, ..., i, 100) ← 插入 key
↓
insert(mValues, ..., i, "A") ← 插入 value
↓
mSize++
5.2 get 流程
scss
get(100)
↓
binarySearch(mKeys, mSize, 100)
↓
找到索引 i
↓
检查 mValues[i] == DELETED
↓
如果是 DELETED → 返回默认值
↓
否则 → 返回 mValues[i]
5.3 delete 流程
scss
delete(100)
↓
binarySearch(mKeys, mSize, 100)
↓
找到索引 i
↓
mValues[i] = DELETED ← 标记删除
↓
mGarbage = true ← 设置标记
5.4 为什么删除时不立即从数组移除?
删除时只标记为 DELETED,在下次扩容或 GC 时统一压缩,这样做可以:
- 避免频繁的数组移动操作(O(n))
- 提高删除操作的性能
- 在下次插入时统一处理
6. SparseArray vs HashMap 性能对比
6.1 时间复杂度
| 操作 | SparseArray | HashMap (int key) |
|---|---|---|
| 插入 | O(n) 最坏,O(log n) 平均 | O(1) 平均 |
| 查找 | O(log n) | O(1) 平均 |
| 删除 | O(n) | O(1) 平均 |
| 遍历 | O(n) | O(n) |
scss
为什么 SparseArray 插入是 O(n)?
因为插入需要移动数组元素:
┌─────────────────────────────────────────────────────────────┐
│ 插入前:[5, 12, 18, 23, 30] │
│ │
│ 插入 15: │
│ 1. 二分查找确定位置:index = 2 │
│ 2. 移动元素:[5, 12, __, 18, 23, 30] ← 向右移动 │
│ 3. 插入元素:[5, 12, 15, 18, 23, 30] │
│ │
│ 移动元素需要 O(n) 时间 │
└─────────────────────────────────────────────────────────────┘
6.2 内存占用对比
java
// HashMap<int, String>
HashMap<Integer, String> map = new HashMap<>();
// 每个 Entry 的内存占用:
// - Integer 对象:16 字节(对象头) + 4 字节(int 值) = 20 字节
// - String 引用:8 字节
// - Entry 对象:16 字节(对象头) + 8 字节(next 指针) + ... = 约 40 字节
// - 数组开销:每个元素 8 字节
// 总计:约 70+ 字节 per entry
// SparseArray<String>
SparseArray<String> array = new SparseArray<>();
// 每个 entry 的内存占用:
// - int key:4 字节
// - String 引用:8 字节
// 总计:约 12 字节 per entry
6.3 内存结构对比
vbnet
HashMap<Integer, String> 的内存结构:
HashMap 对象
├── table: Entry[] 数组
│ └── Entry 节点
│ ├── key: Integer 对象 (自动装箱)
│ │ └── value: int 值
│ ├── value: String 引用
│ └── next: Entry 引用 (链表)
├── size: int
├── threshold: int
└── ...
每个 entry 的内存:
- Integer 对象: 16 字节(对象头) + 4 字节(int) = 20 字节
- Entry 对象: 16 字节(对象头) + 8 字节(next) + 8 字节(key) + 8 字节(value) = 40 字节
- 数组引用: 8 字节
- 总计: 约 70 字节/entry
SparseArray<String> 的内存结构:
SparseArray 对象
├── mKeys: int[] 数组
├── mValues: Object[] 数组
└── mSize: int
每个 entry 的内存:
- int key: 4 字节
- value 引用: 8 字节
- 总计: 12 字节/entry
7. 性能测试
测试代码
java
// 插入 10000 个元素
int count = 10000;
// HashMap
HashMap<Integer, String> map = new HashMap<>();
long start1 = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
map.put(i, "Value" + i);
}
long end1 = System.currentTimeMillis();
Log.d("Performance", "HashMap put: " + (end1 - start1) + "ms");
// SparseArray
SparseArray<String> sparse = new SparseArray<>();
long start2 = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
sparse.put(i, "Value" + i);
}
long end2 = System.currentTimeMillis();
Log.d("Performance", "SparseArray put: " + (end2 - start2) + "ms");
测试结果(典型)
| 元素数量 | HashMap put | SparseArray put | HashMap get | SparseArray get |
|---|---|---|---|---|
| 1000 | ~2ms | ~1ms | ~0.5ms | ~0.5ms |
| 10000 | ~5ms | ~3ms | ~1ms | ~1ms |
| 100000 | ~15ms | ~20ms | ~2ms | ~5ms |
性能结论
┌─────────────────────────────────────────────────────────────┐
│ │
│ 小数据量(< 10000): │
│ - SparseArray 更快(减少内存分配和 GC) │
│ │
│ 中等数据量(10000 ~ 50000): │
│ - 两者接近,SparseArray 略省内存 │
│ │
│ 大数据量(> 50000): │
│ - HashMap 更快(插入/查找更快,GC 压力小) │
│ │
└─────────────────────────────────────────────────────────────┘
8. 内存对比详细分析
8.1 内存计算示例
java
// 存储 10000 个 int-String 映射
// HashMap:
// 10000 entries × 70 字节 = 700,000 字节 ≈ 683 KB
// 加上数组开销和负载因子,实际约 1 MB+
// SparseArray:
// 10000 entries × 12 字节 = 120,000 字节 ≈ 117 KB
// 加上数组扩容,实际约 150 KB
// 内存节省:约 85%
8.2 GC 压力对比
diff
HashMap:
- 每次插入/删除都创建新对象
- Integer 对象频繁创建和回收
- Entry 对象频繁创建和回收
- GC 压力大
SparseArray:
- 只有两个数组,无额外对象创建
- int 键无需装箱
- GC 压力小
9. 何时使用 SparseArray
选择指南
| 场景 | 推荐 | 原因 |
|---|---|---|
| key 是 int 类型 | ✅ SparseArray | 省内存、性能好 |
| key 是 long 类型 | ✅ LongSparseArray | 类似 SparseArray |
| key 是 Object 类型 | ❌ HashMap | SparseArray 不支持 |
| 数据量很大(> 50000) | ✅ HashMap | HashMap 插入/查找更快 |
| 数据量小(< 10000) | ✅ SparseArray | 内存和性能都更优 |
| 需要线程安全 | ❌ HashMap(使用 ConcurrentHashMap) | SparseArray 不是线程安全的 |
决策树
objectivec
需要存储 key-value 映射
↓
key 是 int?
↓
YES NO
↓ ↓
数据量大? 使用 HashMap
↓
YES NO
↓ ↓
HashMap SparseArray
10. SparseArray 家族
| 类 | Key 类型 | Value 类型 | 说明 |
|---|---|---|---|
SparseArray<E> |
int | Object | 最常用 |
LongSparseArray<E> |
long | Object | long 键版本 |
SparseBooleanArray |
int | boolean | 值是 boolean |
SparseIntArray |
int | int | 值是 int |
SparseLongArray |
int | long | 值是 long |
使用示例
java
// SparseArray - 通用版本
SparseArray<String> array = new SparseArray<>();
array.put(1, "A");
// LongSparseArray - long 键版本
LongSparseArray<User> longArray = new LongSparseArray<>();
longArray.put(123456789L, user);
// SparseBooleanArray - 值是 boolean
SparseBooleanArray boolArray = new SparseBooleanArray();
boolArray.put(1, true);
// SparseIntArray - 值是 int
SparseIntArray intArray = new SparseIntArray();
intArray.put(1, 100);
// SparseLongArray - 值是 long
SparseLongArray longValArray = new SparseLongArray();
longValArray.put(1, 123456789L);
11. 常见问题
Q1: SparseArray 是线程安全的吗?
A : 不是!如果需要线程安全,使用 ConcurrentHashMap 或手动加锁。
java
// 不安全的用法(多线程环境下)
SparseArray<String> array = new SparseArray<>();
// 线程安全的用法
SparseArray<String> array = new SparseArray<>();
synchronized (array) {
array.put(1, "A");
}
Q2: delete 和 remove 有什么区别?
A : 没有区别,remove() 内部调用了 delete()。
java
// SparseArray 源码
public void remove(int key) {
delete(key);
}
Q3: 为什么删除时不立即从数组移除?
A: 为了性能。删除时只标记为 DELETED,在下次扩容或 GC 时统一压缩,避免频繁的数组移动。
Q4: SparseArray 的 key 必须是 int 吗?
A : 是的。如果 key 是 long,使用 LongSparseArray。
Q5: 什么时候触发 gc()?
A: 在以下情况触发:
put()时发现需要扩容put()时发现标记为需要 GC(mGarbage = true)
12. 总结
核心特性对比
| 特性 | SparseArray | HashMap<int, T> |
|---|---|---|
| 内存占用 | 低(约 12 字节/entry) | 高(约 70 字节/entry) |
| 插入性能 | O(n) 最坏,O(log n) 平均 | O(1) 平均 |
| 查找性能 | O(log n) | O(1) 平均 |
| 自动装箱 | 无(int 原生) | 有(Integer 对象) |
| 线程安全 | 否 | 否(ConcurrentHashMap 是) |
| 适用场景 | int 键、小中数据量 | Object 键、大数据量 |
选择建议
arduino
选择建议:
int 键 + 小中数据量(< 10000)→ SparseArray ✓
int 键 + 大数据量(> 50000)→ HashMap ✓
long 键 → LongSparseArray ✓
Object 键 → HashMap ✓
需要线程安全 → ConcurrentHashMap ✓
最佳实践
java
// ✅ 推荐:Android 中使用 SparseArray
private SparseArray<View> cachedViews = new SparseArray<>();
// ❌ 避免:用 HashMap 存储 int-Object 映射
private Map<Integer, View> cachedViews = new HashMap<>();
// ✅ 推荐:使用 LongSparseArray 存储 long 键
private LongSparseArray<User> userCache = new LongSparseArray<>();
// ✅ 推荐:使用 SparseIntArray 存储 int-int 映射
private SparseIntArray colorMap = new SparseIntArray();
// ✅ 推荐:指定初始容量
SparseArray<String> array = new SparseArray<>(100);
// ✅ 推荐:使用 append() 当 key 大于现有所有 key
array.append(lastKey + 1, value);