说到SimpleArrayMap首先要说一下HashMap,HashMap是用数组与链表(JAVA8树),hash算法构造的一个key,value结构。
HashMap在存取的时候有O(1)的时间复杂度。同时也有以下缺点。
1.每个对象需要Entry来包一层,造成了额外的空间与创建对象的成本。
2.更多的对象造成了垃圾回收的成本加大。
3.桶并没有充分利用,桶扩容的时候需要很多时间。
4.如果哈希冲突严重,时间复杂度会降为O(N)。
由于在移动端内存与CPU都是很宝贵的资源。在Android中可以使用SimpleArrayMap来代替HashMap实现Map的功能,SimpleArrayMap内部使用了两个数组,一个是Hash数组mHashes,另一个是2倍大小的Object数组mArray。Object数组中使用key+value间隔存取的方式;另外Hash数组,则是对应的 Key 的Hash值数组,并且这是一个递增的int数组,这样在进行Key的查找时,可以使用二分查找。由于数组是连续存储的,对内存使用率高了很多。
默认的就是初始化了一个空的mHashes与mArray数组。另一个构造函数根据传入的值初始化一个数组,同时如果满足小于4或8会看能否使用缓存的废弃的数组,就直接用回收的数组。
/**
* Create a new empty ArrayMap. The default capacity of an array map is 0, and
* will grow once items are added to it.
*/
public SimpleArrayMap() {
mHashes = ContainerHelpers.EMPTY_INTS;
mArray = ContainerHelpers.EMPTY_OBJECTS;
mSize = 0;
}
/**
* Create a new ArrayMap with a given initial capacity.
*/
public SimpleArrayMap(int capacity) {
if (capacity == 0) {
mHashes = ContainerHelpers.EMPTY_INTS;
mArray = ContainerHelpers.EMPTY_OBJECTS;
} else {
allocArrays(capacity);
}
mSize = 0;
}
当调用SimpleArrayMap的get,put方法的时候,会根据key的hashCode值,在mHashes中进行二分查找找到位置乘以2就可以找到mArray数组中的key,value。
int indexOf(Object key, int hash) {
final int N = mSize;
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
int index = ContainerHelpers.binarySearch(mHashes, N, hash);
// If the hash code wasn't found, then we have no entry for this key.
if (index < 0) {
return index;
}
// If the key at the returned index matches, that's what we want.
if (key.equals(mArray[index<<1])) {
return index;
}
// Search for a matching key after the index.
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
if (key.equals(mArray[end << 1])) return end;
}
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
if (key.equals(mArray[i << 1])) return i;
}
// 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;
}
上述方法主要是用在对map的put与get方法中。
get方法就是通过上述indexOf返回key的hashCode的位置,然后可以算出key,value在mArray中的位置:indexOf乘以2就是key的位置,乘以2+1就是value的位置。
put方法就是通过二分查找看对应位置是否存在元素,存在直接替换mArray内部的数据。不存在则查看是否需要扩容,然后根据二分查找的位置取反,即是要hashCode要插入的位置,同时2倍与其加1,即是key,value在mArray的存储位置。数组的插入需要向右移动之后的元素。
SimpleArrayMap为了解决内存抖动问题,把一些废弃的hash数组与object数组会缓存起来,下次在分配内存的时候直接使用缓存的数组。
有两个重要的函数如下
private void allocArrays(final int size) 用来从缓存中分配数组的方法。等于8去mTwiceBaseCache链表中查找有没有,等于4去mBaseCache链表中查找,都没有就直接创建n大小的mHashes数组,2n大小的mArray数组。
private static void freeArrays(final int[] hashes, final Object[] array, final int size) 用来把废弃的数组加入到缓存中。如果等于8就放入mTwiceBaseCache链表,等于4就放入mBaseCache数组。缓存链表最大长度为CACHE_SIZE=10。
乍一看mBaseCache,mTwiceBaseCache不是两个静态数组嘛,怎么就是链表了呢,可以把SimpleArrayMap代码copy出来,然后断点调试查看,是如何缓存的。
SimpleArrayMap使用了两个静态数组来缓存废弃掉的hash数组与object数组,其实是所有废弃的数组组成了一个链表,mArray的第一个元素指向下一个mArray数组,mArray数组的第二个元素为mHashs,其他数组元素置为空如下图。
可以使用以下代码测试
public static void main(String[] args) {
SimpleArrayMap map1 = new SimpleArrayMap<>();
map1.put("k11", "value");
map1.put("k12", "value");
map1.put("k13", "value");
SimpleArrayMap map2 = new SimpleArrayMap<>();
map2.put("k21", "value");
map2.put("k22", "value");
map2.put("k23", "value");
SimpleArrayMap map3 = new SimpleArrayMap<>();
map3.put("k31", "value");
map3.put("k32", "value");
map3.put("k33", "value");
map3.put("k34", "value");
map1.clear();
map2.clear();
map3.clear();
SimpleArrayMap map4 = new SimpleArrayMap<>();
map4.put("k31", "value");
map4.put("k32", "value");
map4.put("k33", "value");
map4.put("k34", "value");
map4.put("k35", "value");
map4.put("k36", "value");
SimpleArrayMap map5 = new SimpleArrayMap<>();
map5.put("k41", "value");
map5.put("k42", "value");
map5.put("k43", "value");
map5.put("k44", "value");
map5.put("k45", "value");
map5.put("k46", "value");
SimpleArrayMap map6 = new SimpleArrayMap<>();
map6.put("k51", "value");
map6.put("k52", "value");
map6.put("k53", "value");
map6.put("k54", "value");
map6.put("k55", "value");
map6.put("k56", "value");
map6.put("k57", "value");
map6.put("k58", "value");
map4.clear();
map5.clear();
map6.clear();
}
都知道HashMap在put元素的时候,当达到装填因子的时候会扩容数组。
看SimpleArrayMap的put方法节选可以了解他的扩容机制
if (mSize >= mHashes.length) {
// 第一次是 mSize=0,增加为 BASE_SIZE=4
// 第二次是 mSize=4,增加为 BASE_SIZE*2=8
// 第三次是 mSize=8,增加为 mSize+(mSize>>1)=12
// 第四次是 mSize=8,增加为 mSize+(mSize>>1)=18
// 后边就是以1.5倍增长了
final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
: (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
// 得到的倍数会传给allocArrays方法,进行数组扩容,内部大概逻辑是,等于8去mTwiceBaseCache链表中查找有没有,等于4去mBaseCache链表中查找,都没有就直接创建n大小的mHashes数组,2n大小的mArray数组。
allocArrays(n);
if (mHashes.length > 0) {
if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
freeArrays(ohashes, oarray, mSize);
}
总结:由于SimpleArrayMap是连续存储的比HashMap节省内存,由于SimpleArrayMap会回收废弃的数组,在使用小于8的数组的时候,不必频繁的开辟空间。
几百行的SimpleArrayMap中使用到了二分查找,哈希,链表,缓存。很有学习的意义,当然也要好好学习数据结构算法。