引言
-
SparseArray
是在API level 1
就已經添加的適用于Android
的集合類,而ArrayMap
實在API level 19
才加入的集合類,雖說這兩者實在不同時期加入的,但是它們的目的只有一個,那就是在小數據量的情況下盡可能權衡內存占用以及使用效率,從而達到小數據量時能夠替換JDK
中類似于HashMap
之類的集合類模板。 - 由于兩者源碼實際上存在很多相似之處,因此就放一起看了,源碼還是比較容易理解的。
SparseArray
-
SparseArray
的數據構造可以從下面這幾行代碼得知,顯然是使用兩個數組分別存儲key
和value
的值,而這兩者又因元素索引存在對應關系而剛好形成元素間的相互映射,可以說是很簡單粗暴又有效了。public class SparseArray<E> implements Cloneable { // ... private int[] mKeys; private Object[] mValues; private int mSize; }
-
put方法:揭露核心的方法
- 通過源碼可以看出,
SparseArray
使用的是二分查找法來查找key所在的位置索引,如果存在則替換掉對應索引上的value,如果不存在,則將在索引取反后的位置上添加元素,這點跟JDK
中的Map
是一樣的行為,存在則覆蓋。
public void put(int key, E value) { // 二分查找算法 找到 key,也就是索引位置 int i = ContainerHelpers.binarySearch(mKeys, mSize, key); if (i >= 0) { // 存在 mValues[i] = value; // value數組中找到key位置上的value } else { // 不存在 i = ~i; if (i < mSize && mValues[i] == DELETED) { mKeys[i] = key; mValues[i] = value; return; } if (mGarbage && mSize >= mKeys.length) {//可能value元素已經被刪除了 gc(); // 那么chufa一次gc // 中心搜索一遍 i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); } mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } }
- 這里的
gc
并非JVM
中的那個GC
,而是說當我們刪除了某個元素之后,被刪除元素所占用的那個位置上的數據就標記成了垃圾數據,然后就會通過gc
來去除這個位置上的元素,而本質上,對于數組而言,就是挪動位置覆蓋掉這個位置咯。
private void gc() { int n = mSize; int o = 0; int[] keys = mKeys; Object[] values = mValues; for (int i = 0; i < n; i++) { Object val = values[i]; if (val != DELETED) { if (i != o) { // 相當于把key、value元素都向前挪動一次 keys[o] = keys[i]; values[o] = val; values[i] = null; } o++; } } mGarbage = false; mSize = o; }
- 上面代碼中的
mGarbage
會在刪除元素時被設置為true
,也就是說標記這個位置上的元素為垃圾數據。
public void delete(int key) { int i = ContainerHelpers.binarySearch(mKeys, mSize, key); if (i >= 0) { if (mValues[i] != DELETED) { mValues[i] = DELETED; mGarbage = true; } } }
- 通過源碼可以看出,
SparseArray 綜述
特點:使用
int
數組作為map
的key
容器,Object
數組作為value
容器,使用索引對應的形式組成key-value
這使得SparseArray
可以不按照像數組索引那樣的順序來添加元素。可看成增強型的數組或者ArrayList
。查找:使用二分查找法查找
key
在數組中的位置,然后根據這個數組位置得到對應value
數組中的value
值。優劣:相對于
HashMap
,合理使用SparseArray
可以節省大量創建Entry
節點時產生的內存,不需要拆箱裝箱操作,提高性能,但是因為基于數組,插入和刪除操作需要挪動數組,已經使用了時間復雜度為O(logN)
的二分查找算法,相對HashMap
來說,非常消耗性能,當數據有幾百條時,性能會比HashMap低近50%,因此SparseArray
適用于數據量很小的場景。-
使用場景舉例:
-
通過
View id
來映射View
對象實例。 一個非常現實的場景是,當我們使用RecyclerView
時,我們可能需要創建若干個ViewHolder
,特別是如果一個列表包含若干中布局類型的時候,而如果我們不適用類似于ButterKnife
或DataBinding
這類工具的話,那么我們需要對每個控件
進行findViewById
,這是件很糟心的事情,那么我們可以怎么簡化呢?欸,這種情況可以使用SparseArray
來緩存一下我們的View
(實際上ArrayMap
、HashMap
等也可以),比方說以下示例代碼:
public class CommViewHolder extends RecyclerView.ViewHolder { private SparseArray<View> mViewCache; private Object tag; public CommViewHolder(@NonNull View itemView) { super(itemView); mViewCache = new SparseArray<>(); } public <T extends View> T getView(@IdRes int id) { T t = (T) mViewCache.get(id); if (t == null) { t = itemView.findViewById(id); mViewCache.put(id, t); } return t; } public void releaseCache(){ mViewCache.clear(); mViewCache = null; } public void setTag(Object tag) { this.tag = tag; } public Object getTag() { return tag; } }
通過上面這個通用的
ViewHolder
,我們就可以應用于任意的布局類型,而不用每個都去寫一個對應的ViewHolder
了,可以說很方便了。 -
通過
ArrayMap
- 通過閱讀
ArrayMap
的源碼可以發現,它和SparseArray
簡直就是親生兄弟啊,不同點就是,ArrayMap
具備完整的Map
特性,因為實現了Map
,并且具備哈希表的相關特性。public final class ArrayMap<K, V> implements Map<K, V> { final boolean mIdentityHashCode; int[] mHashes; // 存儲哈希值 Object[] mArray; // 存儲元素 int mSize; MapCollections<K, V> mCollections; }
-
我們先來看看查找索引
index
源碼,可以看到想要得到index
,需要先使用二分查找法去mHashs
數組中找出這個hash
在數組中的位置,而這個位置就是index
。這里就必然存在幾種情況,具體可以看下面的注釋。int indexOf(Object key, int hash) { final int N = mSize; // 使用二分查找算法搜索元素位置 int index = binarySearchHashes(mHashes, N, hash); // 1. 不存在相關該hash if (index < 0) { return index; } // 2. 存在該hash,且對應位置上有對應key if (key.equals(mArray[index<<1])) { return index; } // 3. 存在該hash,但是對應位置上無對應key, 也就是說沖突了 // 那么先搜索后半部分,之所以分成兩半來查找是為了縮小查詢范圍,提升搜索速度 // 這實際實在賭博,賭目標值在數組后半段 ,最終能否提升速度就看是不是在數組后半段了 int end; for (end = index + 1; end < N && mHashes[end] == hash; end++) { if (key.equals(mArray[end << 1])) return end; } // 再搜索前半部分 for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { if (key.equals(mArray[i << 1])) return i; } // 對應key實在沒找到,說明確實是沖突了,那么返回個mHashes數組大小的取反值(負數) return ~end; }
-
有了上面的分析過程,我們已經了解了
ArrayMap
的index
是如何決定的了,那么通過put
方法就可以比較直觀地看出ArrayMap
的存儲過程了,首先會計算我們給定key
的哈希值,然后通過這個哈希值去查找index
,如果在這個index
上已經元素,那么替換這個元素,如果不存在,那么將數據存入數組中;如果存在沖突,則校檢一下數組容量(看看需不需要擴容),然后存入數組。public V put(K key, V value) { final int osize = mSize; final int hash; int index; // 1. 查找hash,計算index if (key == null) { hash = 0; index = indexOfNull(); } else { hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode(); index = indexOf(key, hash); } // 2. 根據index存值 // 存在,則覆蓋掉舊元素,并返回舊元素 if (index >= 0) { index = (index<<1) + 1; final V old = (V)mArray[index]; mArray[index] = value; return old; } // 有沖突的情況 index = ~index; // 數組可用空間不夠,那么擴容 if (osize >= mHashes.length) { final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1)) : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); final int[] ohashes = mHashes; final Object[] oarray = mArray; allocArrays(n); // 擴容 // ... if (mHashes.length > 0) { // 轉移元素到新數組 System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); System.arraycopy(oarray, 0, mArray, 0, oarray.length); } freeArrays(ohashes, oarray, osize); // 清理就數組 } // 3. 存值 if (index < osize) { System.arraycopy(mHashes, index, mHashes, index + 1, osize - index); System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1); } mHashes[index] = hash; // key和value存在同一個數組上 mArray[index<<1] = key; mArray[(index<<1)+1] = value; mSize++; return null; }
上面有個比較新穎的地方,就是它把
key
和value
都存到了一個數組中去了,也就是mArray
數組,key
在前,value
在后,同index
的關系如下圖:
ArrayMap 綜述
-
特點
- 實現了
Map
接口,并使用int[]
數來存儲key
的hash
值,數組的索引用作index
,而使用Object[]
數組來存儲key<->value
,這還是比較新穎的 。 - 使用二分查找查找
hash
值在key
數組中的位置,然后根據這個位置得到value
數組中對應位置的元素。 - 和
SparseArray
類似,當數據有幾百條時,性能會比HashMap低50%,因此ArrayMap
適用于數據量很小的場景
- 實現了
-
ArrayMap和HashMap的區別?
- ArrayMap的存在是為了解決HashMap占用內存大的問題,它內部使用了一個int數組用來存儲元素的hashcode,使用了一個Object數組用來存儲元素,兩者根據索引對應形成key-value結構,這樣就不用像HashMap那樣需要額外的創建Entry對象來存儲,減少了內存占用。但是在數據量比較大時,ArrayMap的性能就會遠低于HashMap,因為 ArrayMap基于二分查找算法來查找元素的,并且數組的插入操作如果不是末尾的話需要挪動數組元素,效率較低。
- 而HashMap內部基于數組+單向鏈表+紅黑樹實現,也是key-value結構, 正如剛才提到的,HashMap每put一個元素都需要創建一個Entry來存放元素,導致它的內存占用會比較大,但是在大數據量的時候,因為HashMap中當出現沖突時,沖突的數據量大于8,就會從單向鏈表轉換成紅黑樹,而紅黑樹的插入、刪除、查找的時間復雜度為O(logn),相對于ArrayMap的數組而言在插入和刪除操作上要快不少,所以數據量上百的情況下,使用HashMap會有更高的效率。
如何解決沖突問題? 在
ArrayMap
中,假設存在沖突的話,并不會像HashMap
那樣使用單向鏈表或紅黑樹來保留這些沖突的元素,而是全部key
、value
都存儲到一個數組當中,然后查找的話通過二分查找進行,這也就是當數據量大時不宜用ArrayMap
的原因了。