Understanding Android's ArrayMap
20 May 2016
1 引言
上一篇学习了SparseArray,那都是比较简单的思想,代码量也很少,可用性也比较低,限制了key只能是int类型。今天继续学习另外一个数据结构。
2 ArrayMap
由于SparseArray只能用int当作key,并不能完全代替HashMap,所以有了ArrayMap,根据官方的描述,它是用一个interger数组来存储每个item的hashcode,用object数组来存kv对。这就避免了创建额外的entry,并且它会控制这个增长的大小,并不会重建hashmap,而是复制移动数组。
针对标准的map,所以里面实现了比较多的东西,如iterator,如果我们并不需要这些,可以去使用SimpleArrayMap。
里面使用同样使用了二分查找,所以效率问题跟SparseArray一样,在一千以内性能极佳,过多则不好了。本质的实现其实也是稀疏矩阵。
2.1 核心源码学习
这个类的实现比较复杂,代码会比较多,这里主要是展示核心的理解就好了。里面很多核心的思想和算法,如缓存等等的。
先看看它的关键属性和构造方法:
其中ContainerHelpers是一个二分查找的工具类,和SparseArray一样的;我们默认使用空的构造方法,也可以初始化容量。immutable这个参数没用,后面再研究。mCollections属性也暂时猜测不出来是干什么用的,而mHashes和mArray这两个属性和SparseArray的mKeys和mValues很像,就先猜想一致的。
Put方法
最关键的便是put方法,源码如下:
上面的逻辑很清晰,看完基本能了解算法了,也能猜测出个大概了:首先把key计算hashcode并转换成一个index,然后判断是否已经存在index,存在则覆盖,否则判断是否需要扩容和移动数组,最后把index值和key-value对设置在mHashes和mArray数组即可。
indexOf方法
再看关键方法indexOf()
这个方法的逻辑也很清晰,mHashes数组存放的是排序的key的hashcode,先从这里二分查找,如果找到,还需要调用equals判断,确认相同的key以后就返回;如果hashcode冲突了,则继续查找,依然没找到,则是一个新的冲突,返回反码,将会在这里插入新值。
其中indexOfNull()方法查找的hashcode是0,也就是说,如果key是null的话,默认存在第一个位置。
remove/get方法
再看两个重要的方法,都是经常用到的,源码如下:
可以看到这两个方法的关键都是indexOf,首先找到index然后就能确定value并get到返回了。如果是删除操作,则还会移动数组。
其他的关于MapCollections<K, V> mCollections
这个属性,其实是为了满足标准的map接口而实现的,以后去研究HashMap的时候再看。
2.2 结合SparseArray再理解
回想一下SparseArray,它是稀疏矩阵的实现,用两个数组实现,一个存放key(它的key是int类型),一个存放value;而key是按顺序排列的,当操作的时候用二分查找实现,kv两个数组必须一一对应。
同理在ArrayMap里面,它也是稀疏矩阵的实现,原理都和SparseArray一样的,用两个数组实现,不过key不再是int类型了,而是任意类型,那么int数组存放的就是key的hashcode,并且object数组不仅仅存放value了,还存放了key,所以,它是int数组大小的两倍,两个数组并不是一一对应的了,不过操作依然是根据int数组来二分查找的,结构如下:
int数组 | object数组 |
key1的hashcode | key1 |
</tr>
key2的hashcode | value1 |
null | key2 |
null | value2 |
2.3 关于缓存
除了上面所说的属性以外,它还有下面几个属性:
我们已经知道BASE_SIZE是4,这里又有一个为10的CACHE_SIZE,用来缓存的吗?另外,这四个static的属性又有什么用?之前分析put方法的时候,跳过了allocArrays()和freeArrays()方法,现在来看看里面的具体实现:
在ArrayMap里面找不到给mTwiceBaseCache这些变量赋值的地方,具体的原理目前我也还没弄懂,先留在这里,以后总会触机顿悟的,到时再来记录。
由此我们也知道了,不管是ArrayMap还是SparseArray都是比较轻量级的数据结构,适合那些数据量很小的场景,如果数据量小的话,使用重量级的数据结构,如HashMap等等,会影响性能;对于数据量大的时候就会性能差了,使用重量级的Java原生数据结构即可。