說到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<String, String> map1 = new SimpleArrayMap<>();
map1.put("k11", "value");
map1.put("k12", "value");
map1.put("k13", "value");
SimpleArrayMap<String, String> map2 = new SimpleArrayMap<>();
map2.put("k21", "value");
map2.put("k22", "value");
map2.put("k23", "value");
SimpleArrayMap<String, String> 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<String, String> 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<String, String> 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<String, String> 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中使用到了二分查找,哈希,鏈表,緩存。很有學習的意義,當然也要好好學習數據結構算法。