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

4 阅读10分钟

目录


1. 概述

SparseArray 是 Android 专门为 intObject 映射设计的稀疏数组,相比 HashMap 在 Android 上更省内存、性能更好。

SparseArray = int 键 + 值 + 自动扩容 + 二分查找

核心优势

优势说明
省内存相比 HashMap 内存占用减少约 80%
避免装箱int 键无需自动装箱为 Integer
性能好小中数据量下性能优于 HashMap
Android 原生专为 Android 优化,无额外依赖

2. 设计原理

数据结构

SparseArray 内部使用两个数组存储数据:

public class SparseArray<E> implements Cloneable {
    // 存储 key(int)
    private int[] mKeys;

    // 存储 value(Object)
    private Object[] mValues;

    // 实际存储的元素个数
    private int mSize;
}

内部存储结构

┌─────────────────────────────────────────────────────────────┐
                    SparseArray                              
├─────────────────────────────────────────────────────────────┤
  mKeys:    [5, 12, 18, 23, 30]   有序存储               
  mValues:  [A,  B,  C,  D,  E ]   对应的值             
  mSize:    5                      实际元素个数             
└─────────────────────────────────────────────────────────────┘
                                
             索引 0     索引 2     索引 4

二分查找原理

由于 mKeys 是有序的,可以使用二分查找快速定位 key,时间复杂度 O(log n)。

查找 key = 18:

┌─────────────────────────────────────────────────────────────┐
│  mKeys:    [5, 12, 18, 23, 30]                           │
│            ↑     ↑     ↑     ↑     ↑                       │
│           lo          mid         hi                        │
│                                                         │
│  1. mid = 18 == key  ← 找到了!                          │
│  2. 返回 mValues[2] = "C"                               │
└─────────────────────────────────────────────────────────────┘

3. 基本使用

3.1 创建和操作

// 创建 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 遍历

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 高级操作

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 类结构

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 方法(核心)

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 方法

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(二分查找)

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 方法

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 方法(压缩数组)

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 方法(插入数组)

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 容量增长规则

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;
}
容量增长规则:
need = 10 → 返回 16
need = 17 → 返回 32
need = 33 → 返回 64
need = 65 → 返回 128
...

5. 完整调用流程

5.1 put 流程

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 流程

get(100)
    ↓
binarySearch(mKeys, mSize, 100)
    ↓
找到索引 i
    ↓
检查 mValues[i] == DELETED
    ↓
如果是 DELETED → 返回默认值
    ↓
否则 → 返回 mValues[i]

5.3 delete 流程

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 时间复杂度

操作SparseArrayHashMap (int key)
插入O(n) 最坏,O(log n) 平均O(1) 平均
查找O(log n)O(1) 平均
删除O(n)O(1) 平均
遍历O(n)O(n)
为什么 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 内存占用对比

// 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 内存结构对比

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. 性能测试

测试代码

// 插入 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 putSparseArray putHashMap getSparseArray 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 内存计算示例

// 存储 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 压力对比

HashMap:
- 每次插入/删除都创建新对象
- Integer 对象频繁创建和回收
- Entry 对象频繁创建和回收
- GC 压力大

SparseArray:
- 只有两个数组,无额外对象创建
- int 键无需装箱
- GC 压力小

9. 何时使用 SparseArray

选择指南

场景推荐原因
key 是 int 类型✅ SparseArray省内存、性能好
key 是 long 类型✅ LongSparseArray类似 SparseArray
key 是 Object 类型❌ HashMapSparseArray 不支持
数据量很大(> 50000)✅ HashMapHashMap 插入/查找更快
数据量小(< 10000)✅ SparseArray内存和性能都更优
需要线程安全❌ HashMap(使用 ConcurrentHashMap)SparseArray 不是线程安全的

决策树

需要存储 key-value 映射
         ↓
    key 是 int?
         ↓
    YES        NO
    ↓          ↓
 数据量大?  使用 HashMap
    ↓
 YES      NO
  ↓         ↓
HashMap  SparseArray

10. SparseArray 家族

Key 类型Value 类型说明
SparseArray<E>intObject最常用
LongSparseArray<E>longObjectlong 键版本
SparseBooleanArrayintboolean值是 boolean
SparseIntArrayintint值是 int
SparseLongArrayintlong值是 long

使用示例

// 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 或手动加锁。

// 不安全的用法(多线程环境下)
SparseArray<String> array = new SparseArray<>();

// 线程安全的用法
SparseArray<String> array = new SparseArray<>();
synchronized (array) {
    array.put(1, "A");
}

Q2: delete 和 remove 有什么区别?

A: 没有区别,remove() 内部调用了 delete()

// 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. 总结

核心特性对比

特性SparseArrayHashMap<int, T>
内存占用低(约 12 字节/entry)高(约 70 字节/entry)
插入性能O(n) 最坏,O(log n) 平均O(1) 平均
查找性能O(log n)O(1) 平均
自动装箱无(int 原生)有(Integer 对象)
线程安全否(ConcurrentHashMap 是)
适用场景int 键、小中数据量Object 键、大数据量

选择建议

选择建议:

int 键 + 小中数据量(< 10000)→ SparseArray ✓
int 键 + 大数据量(> 50000)→ HashMap ✓
long 键 → LongSparseArray ✓
Object 键 → HashMap ✓
需要线程安全 → ConcurrentHashMap ✓

最佳实践

// ✅ 推荐: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);

参考资源