最近在整理SparseArray這一知識點的時候,發現網上大多數SparseArray原理分析的文章都存在不少問題(能夠說不少做者並無讀懂SparseArray的源碼),也正所以,纔有了這篇文章。咱們知道,SparseArray與ArrayMap是Android中高效存儲K-V的數據結構,也是是Android面試中的常客,弄懂它們的實現原理是頗有必要的,本篇文章就以SparseArray的源碼爲例進行深刻分析。java
開始以前先給你們推薦一下AndroidNote這個GitHub倉庫,這裏是個人學習筆記,同時也是我文章初稿的出處。這個倉庫中彙總了大量的java進階和Android進階知識。是一個比較系統且全面的Android知識庫。對於準備面試的同窗也是一份不可多得的面試寶典,歡迎你們到GitHub的倉庫主頁關注。git
SparseArray能夠翻譯爲稀疏數組,從字面上能夠理解爲鬆散不連續的數組。雖然叫作Array,但它倒是存儲K-V的一種數據結構。其中Key只能是int類型,而Value是Object類型。咱們來看下它的類結構:github
public class SparseArray<E> implements Cloneable {
// 用來標記此處的值已被刪除
private static final Object DELETED = new Object();
// 用來標記是否有元素被移除
private boolean mGarbage = false;
// 用來存儲key的集合
private int[] mKeys;
// 用來存儲value的集合
private Object[] mValues;
// 存入的元素個數
private int mSize;
// 默認初始容量爲10
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;
}
// ...省略其餘代碼
}
複製代碼
能夠看到SparseArray僅僅實現了Cloneable接口並無實現Map接口,而且SparseArray內部維護了一個int數組和一個Object數組。在無參構造方法中調用了有參構造,並將其初始容量設置爲了10。面試
是否是以爲很奇怪?做爲一個容器類,不先講put方法怎麼先將remove呢?這是由於remove方法的一些操做會影響到put的操做。只有先了解了remove才能更容易理解put方法。咱們來看remove的代碼:算法
// SparseArray
public void remove(int key) {
delete(key);
}
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
複製代碼
能夠看到remove方法直接調用了delete方法。而在delete方法中會先經過二分查找(二分查找代碼後邊分析)找到key所在的位置,而後將這一位置的value值置爲DELETE,注意,這裏還將mGarbage設置爲了true來標記集合中存在刪除元素的狀況。想象一下,在刪除多個元素後這個集合中是否是就可能會出現不連續的狀況?大概這也是SparseArray名字的由來吧。數組
做爲一個存儲K-V類型的數據結構,put方法是key和value的入口。也是SparseArray中最重要的一個方法。先來看下put方法的代碼:markdown
// SparseArray
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) { // 意味着以前mKeys中已經有對應的key存在了,第i個位置對應的就是key。
mValues[i] = value; // 直接更新value
} else { // 返回負數說明未在mKeys中查找到key
// 取反獲得待插入key的位置
i = ~i;
// 若是插入位置小於size,而且這個位置的value恰好是被刪除掉的,那麼直接將key和value分別插入mKeys和mValues的第i個位置
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
// mGarbage爲true說明有元素被移除了,此時mKeys已經滿了,可是mKeys內部有被標記爲DELETE的元素
if (mGarbage && mSize >= mKeys.length) {
// 調用gc方法移動mKeys和mValues中的元素,這個方法能夠後邊分析
gc();
// 因爲gc方法移動了數組,所以插入位置可能有變化,因此須要從新計算插入位置
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
// GrowingArrayUtils的insert方法將會將插入位置以後的全部數據向後移動一位,而後將key和value分別插入到mKeys和mValue對應的第i個位置,若是數組空間不足還會開啓擴容,後邊分析這個insert方法
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
複製代碼
雖然這個方法只有寥寥數行,可是想要徹底理解卻並不是易事,即便寫了很詳細的註釋也不容易讀懂。咱們不妨來詳細分析一下。第一行代碼經過二分查找獲得了一個index。看下二分查找的代碼:數據結構
// ContainerHelpers
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就是這個元素的最佳存儲位置。上述代碼中將lo取反做爲了返回值。由於lo必定是大於等於0的數,所以取反後的返回值一定小於等於0.明白了這一點,再來看put方法中的這個if...else是否是很容易理解了?app
// SparseArray
public void put(int key, E value) {
if (i >= 0) {
mValues[i] = value; // 直接更新value
} else {
i = ~i;
// ... 省略其它代碼
}
}
複製代碼
若是i>=0,意味着當前的這個key已經存在於mKeys中了,那麼此時put只須要將最新的value更新到mValues中便可。而若是i<=0就意味着mKeys中以前沒有對應的key。所以就須要將key和value分別插入到mKeys和mValues中。而插入的最佳位置就是對i取反。svn
獲得插入位置以後,若是這個位置是被標記爲刪除的元素,那麼久能夠直接將其覆蓋掉了,所以有如下代碼:
public void put(int key, E value) {
// ...
if (i >= 0) {
// ...
} else {
// 若是i對應的位置是被刪除掉的,能夠直接將其覆蓋
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
// ...
}
}
複製代碼
若是上邊條件不知足,那麼繼續往下看:
public void put(int key, E value) {
// ...
if (i >= 0) {
// ...
} else {
// mGarbage爲true說明有元素被移除了,此時mKeys已經滿了,可是mKeys內部有被標記爲DELETE的元素
if (mGarbage && mSize >= mKeys.length) {
// 調用gc方法移動mKeys和mValues中的元素,這個方法能夠後邊分析
gc();
// 因爲gc方法移動了數組,所以插入位置可能有變化,因此須要從新計算插入位置
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
// ...
}
}
複製代碼
上邊咱們已經知道,在remove元素的時候mGarbage會被置爲true,這段代碼意味着有被移除的元素,被移除的位置並非要插入的位置,而且若是mKeys已經滿了,那麼就調用gc方法來移動元素填充被移除的位置。因爲mKeys中元素位置發生了變化,所以key插入的位置也可能改變,所以須要再次調用二分法來查找key的插入位置。
以上代碼最終會肯定key被插入的位置,接下來調用GrowingArrayUtils的insert方法來進行key的插入操做:
// SparseArray
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
// ...
} else {
// ...
// GrowingArrayUtils的insert方法將會將插入位置以後的全部數據向後移動一位,而後將key和value分別插入到mKeys和mValue對應的第i個位置,若是數組空間不足還會開啓擴容,後邊分析這個insert方法
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
複製代碼
GrowingArrayUtils的insert方法代碼以下:
// GrowingArrayUtils
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
assert currentSize <= array.length;
// 若是插入後數組size小於數組長度,能進行插入操做
if (currentSize + 1 <= array.length) {
// 將index以後的全部元素向後移動一位
System.arraycopy(array, index, array, index + 1, currentSize - index);
// 將key插入到index的位置
array[index] = element;
return array;
}
// 來到這裏說明數組已滿,需須要進行擴容操做。newArray即爲擴容後的數組
T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
// 返回擴容後的size
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
複製代碼
insert方法的代碼比較容易理解,若是數組容量足夠,那麼就將index以後的元素向後移動一位,而後將key插入index的位置。若是數組容量不足,那麼則須要進行擴容,而後再進行插入操做。
這個方法其實很容易理解,咱們知道Java虛擬機在內存不足時會進行GC操做,標記清除法在回收垃圾對象後爲了不內存碎片化,會將存活的對象向內存的一端移動。而SparseArray中的這個gc方法其實就是借鑑了垃圾收集整理碎片空間的思想。
關於mGarbage這個參數上邊已經有提到過了,這個變量會在刪除元素的時候被置爲true。以下:
// SparseArray中全部移除元素的方法中都將mGarbage置爲true
public E removeReturnOld(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
final E old = (E) mValues[i];
mValues[i] = DELETED;
mGarbage = true;
return old;
}
}
return null;
}
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
public void removeAt(int index) {
if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
throw new ArrayIndexOutOfBoundsException(index);
}
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
複製代碼
而SparseArray中全部插入和查找元素的方法中都會判斷若是mGarbage爲true,而且mSize >= mKeys.length時調用gc,以append方法爲例,代碼以下:
public void append(int key, E value) {
if (mGarbage && mSize >= mKeys.length) {
gc();
}
// ... 省略無關代碼
}
複製代碼
源碼中調用gc方法的地方多達8處,都是與添加和查找元素相關的方法。例如put()、keyAt()、setValueAt()等方法中。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);
}
複製代碼
這個方法就比較簡單了,由於put的時候是維持了一個有序數組,所以經過二分查找能夠直接肯定key在數組中的位置。
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];
}
}
複製代碼
可見SparseArray是一個使用起來很簡單的數據結構,可是它的原理理解起來彷佛卻沒那麼容易。這也是網上大部分文章對應SparseArray的解析都是含糊不清的緣由。相信經過本篇文章的學習必定對SparseArray的實現有了新的認識!