深入剖析 Android中的 ArrayMap

數據集合在任何一門編程語言中都是很重要的一部分,在 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的一種替代,但是我們在使用時也要注意選擇適宜的場景,切莫一概而論。

發佈了15 篇原創文章 · 獲贊 9 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章