SparseArray詳解及原始碼簡析
一、前言
SparseArray 是 Android 在 Android SdK 為我們提供的一個基礎的資料結構,其功能類似於 HashMap。與 HashMap 不同的是它的 Key 只能是 int 值,不能是其他的型別。
二、程式碼分析
1. demo 及其簡析
首先也還是先通過 demo 來看一看 SparseArray 的基本使用方法,主要就是插入方法以及遍歷方法。這也會後面的程式碼分析打下一個基礎。
SparseArray<Object> sparseArray = new SparseArray<>();
sparseArray.put(0,null);
sparseArray.put(1,"fsdfd" );
sparseArray.put(2,new String("fjdslfjdk"));
sparseArray.put(3,1);
sparseArray.put(4,new Boolean(true));
sparseArray.put(5,new Object());
sparseArray.put(8,new String("42fsjfldk"));
sparseArray.put(20,"jfslfjdkfj");
sparseArray.put(0,"chongfude" );
int size = sparseArray.size();
for (int i = 0;i < size;i++) {
Log.d(TAG, "sparseArraySample: i = " + i + ";value = " + sparseArray.get(sparseArray.keyAt(i)) );
}
複製程式碼
上面程式碼先是 new 了一個 SparseArray,注意宣告時只能指定 value 的型別,而 key 是固定為 int 的。然後再往裡面新增 key 以及 value。這裡注意一下的是 key 為 0 的情況插入了 2 次。遍歷時,是先通過順序的下標取出 key ,再通過 keyAt 來 get 出 value。當然也可以一步到位通過 valueAt() 直接獲取到 value。然後這個 demo 的執行結果如下。
sparseArraySample: i = 0;value = chongfude sparseArraySample: i = 1;value = fsdfd sparseArraySample: i = 2;value = fjdslfjdk sparseArraySample: i = 3;value = 1 sparseArraySample: i = 4;value = true sparseArraySample: i = 5;value = [email protected] sparseArraySample: i = 6;value = 42fsjfldk sparseArraySample: i = 7;value = jfslfjdkfj
然後通過 Debug 來看一看在記憶體中,SparseArray 實際是如何儲存的。如下圖分別是 key 與 value 在記憶體中的形式。可以看出 keys 和 values 的大小都為 13,而且 keys 的值是按從小到大順序排列的。
2.原始碼分析
下面是 SparseArray 的類圖結構,可以看到其屬性非常的少,也可以看出其分別用了陣列 int[] 和 object[] 來儲存 key 以及 value。
- SparseArray 初始化 SparseArray 的初始化也就是它的構造方法。
public SparseArray() {
this(10);
}
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;
}
複製程式碼
其有 2 個構造方法,帶參與不帶參。當然,這個引數就是指定陣列初始大小,也就是 SparseArray 的初始容量。而不帶引數則預設指定陣列大小為 10 個。
- 插入資料 put() 方法
public void put(int key, E value) {
// 1.先進行二分查詢
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
// 2. 如果找到了,則 i 必大於等於 0
if (i >= 0) {
mValues[i] = value;
} else {
// 3. 沒找到,則找一個正確的位置再插入
i = ~i;
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);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
複製程式碼
這裡呼叫了很多外部的方法以及內部的方法。首先是 ContainerHelpers#binarySearch() 的二分查詢演算法。
//This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
// 高位+低位之各除以 2,寫成右移,即通過位運算替代除法以提高運算效率
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
}
}
//若沒找到,則lo是value應該插入的位置,是一個正數。對這個正數去反,返回負數回去
return ~lo; // value not present
}
複製程式碼
二分查詢的分析屬於基礎內容,在註釋中了。回到 put() 方法首先通過二分查詢演算法從當前 keys 中查詢是否已經存在相同的 key 了,如果存在則會返回大於等於 0 的下標,然後接下來就會將原下標下的 values 中的舊value 替換成新的 value 值,即發生了覆蓋。
那如果沒有找到,那麼將 i 取反就是要插入的位置了,這一結論正好來自 binarySearch() 的返回結果。可以看到其最後如果沒有找到的話,就會返回 lo 的取反數。那麼這裡再把它取反過來那就是 lo 了。
這裡如果 i 是在大小 mSizes 的範圍內的,且其對應的 values[i] 又剛是被標記為刪除的物件,那麼就可以複用這個物件,否則就還是要依當前的 i 值進一步尋找要插入的位置,再插入相應的 value。
在插入之前,如果由於之前進行過 delete(),remoeAt() 以及 removeReturnOld() 中的某一個方法,那就可能要進行 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) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
}
複製程式碼
通過程式碼很容易分析得出,這裡的 gc ,實際就是壓縮儲存,簡單點說就是讓元素捱得緊一點。
而 gc() 完之後,下標 i 可能會發生變化,因此需要重新查詢一次,以得到一個新的下標 i。
最後就是通過 GrowingArrayUtils.insert() 來進行 key 和 value 的插入。這個 insert() 根據陣列型別過載了多個,這裡只分析 int[] 型別的即可。
public static int[] insert(int[] array, int currentSize, int index, int element) {
//確認 當前集合長度 小於等於 array陣列長度
assert currentSize <= array.length;
//不需要擴容
if (currentSize + 1 <= array.length) {
//將array陣列內從 index 移到 index + 1,共移了 currentSize - index 個,即從index開始後移一位,那麼就留出 index 的位置來插入新的值。
System.arraycopy(array, index, array, index + 1, currentSize - index);
//在index處插入新的值
array[index] = element;
return array;
}
//需要擴容,構建新的陣列,新的陣列大小由growSize() 計算得到
int[] newArray = new int[growSize(currentSize)];
//這裡再分 3 段賦值。首先將原陣列中 index 之前的資料複製到新陣列中
System.arraycopy(array, 0, newArray, 0, index);
//然後在index處插入新的值
newArray[index] = element;
//最後將原陣列中 index 及其之後的資料賦值到新陣列中
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
複製程式碼
上面的演算法中,如果不需要擴容則直接進行移位以留出空位來插入新的值,如果需要擴容則先擴容,然後根據要插入的位置 index,分三段資料複製到新的陣列中。這裡再看看 growSize() 是如何進行擴容 size 的計算的。
public static int growSize(int currentSize) {
//如果當前size 小於等於4,則返回8, 否則返回當前size的兩倍
return currentSize <= 4 ? 8 : currentSize * 2;
}
複製程式碼
程式碼相對簡單,當前 size 小於等於 4 則為 8 ,否則為 2 倍大小。
- get() 方法
public E get(int key) {
return get(key, null);
}
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];
}
}
複製程式碼
get() 方法就是通過 key 來返回對應的 value,前面在分析 put() 的時候已經分析過了二分查詢。那麼這裡如果找到了,就會通過下標直接從 mValues[] 中返回。
- delete() 方法
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
複製程式碼
delete() 也非常簡單,通過二分查詢演算法定位到下標,然後將對應的 value 標記為 DELETE,並且標記需要進行 gc 了。這裡需要注意的是被標記為 DELETE 的 value 不會在 gc 中被移除掉,而只會被覆蓋掉,從而提高了插入的效率。
三、總結
文章對 SparseArray 進行了簡要的分析,文章也只對主要的幾個方法進行了分析,其他沒有分析到的方法在這個基礎上再進行分析相信也是很簡單的。而總結下來幾點是:
- 其內部主要通過 2 個數組來儲存 key 和 value,分別是 int[] 和 Object[]。這也限定了其 key 只能為 int 型別,且 key 不能重複,否則會發生覆蓋。
- 一切操作都是基於二分查詢演算法,將 key 以升序的方法 “緊湊” 的排列在一起,從而提高記憶體的利用率以及訪問的效率。相比較 HashMap 而言,這是典型的時間換空間的策略。
- 刪除操作並不是真的刪除,而只是標記為 DELETE,以便下次能夠直接複用。
最後,感謝你能讀到並讀完此文章。受限於作者水平有限,如果存在錯誤或者疑問都歡迎留言討論。如果我的分享能夠幫助到你,也請記得幫忙點個贊吧,鼓勵我繼續寫下去,謝謝。