一、概述
最近看一些關于Android性能優化方面的書,有講到了使用Android提供的SparseArray代替Java的HashMap來存儲key-value鍵值對,一定程度上能夠提升性能,但是SparseArray也有一定的局限性,比如key的類型固定為int,存儲的元素個數過大時對性能有較大的影響等。平時使用過SparseArray,但是未進行深入研究,藉此機會來學習一下。
二、使用SparseArray示例
SparseArray采用泛型(
SparseArray<E>
)來支援存儲任意引用類型的元素,它的使用是比較簡單的,因為它的key固定為int類型,是以隻需指定value的類型即可。下面為了示範,采用String作為value的類型。實際使用時指定我們需要的類型即可,最終存儲的key-value的類型分别為
<int,E>
。
示例如下,列舉了常用的幾個方法(更多方法可以檢視其api):
// 建立SparseArray,指定存儲的值的類型為String
SparseArray<String> sparseArray = new SparseArray<>();
for (int i = ; i < ; i++) {
sparseArray.put(i, "element" + i); // put插入元素
}
// get擷取元素值
System.out.println("element value:"+sparseArray.get());
// 删除元素
sparseArray.delete();
// 擷取key對應的索引
int index = sparseArray.indexOfKey();
// valueAt根據索引擷取元素值
System.out.println("element value:"+sparseArray.valueAt(index));
// 删除一個範圍的值,從index起始,删除6個
sparseArray.removeAtRange(index, );
三、SparseArray源碼分析
SparseArray是一種存儲key-value鍵值對的容器,前面使用時我們就發現,隻需指定value的類型即可,那是因為它内部對key的類型已經做了嚴格限定(隻允許為int類型),是以隻需指定value的類型即可,并且容器存儲的value隻允許為引用類型。
SparseArray将key和value分别使用數組存儲,使用數組位置的索引作為key-value的映射紐帶,通過二分查找key對應的索引,進而通過索引找到value的值。對元素的存儲也有一套管理機制(gc調節),處理已經處于DELETED 狀态的元素達到複用,使存儲空間達到最優利用。
下面我們通過SparseArray的使用流程,來一步一步解剖它。
1. 建立SparseArray
建立SparseArray隻需要一句話,如下:
SparseArray<String> sparseArray = new SparseArray<>();
實際上這是建立一個預設的SparseArray,它初始大小為10。檢視源碼可知,它是調用了另一個構造方法,初始化了一個大小為10的容器。
/**
* Creates a new SparseArray containing no mappings.
*/
public SparseArray() {
this();
}
/**
* Creates a new SparseArray containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings. If you supply an initial capacity of 0, the
* sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
public SparseArray(int initialCapacity) {
if (initialCapacity == ) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = ;
}
看到這裡,出現了好幾個全局變量:mKeys ,mValues ,mSize ,它們的定義如下:
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys; // 存儲key的數組容器,類型為int
private Object[] mValues; // 存儲value的數組容器,類型為Object,可存儲任意引用類型
private int mSize; //實際存儲的元素個數
可以發現key、value是單獨存儲的,對于上面的mKeys ,mValues ,mSize 都是比較好了解的,但是DELETED 常量是什麼意思?見名知意,它确實表示一種删除的對象,當對mValues中的元進行删除時,實際上是指派為DELETED 的,表示目前元素被删除了,對DELETED 的了解可以認為目前元素是無意義的或者是已經删除的。
那麼mGarbage 又是什麼?從名字上看,似乎與垃圾回收有關系,誠然它對SparseArray存儲空間的動态管理起着重要的作用,在後面後詳細分析它。
2. SparseArray存儲元素
SparseArray使用put方法來存儲元素,該方法定義如下:
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, E value) {// 插入 , key是有序存儲的,從小到大
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 二分查找key是否存在,傳回key的索引
if (i >= ) { // key存在則直接覆寫value
mValues[i] = value;
} else { // key不存在
i = ~i; // i= -i-1 ; 如i=-15 , 則i = ~i後i=14
if (i < mSize && mValues[i] == DELETED) { // 如果要存儲的索引對應的值是無意義的,則直接覆寫
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) { // 記憶體管理,對數組進行調整
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); // 将key插入指定位置i
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); // 将value插入指定位置i
mSize++;
}
}
可以發現對key進行二分查找,擷取key在mKeys中的索引,然後根據這個索引來存儲value。是以,對mKeys來說是有序的(二分查找的保證),并且根據實作來看是從小到大的。
搜尋key的索引可能不存在,此時二分查找傳回的i是實際應該插入的位置index取負數-1(-index-1,即~i),此時進行 i = ~i;操作,即可得到應該插入的位置index,當然這個位置上的value有2中可能情況:
- 1.value可能是DELETED,此時直接進行覆寫就可以了
- 2.value的值是有意義的,此時需要騰出位置來以供插入
對于第二種情況,實作是由GrowingArrayUtils的insert方法來完成的,如下:
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
assert currentSize <= array.length;
// 插入索引在數組範圍内,即數組長度夠,不需重新開辟空間
if (currentSize + <= array.length) {
// 将從index起的數組元素都後移一位,騰出位置
System.arraycopy(array, index, array, index + , currentSize - index);
// 插入到騰出的index位置
array[index] = element;
return array;
}
// 數組空間不夠,重新開辟空間
T[] newArray = (T[]) Array.newInstance(array.getClass().getComponentType(),
growSize(currentSize));
System.arraycopy(array, , newArray, , index); // 拷貝0~index-1 到新數組
newArray[index] = element; // 插入到index位置
// 拷貝index 開始的原數組元素到新數組的index + 1起始,拷貝剩餘的元素
System.arraycopy(array, index, newArray, index + , array.length - index);
return newArray; // 最後傳回新建立的數組
}
特别說明:
arraycopy函數是System類的一個靜态函數,用于将一個數組的内容拷貝到另一個數組。
函數原型
public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
- 從指定源數組中複制一個數組,複制從指定的位置開始,到目标數組的指定位置結束。
- 源數組中位置在 srcPos 到 srcPos+length-1 之間的元件被分别複制到目标數組中的 destPos 到 destPos+length-1 位置。
參數:
- src - 源數組。
- srcPos - 源數組中的起始位置。
- dest - 目标數組。
- destPos - 目标資料中的起始位置。
- length - 要複制的數組元素的數量。
當數組長度不大于4時,指定增長為8,否則增長原來長度的兩倍,增長規律通過如下函數控制:
public static int growSize(int currentSize) {
return currentSize <= ? : currentSize * ;
}
使用ContainerHelpers的binarySearch方法進行二分查找,對于其中的二分算法如下,沒有什麼好說的:
// This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = ;
int hi = size - ;
while (lo <= hi) {
final int mid = (lo + hi) >>> ;
final int midVal = array[mid];
if (midVal < value) {
lo = mid + ; // mid + 1 ~ hi
} else if (midVal > value) {
hi = mid - ; // lo ~ mid - 1
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
2. SparseArray擷取元素
SparseArray擷取元素使用 get方法,定義如下:
/**
* Gets the Object mapped from the specified key, or <code>null</code>
* if no such mapping has been made.
*/
public E get(int key) {
return get(key, null);
}
/**
* Gets the Object mapped from the specified key, or the specified Object
* if no such mapping has been made.
*/
public E get(int key, E valueIfKeyNotFound) {
// 二分查找key是否存在,傳回key的索引
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 不存在key或key對應的值為DELETED,則傳回指定的預設值valueIfKeyNotFound
if (i < || mValues[i] == DELETED) { return valueIfKeyNotFound;
} else {
return (E) mValues[i]; // 存在key則傳回mValues中相同索引的value
}
}
可以發現,擷取元素是非常簡單的,兩個方法,要麼不指定預設值,不存在則傳回null;要麼指定預設值valueIfKeyNotFound,不存在則傳回之。和存儲元素類似,使用二分查找獲得索引,然後得到value。
還有類似的valueAt、keyAt等就不一一分析了,原理大緻相同。
3. SparseArray删除元素
SparseArray使用delete方法,通過key來删除一組key-value映射,該方法定義如下:
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 二分查找key是否存在,傳回key的索引
if (i >= ) { // 存在key
if (mValues[i] != DELETED) { // value有意義
mValues[i] = DELETED; // 指派為DELETED
mGarbage = true; // 垃圾回收flag置為true
}
}
}
delete方法實作比較簡單,主要有幾個關鍵點:
- 1.使用二分查找得到索引,和之前一樣
- 2.對要删除的value指派為DELETED,即将此處的value置為無意義的
- 3.置mGarbage 為true,為記憶體空間的管理做準備
4. SparseArray記憶體管理
上面多次見過gc方法,那它是幹什麼的呢?對于一個容器而言,增删是很平常的事,是以如何管理記憶體是一個關鍵點。SparseArray對記憶體管理使用gc方法來實作,定義如下:
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize;
int o = ;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = ; i < n; i++) { // 循環周遊,将數組後面有意義的值,移動到前面為DELETED的位置
Object val = values[i];
if (val != DELETED) {
if (i != o) { // 将i處的key-value映射前移到o處
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o; // 調整後的數組的元素的真實個數
// Log.e("SparseArray", "gc end with " + mSize);
}
這個方法比較關鍵,對我們掌握SparseArray原理至關重要。通常有兩個條件同時滿足時,才觸發gc操作(某些方法隻需要mGarbage為true,如size()方法):
- mGarbage
- mSize >= mKeys.length
mGarbage 标志着是否有元素存在垃圾狀态,即被置為DELETED
mSize >= mKeys.length 表示此時存儲的元素個數已滿,看看是否需要調整
對于gc的原理是這樣的:當滿足gc條件後,執行gc,周遊所有mValues的元素,發現DELETED狀态,則記錄下來(o),在後面的周遊發現不為DELETED時,則将目前的元素key-value映射移動到之前記錄的o處,這樣使mValues數組的前面x項都被填充有意義的值,後面的空間都騰出來,使數組能夠充分利用空間,而不是再開辟新空間來容納元素。
舉個栗子:
gc前的mValues假設是這樣子的,目前mSize=7 :
element1, element2,element3,DELETED,element5,DELETED,element7
注:DELETED元素一般是删除了元素造成的,會使mGarbage=true
gc後的mValues變成了如下,目前mSize=5:
element1, element2,element3,element5,element7,null,null
gc後使得mValues的空間得以較好的利用,而不必從新開辟空間造成浪費。
5. SparseArray 使用index擷取key與value
在了解上面的内容的基礎上,看懂下面代碼也比較容易了:
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseArray stores.
*
* <p>The keys corresponding to indices in ascending order are guaranteed to
* be in ascending order, e.g., <code>keyAt(0)</code> will return the
* smallest key and <code>keyAt(size()-1)</code> will return the largest
* key.</p>
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
*/
public int keyAt(int index) { // 未做index檢查,可能導緻異常産生
if (mGarbage) {
gc();
}
return mKeys[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the value from the <code>index</code>th key-value mapping that this
* SparseArray stores.
*
* <p>The values corresponding to indices in ascending order are guaranteed
* to be associated with keys in ascending order, e.g.,
* <code>valueAt(0)</code> will return the value associated with the
* smallest key and <code>valueAt(size()-1)</code> will return the value
* associated with the largest key.</p>
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
*/
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
從上面的注釋可以得到:
- 在得到key或value時,可能會觸發gc操作
- keyAt(0)獲得最小的key,keyAt(size()-1)可獲得最大的key
- valueAt(0)傳回的是最小key對應的value,valueAt(size()-1)傳回的是最大key對應的位置
- index的範圍在0…size()-1其他值是不被支援的
6. SparseArray 删除指定索引、指定範圍以及清空
删除指定索引處
/**
* Removes the mapping at the specified index.
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
*/
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED; // 直接指派為DELETED
mGarbage = true;
}
}
删除範圍,利用removeAt來完成
/**
* Remove a range of mappings as a batch.
*
* @param index Index to begin at
* @param size Number of mappings to remove
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
*/
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
removeAt(i);
}
}
清空 容器
/**
* Removes all key-value mappings from this SparseArray.
*/
public void clear() {
int n = mSize;
Object[] values = mValues;
for (int i = ; i < n; i++) {
values[i] = null; // 清空時會将元素的值指派為null
}
mSize = ;
mGarbage = false;
}
注意:
- removeAt、removeAtRange删除元素會導緻mGarbage 被置為true,但不會使mSize變化
- clear操作會将所有元素的值指派為null,并且置mSize為0
四、總結
- 1.對于SparseArray而言,對其key-value映射方式(index,二分查找),插入元素的處理(直接覆寫還是需要拓展空間)、以及記憶體管理(gc)的了解是較為關鍵的。
- 2.對于SparseArray 而言,适用于存在數量不多的資料(通常推薦是1000以内),這樣才能達到性能較優。畢竟當數量大了以後,每當有删除操作,接下來的操作(比如keyAt)都可能導緻gc,會比較消耗記憶體,影響性能。
- 3.由于key被限定為int類型,是以在選擇時需要考慮存入的元素的key的類型,這需要結合具體的情況綜合考慮與運用。
- 4.SparseArray 為Android提供的容器,用來替代HashMap等Java類容器,是以使用範圍有一定的平台局限性。
- 5.為了達到Android前後版本的相容性,可以考慮使用v4包的SparseArrayCompat來代替SparseArray。