容器類原始碼解析系列(四)---SparseArray分析(最新版)
引言
容器類原始碼解析系列已經更新到了第四篇,前三篇已經分別對ArrayList、LinkedList、HashMap進行原始碼分析。
SparseArray 是用來儲存Key-Value這種對映關係的容器,它要求Key的型別必須是int型。
要點
-
Key的型別必須是int型;
-
SparseArray底層通過雙陣列的結構實現資料儲存,一個陣列用來儲存key值,一個陣列用來儲存value;
-
相較於HashMap,在儲存key(int型)-value資料時,SparseArray會更省記憶體,但是在資料量大的情況下,查詢效率沒有HashMap好。
-
Sparse可以儲存NULL值,沒有fail-fast機制;
關於fail-fast機制,容器類原始碼解析系列(一)—— ArrayList 原始碼分析(最新版) 這篇文章有詳細介紹。
構造方法
在看構造方法之前先看一下幾個重要成員變數。
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
複製程式碼
mKeys和mValues是我上面提到的那兩個陣列,分別用來儲存key和value的。mSize表示容器中key-value鍵值對的數量。 DELETED是什麼呢?還有mGarbage? 上面的要點中,我們提到,SparseArray在儲存資料時比HashMap更省記憶體,但是效率沒有HashMap高,SparseArray使用了二分查詢,這個我們在後面的原始碼分析中能夠看到。 所以SparseArray想了一個方法來提高效率,就用到了DELETED和mGarbage這兩個變數。這個方法是,在刪除資料時,沒有立馬把資料置空回收,重組陣列結構,而是先把要刪除的value先置為DELETED狀態,在後面合適的時機,mGarbage會被置為true,然後呼叫gc方法,統一清除DELETED狀態的資料,重新調整容器結構。而在這個過程中,如果有新新增的資料,是可以複用DELETED狀態對應的index的,這樣DELETED資料又會變成正常資料,不會被回收了。 這樣就避免了頻繁的回收調整次數。
/**
* Creates a new SparseArray containing no mappings.
*/
public SparseArray() {
this(10);
}
/**
* 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 == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;
}
複製程式碼
構造方法很簡單,就兩個構造方法,預設的不傳capacity引數的情況下建立的陣列長度是10。
常規操作
新增資料
/**
* 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) {
//通過二分查詢來找到mKeys陣列中對應key的index索引。
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) { //如果找到了,表示之前存過這個key,則覆蓋舊的value。
mValues[i] = value;
} else {
i = ~i;//取反,把負數變成正數。(註釋一)
if (i < mSize && mValues[i] == DELETED) {//如果這個key對應的value之前被刪除了,但是還沒有被執行gc操作,目前還是DELETED狀態,那麼就複用此index。(註釋二)
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,如果需要擴容,就像ArrayList那樣,通過copy操作來完成。
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);//插入新value
mSize++;//表示新新增了一對key-value鍵值對。
}
}
複製程式碼
上面在查詢key對應的索引時,使用了二分查詢二分搜尋演算法 。我們看下程式碼:
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final 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
}
複製程式碼
~ 操作是什麼意思呢?表示按位取反。
假設lo值為3;int是四個位元組,其二進位制表示為:00000000 00000000 00000000 00000011,那麼~3 就等於:
11111111 11111111 11111111 11111100 等於-4。
所以註釋一處的操作就好理解了。註釋二 的行為表現就是我們上面說到的DELETED狀態的妙用,用來提高效率的。
set 操作
public void setValueAt(int index, E value) {
if (mGarbage) {
gc();
}
mValues[index] = value;
}
複製程式碼
這裡要注意index的範圍要在0~size()-1之間。
刪除操作
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
複製程式碼
**可以看到,它在執行刪除操作時並沒有立馬把對應的value置為null。而是先設定為DELETED狀態,然後後面找到合適時機一致回收,這個期間該key是可以被複用的,如果被複用那麼DELETED狀態可以重新變成NORMAL狀態。**我們同時也注意到mGarbage這個標誌位在此刻被置為了true。
/**
* Removes the mapping at the specified index.
*
* <p>For indices outside of the range <code>0...size()-1</code>,
* the behavior is undefined.</p>
* 主要index的範圍問題
*/
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
複製程式碼
get操作
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.
*/
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
複製程式碼
gc
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
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) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
複製程式碼
在呼叫gc操作後,會對那些個DELETED狀態的value統一置為null ,方便回收。同時會對index進行一次重新排序。
我們看看有哪些操作可能會觸發SparseArray的gc方法 注意哦,我這篇文章裡說的gc操作,指的都是SparseArray內部的gc方法。
put(int key, E value) | size() | keyAt(int index) |
---|---|---|
valueAt(int index) | setValueAt(int index, E value) | indexOfKey(int key) |
indexOfValue(E value) | indexOfValueByValue(E value) | append(int key, E value) |
總結
android開發用如果儲存key-value下的key是int型的話,建議使用SparseArray容器來操作,可以減少記憶體消耗。
個人站
掃碼加入我的個人微信公眾號:Android開發圈 ,一起學習Android知識!!