本文SimpleArrayMap源碼分析是基于support v4 23.3.0版本的。
另外,因ArrayMap涉及的多是算法知識,而主要的思想比較簡單,所以本文會主要以代碼為主,細講其每個實現。
為什么要引入ArrayMap?
在Android設備上,因為App的內存限制,出現OOM的錯誤,導致開發者不得不關注一些底層數據結構以及去分析App的內存使用情況。提及數據結構,HashMap是我們最經常使用到的,而我們是否會注意其實現的細節以及有什么優缺點呢?
這里簡單提及一下HashMap在擴容時采取的做法是:將當前的數據結構所占空間*2,而這對安卓稀缺的資源來說,可是非常大的消耗。所以就誕生了ArrayMap,它是在API19引入的,這樣我們在兼容以前版本的時候,support包就派上用場了,可是為什么不直接是使用ArrayMap,而會多出來一個SimpleArrayMap呢?不得不說這是谷歌的厚道、人性化處,考慮我們使用ArrayMap時,可能不需要使用Java標準的集合API,而給我們提供的一個純算法實現的ArrayMap。
上面提到的集合API,是SimpleArrayMap跟v4包中的ArrayMap最大的區別,證明就是ArrayMap繼承了SimpleArrayMap,又實現了Map的接口;主要的操作,則是通過引入MapCollections類,使用Map中的Entry結構,這樣在ArrayMap中就可以通過Iterator
來進行數據的的迭代操作。
實現思想
簡單地了解一下其思想,是我們接下來進行源碼分析的必要步驟,方便我們帶著問題去驗證我們所想。兵馬未動,糧草先行。做事前一定要先把準備工作做好,事情理順,盡量地充分考慮工作的細節 ,再開始進行工作。正如我們現在項目開發之前,一定要先進行任務點的分解,而這時思維導圖、UML建模工具則是我們必須玩轉的東西。
- 思想:SimpleArrayMap采用了兩個數組來進行hash值與key、value值得保存,另外,數組大小超過8時,并需要進行擴容時,只增大當前數組大小的一半,并對大小為4和8的數組進行緩存。這樣最后帶來的好處就是最大程度保證了數組空間都能夠被使用,一定程度上避免了內存空間的浪費。
- 數據結構方式:使用了兩個數組,一個是Hash數組,另一個是大小*2的Array數組,為了保證通用性,這里所使用的是Object數組。Array數組中使用key+value間隔存取的方式,偶數為即
0 -> key1 1 -> value1 2 -> key2 3 -> value2
。另外Hash數組,則是對應的Key的Hash值數組,并且這是一個有序的int數組,這樣在進行Key的查找時,使用二分查找則是最有效率的方式了。如下圖:
數據結構定義
1.數據結構
int[] mHashes;
Object[] mArray;
int mSize;
代碼中,mHashes
數組為mArray
中的key對應的hash值得數組,而mArray
即是HashMap
中key與value間隔混合的一個數組。
2.初始化
- 默認構造器(初始大小為0)
/**
* 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賦值
/**
* Create a new ArrayMap with the mappings from the given ArrayMap.
*/
public SimpleArrayMap(SimpleArrayMap map) {
this();
if (map != null) {
putAll(map);
}
}
3.釋放
/**
* Make the array map empty. All storage is released.
*/
public void clear() {
if (mSize != 0) {
freeArrays(mHashes, mArray, mSize);
mHashes = ContainerHelpers.EMPTY_INTS;
mArray = ContainerHelpers.EMPTY_OBJECTS;
mSize = 0;
}
}
代碼中提及的EMPTY_INTS
及EMPTY_OBJECTS
,僅僅如下的兩個空數組:
static final int[] EMPTY_INTS = new int[0];
static final Object[] EMPTY_OBJECTS = new Object[0];
算法
1. 存數據put(key, value)
存數據的操作,按我們數據結構的定義,應該是需要針對key,獲取其對應的hash值,在Hash數組中,采取二分查找,定位到指定hash值所對應的index值;之后根據index值,來調整并存放key跟value的值。來看看源碼的實現吧:
/**
* Add a new value to the array map.
* @param key The key under which to store the value. <b>Must not be null.</b> If
* this key already exists in the array, its value will be replaced.
* @param value The value to store for the given key.
* @return Returns the old value that was stored for the given key, or null if there
* was no such key.
*/
public V put(K key, V value) {
final int hash;
int index;
if (key == null) {
// 查找key為null的情況
hash = 0;
index = indexOfNull();
} else {
hash = key.hashCode();
index = indexOf(key, hash);
}
if (index >= 0) {
// 數組中存在相同的key,則更新并返回舊的值
index = (index<<1) + 1;
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
index = ~index;
if (mSize >= mHashes.length) {
// 當容量不夠時,需要建立一個新的數組,來進行擴容操作。
final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
: (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = 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);
}
// 將index之后的數據進行后移
if (index < mSize) {
if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
+ " to " + (index+1));
System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
}
// 賦值給index位置上hash值
mHashes[index] = hash;
// 更新array數組中對應的key跟value值。
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
代碼中,可以看出arrayMap允許key為空,所有的key都不能重復。
另外,在進行容量修改的時候,進行的操作是:mSize跟hash數組長度的判斷,當大于等于的時候,需要對數組的容量進行一些擴容,并拷貝數組到新的數組中。(擴容操作:當size大于8, 取size + size /2 ; 當size大于4小于8時, 取8 ,當size小于4時,取4)
2. 取數據get(key)
/**
* Retrieve a value from the array.
* @param key The key of the value to retrieve.
* @return Returns the value associated with the given key,
* or null if there is no such key.
*/
public V get(Object key) {
final int index = indexOfKey(key);
return index >= 0 ? (V)mArray[(index<<1)+1] : null;
}
通過key來獲取數據就非常簡單了,根據key獲取到相應的index值,在array數據中根據index乘2加1返回相應的value即可。
3. 刪除數據remove(key)
/**
* Remove an existing key from the array map.
* @param key The key of the mapping to remove.
* @return Returns the value that was stored under the key, or null if there
* was no such key.
*/
public V remove(Object key) {
final int index = indexOfKey(key);
if (index >= 0) {
return removeAt(index);
}
return null;
}
根據key來刪除時,先會根據key來獲取其對應的index值,再通過removeAt(int index)
方法來進行刪除操作。
/**
* Remove the key/value mapping at the given index.
* @param index The desired index, must be between 0 and {@link #size()}-1.
* @return Returns the value that was stored at this index.
*/
public V removeAt(int index) {
final Object old = mArray[(index << 1) + 1];
if (mSize <= 1) {
// Now empty.
if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
freeArrays(mHashes, mArray, mSize);
mHashes = ContainerHelpers.EMPTY_INTS;
mArray = ContainerHelpers.EMPTY_OBJECTS;
mSize = 0;
} else {
// 滿足條件,對數組進行加入緩存的操作。
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
// Shrunk enough to reduce size of arrays. We don't allow it to
// shrink smaller than (BASE_SIZE*2) to avoid flapping between
// that and BASE_SIZE.
final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2);
if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);
mSize--;
if (index > 0) {
if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, index);
System.arraycopy(oarray, 0, mArray, 0, index << 1);
}
if (index < mSize) {
if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize
+ " to " + index);
System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index);
System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
(mSize - index) << 1);
}
} else {
mSize--;
if (index < mSize) {
if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize
+ " to " + index);
System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index);
System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
(mSize - index) << 1);
}
mArray[mSize << 1] = null;
mArray[(mSize << 1) + 1] = null;
}
}
return (V)old;
}
這里先忽略hash數組長度的判斷(主要進行數組緩存的操作)只看主要的代碼,即最后的一個else的代碼,使用System.arraycopy方法將hash數組跟array數組中index之后的數據往前移動1位,而將最后一位的數據進行至空。
4. indexOfKey (key)
上面代碼中,都可以看到indexOfKey
身影的出現,來看到其中如何實現的:
/**
* Returns the index of a key in the set.
*
* @param key The key to search for.
* @return Returns the index of the key if it exists, else a negative integer.
*/
public int indexOfKey(Object key) {
return key == null ? indexOfNull() : indexOf(key, key.hashCode());
}
由上發現允許key為null,進行index的查詢,當key不為空時,通過key及其key的hashCode,來進行查詢。
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;
}
代碼中,是先對Hash數組進行二分查找,獲取index,之后根據index獲取hash數組中對應的值,通過與key來比較是否相等,相等則直接返回,若不相等,則先從index之后的數據進行比較,沒找到,則再找之前的數據。可以看出這樣是支持存在多個key的hash值相同的情況,那再看看支不支持多個key為null的情況呢?
int indexOfNull() {
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, 0);
// 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 (null == mArray[index<<1]) {
return index;
}
// Search for a matching key after the index.
int end;
for (end = index + 1; end < N && mHashes[end] == 0; end++) {
if (null == mArray[end << 1]) return end;
}
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
if (null == 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;
}
從上可以看出當key為null的時候,采取獲取的方法跟key不為null獲取是很相似的了,都要進行整個數組的遍歷,不過這里對應的hash都是為0。但key為null只能在數組中存在一個的,因為在數據的put操作的時候,會對key進行檢查,這樣保證了key為null只能存在一個。
5.二分查找
這里,回顧一下,上面代碼中一直會用到的,經典的二分查找的算法:
// This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
int midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
代碼中,采用右移操作來進行除2的操作,而通過三個大于號,則表示無符號操作。
緩存的實現
講到這里,就基本可以結束了,而源碼中看到了兩個神奇的數組,他倆主要的目的是對固定的數組來進行緩存,官方給的說法是避免內存抖動,畢竟這里是純數組來實現的,而當數組容量不夠的時候,就需要建立一個新的數組,這樣舊的數組不就浪費了,所以這里的緩存還是灰常必要的。接下來看看他倆是怎樣玩的,不感興趣的可以略過這里了。先看一下數據結構的實現:
1.數據結構
/**
* 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;
/**
* Maximum number of entries to have in array caches.
*/
private static final int CACHE_SIZE = 10;
/**
* Caches of small array objects to avoid spamming garbage. The cache
* Object[] variable is a pointer to a linked list of array objects.
* The first entry in the array is a pointer to the next array in the
* list; the second entry is a pointer to the int[] hash code array for it.
*/
static Object[] mBaseCache;
static int mBaseCacheSize;
static Object[] mTwiceBaseCache;
static int mTwiceBaseCacheSize;
代碼中有兩個靜態的Object數組,這兩個靜態數組采用鏈表的方式來緩存所有的數組。即Object數組會用來指向array數組,而這個array的第一個值為指針,指向下一個array,而第二個值是對應的hash數組,其他的值則為空。另外,緩存數組即baseCache和twiceBaseCache,它倆大小容量的限制:最小值為4,最大值為10,而BaseCache數組主要存儲的是容量為4的數組,twiceBaseCache主要存儲容量為8的數組。如圖:
2.緩存數據添加
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");
}
}
}
}
這個方法主要調用的地方在于ArrayMap進行容量改變時,代碼中,會對當前數組的array進行清空操作,但第一個值指向之前cache數組,第二個值指向hash數組。
3.緩存數組使用
private void allocArrays(final int size) {
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];
}
這個時候,當size跟緩存的數組大小相同,即要么等于4,要么等于8,即可從緩存中拿取數組來用。這里主要的操作就是baseCache指針的移動,指向array[0]指向的指針,hash數組即為array[0],而當前的這個array咱們就可以使用了。
總結
- SimpleArrayMap是可以替代ArrayMap來使用的,區別只是其內部采用單純的數組來實現,而ArrayMap中采用了EntrySet跟KeySet的結構,這樣方便使用
Iterator
來數據的遍歷獲取。 - ArrayMap適用于少量的數據,因為存取的復雜度,對數量過大的就不太合適。這個量筆者建議破百就放棄ArrayMap的使用吧。
- ArrayMap支持key為null,但數組只能有一個key為null的存在。另外,允許多個key的hash值相同,不過盡量避免吧,不然二分查找獲取不到,又會進行遍歷查找;而key都必須是唯一,不能重復的。
- 主要目的是避免占用大量的內存切無法得到地充分利用。
- 對容量為4和容量為8的數組,進行緩存,來防止內存抖動的發生。
PS: 轉載請注明原文鏈接