數據集合在任何一門編程語言中都是很重要的一部分,在 Android 開發中,我們會實用到ArrayList, LinkedList, HashMap等。其中HashMap是用來處理鍵值對需求的常用集合。 而Android中引入了一個新的集合,叫做ArrayMap,爲鍵值對存儲需求增加了一種選擇。
ArrayMap是什麼
- 一個通用的key-value映射數據結構
- 相比HashMap會佔用更少的內存空間
- android.util和android.support.v4.util都包含對應的ArrayMap類
ArrayMap的內部結構
如上圖所示,在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表中二分查找確定合適的位置。
- 如果新添加的數據的索引不是最後位置,在需要對這個索引之後的全部數據向後移動
-
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算作0
index = 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的一種替代,但是我們在使用時也要注意選擇適宜的場景,切莫一概而論。