SparseArray详解,SparseArray和HashMap性能、内存对比

目录

  • [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 专门为 intObject 映射设计的稀疏数组,相比 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 时统一压缩,这样做可以:

  1. 避免频繁的数组移动操作(O(n))
  2. 提高删除操作的性能
  3. 在下次插入时统一处理

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: 在以下情况触发:

  1. put() 时发现需要扩容
  2. 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);

参考资源

相关推荐
Haha_bj6 天前
Flutter——状态管理 Provider 详解
flutter·app
QING6188 天前
使用ADB分析CPU性能 —— 基础指南
android·前端·app
Haha_bj8 天前
Flutter——List.map()
flutter·app
iOS阿玮9 天前
百款出海社交 App 一夜下架!2026,匿名社交的生死劫怎么破?
uni-app·app·apple
iOS阿玮10 天前
开工第一天,别让AI写的代码触发3.2f封号。
uni-app·app·apple
XLYcmy12 天前
智能体大赛 总结与展望 未来展望
ai·llm·app·prompt·agent·检索·万方数据库
iOS阿玮19 天前
春节提审高峰来袭!App Store 审核时长显著延长。
uni-app·app·apple
熊猫钓鱼>_>21 天前
【开源鸿蒙跨平台开发先锋训练营】Day 12:全场景适配与异常防护——构建高可靠的鸿蒙跨端体验
react native·ui·华为·开源·app·harmonyos·鸿蒙
熊猫钓鱼>_>1 个月前
移动端开发技术选型报告:三足鼎立时代的开发者指南(2026年2月)
android·人工智能·ios·app·鸿蒙·cpu·移动端