arraymap android,深入剖析 Android中的 ArrayMap

article/2025/4/20 12:15:55

数据集合在任何一门编程语言中都是很重要的一部分,在 Android 开发中,我们会实用到ArrayList, LinkedList, HashMap等。其中HashMap是用来处理键值对需求的常用集合。 而Android中引入了一个新的集合,叫做ArrayMap,为键值对存储需求增加了一种选择。

ArrayMap是什么

一个通用的key-value映射数据结构

相比HashMap会占用更少的内存空间

android.util和android.support.v4.util都包含对应的ArrayMap类

ArrayMap的内部结构

arraymap_internal_structure.png

如上图所示,在ArrayMap内部有两个比较重要的数组,一个是mHashes,另一个是mArray。

mHashes用来存放key的hashcode值

mArray用来存储key与value的值,它是一个Object数组。

其中这两个数组的索引对应关系是

mHashes[index] = hash;mArray[index<<1] = key; //等同于 mArray[index * 2] = key;mArray[(index<<1)+1] = value; //等同于 mArray[index * 2 + 1] = value;

注:向左移一位的效率要比 乘以2倍 高一些。

查找数据

查找数据是容器常用的操作,在Map中,通常是根据key找到对应的value的值。

ArrayMap中的查找分为如下两步

根据key的hashcode找到在mHashes数组中的索引值

根据上一步的索引值去查找key所对应的value值

其中占据时间复杂度多的属于步:确定key的hashCode在mHahses中的索引值。

而这一步对mHashes查找使用的是二分查找,即Binary Search。所以ArrayMap的查询时间复杂度为 ‎O(log n)

确定key的hashcode在mHashes中的索引的代码的逻辑

int indexOf(Object key, int hash) {final int N = mSize;//快速判断是ArrayMap是否为空,如果符合情况快速跳出if (N == 0) {return ~0;}//二分查找确定索引值int index = ContainerHelpers.binarySearch(mHashes, N, hash);// 如果未找到,返回一个index值,可能为后续可能的插入数据使用。if (index < 0) {return index;}// 如果确定不仅hashcode相同,也是同一个key,返回找到的索引值。if (key.equals(mArray[index<<1])) {return index;}// 如果key的hashcode相同,但不是同一对象,从索引之后再次找int end;for (end = index + 1; end < N && mHashes[end] == hash; end++) {if (key.equals(mArray[end << 1])) return end;}// 如果key的hashcode相同,但不是同一对象,从索引之前再次找for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {if (key.equals(mArray[i << 1])) return i;}//返回负值,既可以用来表示无法找到匹配的key,也可以用来为后续的插入数据所用。// Key not found -- return negative value indicating where a// new entry for this key should go. We use the end of the// hash chain to reduce the number of array entries that will// need to be copied when inserting.return ~end;}

既然对mHashes进行二分查找,则mHashes必须为有序数组。

插入数据

ArrayMap提供给我们进行插入数据的API有

append(key,value)

put(key,value)

putAll(collection)

以put方法为例,需要注意的有

新数据位置确定

key为null

数组扩容问题

新数据位置确定

为了确保mHashes能够进行二分查找,我们需要保证mHashes始终未有序数组。

在确定新数据位置过程中

根据key的hashcode在mHashes表中二分查找确定合适的位置。

如果新添加的数据的索引不是后位置,在需要对这个索引之后的全部数据向后移动

arraymap_move_to_right.png

key为null时

当key为null时,其实和其他正常的key差不多,只是对应的hashcode会默认成0来处理。

public V put(K key, V value) {final int hash;int index;if (key == null) {hash = 0;//如果key为null,其hashcode算作0index = indexOfNull();}...}

数组扩容问题

首先数组的容量会扩充到BASE_SIZE

如果BASE_SIZE无法容纳,则扩大到2 * BASE_SIZE

如果2 * BASE_SIZE仍然无法容纳,则每次扩容为当前容量的1.5倍。

具体的计算容量的代码为

/*** The minimum amount by which the capacity of a ArrayMap will increase.* This is tuned to be relatively space-efficient.*/private static final int BASE_SIZE = 4;final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1)): (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

删除数据

删除ArrayMap中的一项数据,可以分为如下的情况

如果当前ArrayMap只有一项数据,则删除操作将mHashes,mArray置为空数组,mSize置为0.

如果当前ArrayMap容量过大(大于BASE_SIZE*2)并且持有的数据量过小(不足1/3)则降低ArrayMap容量,减少内存占用

如果不符合上面的情况,则从mHashes删除对应的值,将mArray中对应的索引置为null

ArrayMap的缓存优化

ArrayMap的容量发生变化,正如前面介绍的,有这两种情况

put方法增加数据,扩大容量

remove方法删除数据,减小容量

在这个过程中,会频繁出现多个容量为BASE_SIZE和2 * BASE_SIZE的int数组和Object数组。ArrayMap设计者为了避免创建不必要的对象,减少GC的压力。采用了类似对象池的优化设计。

这其中设计到几个元素

BASE_SIZE 值为4,与ArrayMap容量有密切关系。

mBaseCache 用来缓存容量为BASE_SIZE的int数组和Object数组

mBaseCacheSize mBaseCache缓存的数量,避免无限缓存

mTwiceBaseCache 用来缓存容量为 BASE_SIZE * 2的int数组和Object数组

mTwiceBaseCacheSize mTwiceBaseCache缓存的数量,避免无限缓存

CACHE_SIZE 值为10,用来控制mBaseCache与mTwiceBaseCache缓存的大小

这其中

mBaseCache的个元素保存下一个mBaseCache,第二个元素保存mHashes数组

mTwiceBaseCache和mBaseCache一样,只是对应的数组容量不同

具体的缓存数组逻辑的代码为

private static void freeArrays(final int[] hashes, final Object[] array, final int size) {if (hashes.length == (BASE_SIZE*2)) {synchronized (ArrayMap.class) {if (mTwiceBaseCacheSize < CACHE_SIZE) {array[0] = mTwiceBaseCache;array[1] = hashes;for (int i=(size<<1)-1; i>=2; i--) {array[i] = null;}mTwiceBaseCache = array;mTwiceBaseCacheSize++;if (DEBUG) Log.d(TAG, "Storing 2x cache " + array+ " now have " + mTwiceBaseCacheSize + " entries");}}} else if (hashes.length == BASE_SIZE) {synchronized (ArrayMap.class) {if (mBaseCacheSize < CACHE_SIZE) {array[0] = mBaseCache;array[1] = hashes;for (int i=(size<<1)-1; i>=2; i--) {array[i] = null;}mBaseCache = array;mBaseCacheSize++;if (DEBUG) Log.d(TAG, "Storing 1x cache " + array+ " now have " + mBaseCacheSize + " entries");}}}}

具体的利用缓存数组的代码为

private void allocArrays(final int size) {if (mHashes == EMPTY_IMMUTABLE_INTS) {throw new UnsupportedOperationException("ArrayMap is immutable");}if (size == (BASE_SIZE*2)) {synchronized (ArrayMap.class) {if (mTwiceBaseCache != null) {final Object[] array = mTwiceBaseCache;mArray = array;mTwiceBaseCache = (Object[])array[0];mHashes = (int[])array[1];array[0] = array[1] = null;mTwiceBaseCacheSize--;if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes+ " now have " + mTwiceBaseCacheSize + " entries");return;}}} else if (size == BASE_SIZE) {synchronized (ArrayMap.class) {if (mBaseCache != null) {final Object[] array = mBaseCache;mArray = array;mBaseCache = (Object[])array[0];mHashes = (int[])array[1];array[0] = array[1] = null;mBaseCacheSize--;if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes+ " now have " + mBaseCacheSize + " entries");return;}}}mHashes = new int[size];mArray = new Object[size<<1];}

在Android中的应用

在Android Performance Pattern中,官方给出的使用场景为

1.item数量小于1000,尤其是插入数据和删除数据不频繁的情况。

2.Map中包含子Map对象

通过本文的介绍,我们对于ArrayMap应该有了一个比较深入的了解。虽然ArrayMap是Android系统中HashMap的一种替代,但是我们在使用时也要注意选择适宜的场景,切莫一概而论。

嗨,我是小广告:欢迎参加我的知乎Live 《我学安卓的那些套路》


http://chatgpt.dhexx.cn/article/6kSLxutG.shtml

相关文章

ArrayMap 笔记整理

源码基于 API 25 主要参考文章&#xff1a;面试必备&#xff1a;ArrayMap源码解析 1、概述 截图自&#xff1a;面试必备&#xff1a;ArrayMap源码解析 在开始讲解源码之前&#xff0c;需要说明 ArrayMap 的底层实现结构&#xff0c;即两个数组&#xff1a; int[] mHashes; /…

ArrayMap 原理

一 概述 在移动设备端&#xff0c;内存资源很珍贵&#xff0c;HashMap 为实现快速查询带来了很大内存的浪费。为此&#xff0c;2013年5月20日 Google 工程师 Dianne Hackborn 在 Android 系统源码中新增 ArrayMap 类&#xff0c;从 Android 源码中发现有不少提交&#xff0c;专…

不再害怕面试问ArrayMap一文完全看懂Android ArrayMap源码解析

作者&#xff1a;VIjolie 前言 ArrayMap是谷歌推出的在安卓等设备上用于替代HashMap的数据结构&#xff0c;和HashMap相比&#xff0c;具有更高的内存使用率&#xff0c;因此适合在Android等内存较为紧张的移动设备&#xff0c;下面结合源码分析ArrayMap实现原理&#xff0c;主…

ArrayMap原理解析

1.ArrayMap是什么 一个通用的key-value映射数据结构 相比HashMap会占用更少的内存空间 android.util和android.support.v4.util都包含对应的ArrayMap类 2.为什么要使用ArrayMap ArrayMap是一个普通的键值映射的数据结构&#xff0c;这种数据结构比传统的HashMap有着更好的内…

ArrayMap源码解析

一、数据结构 ArrayMap是一个key-value的数据结构&#xff0c;它比HashMap有更高的内存效率 它映射到两个数组结构&#xff1a;一个整数数组mHashes&#xff0c;用来保存key的hashcode&#xff1b;一个对象数组mArray&#xff0c;保存key-value 它不适用于大量数据的存储&…

ArrayMap的使用与详解

数据集合在任何一门编程语言中都是很重要的一部分&#xff0c;在 Android 开发中&#xff0c;我们会实用到List,ArrayList, HashMap等。List和ArrayList配合使用&#xff0c;其中HashMap是用来处理键值对需求的常用集合。 而Android中引入了一个新的集合&#xff0c;叫做ArrayM…

ipdb 调试 - 终端显示正常,日志显示乱码

问题描述 ipdb调试模型&#xff0c;在 vscode 终端正常显示&#xff0c;但日志文件中&#xff0c;输入流将ipdb调试信息的颜色代码记录在文件中&#xff0c;日志文件无法查阅。 解决方案&#xff1a; 提示&#xff1a;修改ipdb的终端颜色属性 点击 ipdb&#xff0c;进入 ipdb …

ipdb调试dataloader

ipdb不可以直接放入__init__/__len__/__getitem__进行调试 需要在主函数中中断调试&#xff0c;例如colab中&#xff1a; 输入需要查询的变量即可

初次使用python(2)之如何下载ipdb模块

上次提到了ipdb这个python中用于进行调试的模块&#xff0c;但在python中是额外的package。所以&#xff0c;这篇文章写得是如何下载ipdb模块。 在Windows环境中&#xff0c;需要用pip来下载所有模块。 1.安装pip 我们仍需要在python官网上找到pip文件&#xff0c;下载地址是&a…

python ipdb 调试代码

python ipdb 调试代码 安装 pip install ipdb使用 第一种方法 python -m ipdb xxx.py #单步调试也可以写一个.py文件&#xff0c;如下&#xff0c;来执行。 import os os.system(python -m ipdb xxx.py)第二种方法 在需要断点的地方插入两句话 from ipdb import set_tra…

python debug ipdb

pudb使用 to be continued… ipdb和pdb区别 实际上ipdb是pdb的扩展版本&#xff0c;在pdb的基础上添加了如下功能&#xff1a; 可以使用tab&#xff08;提示&#xff09;补全代码的功能&#xff08;我觉得这一点上我就完全倒戈了…&#xff09;调试不再是黑白的&#xff0c…

Python调试pdb和ipdb

什么是pdb和ipdb 不知道大家在用Python写代码出现报错时是怎样调试的&#xff0c;从报错提示定位回去一步一步check每一行&#xff1f;如果没有IDE或者命令行写代码时又该怎样快速调试&#xff1f;这时如果使用pdb进行调试将会异常方便。 Pdb就是Python debugger&#xff0c;…

python调试模块ipdb

1. 调试python ipdb是用来python中用以交互式debug的模块&#xff0c;可以直接利用pip安装; 其功能类似于pycharm中 python控制台&#xff0c; 而使用ipdb 的优点&#xff0c;便是直接在代码中调试&#xff0c; 避免了在python控制台&#xff0c;或者重新设置一些简单变量。…

python调试器 ipdb

文章目录 1. 介绍1.1 常用调试方式1.2 安装 ipdb 2. 用法3. 命令3.1、查看源代码3.2、添加断点3.3 添加临时断点3.4 清除断点3.5、打印变量值3.6、逐行调试命令3.7、非逐行调试命令3.8 跳出函数&#xff0c;跳入函数3.9、查看当前函数所有参数3.10 打印变量的值3.11、打印变量类…

ipdb python下的debug利器

ipdb是一个IPython的python下debug神器&#xff0c;支持在代码中添加断点&#xff0c;进行单步调试&#xff0c;支持在调试过程中查看各种变量的值&#xff0c;新增修改断点&#xff0c;或跳过特定断点。以往进行python代码的调试都是通过pycharm或vscode添加断点&#xff0c;在…

js中函数传参的问题

在使用ajax或者js拼接信息在页面中显示的时候&#xff0c;需要向函数传参的时候往往会有问题。 如果传递的是数字类型&#xff0c;则是可以直接传递。不需要进行转义。 如果是字符串类型&#xff0c;则需要进行转义&#xff0c;才会以存入字符串的参数。

js函数传参

也许大家对于函数的参数都不会太在意&#xff0c;简单来说&#xff0c;把函数外部的值复制给函数内部的参数&#xff0c;就和把值从一个变量复制到另一个变量一样。深入研究&#xff0c;你会发现其实没那么简单&#xff0c;这个传参是要分俩种情况&#xff08;其实这是个错误的…

关于js函数传参的问题

js在拼接函数时&#xff0c;当传递的值为字符串是会出现问题&#xff0c;直接报错&#xff0c;不会进入到函数里&#xff0c;这是受就需要转义符来操作&#xff0c; 例如 var can object; onClickUpdateCollege(" can ") 这时事件触发之后&#xff0c;浏览器会报错…

js 立即执行函数传参问题

正确的执行函数写法如下&#xff1a; (function func(i) {console.log(i);})(j);(function func(i) {console.log(i);}(j));!(function func(i) {console.log(i);})(j); 看看下面这段代码有什么问题&#xff1f; let testStr "test string"(function func(params) …