SparseArray 是 Android 在 Android SdK 爲咱們提供的一個基礎的數據結構,其功能相似於 HashMap。與 HashMap 不一樣的是它的 Key 只能是 int 值,不能是其餘的類型。java
首先也仍是先經過 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 = java.lang.Object@b67a0fa sparseArraySample: i = 6;value = 42fsjfldk sparseArraySample: i = 7;value = jfslfjdkfjbash
而後經過 Debug 來看一看在內存中,SparseArray 實際是如何存儲的。以下圖分別是 key 與 value 在內存中的形式。能夠看出 keys 和 values 的大小都爲 13,並且 keys 的值是按從小到大順序排列的。數據結構
下面是 SparseArray 的類圖結構,能夠看到其屬性很是的少,也能夠看出其分別用了數組 int[] 和 object[] 來存儲 key 以及 value。 源碼分析
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 個。ui
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
//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 值,即發生了覆蓋。spa
那若是沒有找到,那麼將 i 取反就是要插入的位置了,這一結論正好來自 binarySearch() 的返回結果。能夠看到其最後若是沒有找到的話,就會返回 lo 的取反數。那麼這裏再把它取反過來那就是 lo 了。3d
這裏若是 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 倍大小。
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[] 中返回。
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 進行了簡要的分析,文章也只對主要的幾個方法進行了分析,其餘沒有分析到的方法在這個基礎上再進行分析相信也是很簡單的。而總結下來幾點是:
最後,感謝你能讀到並讀完此文章。受限於做者水平有限,若是存在錯誤或者疑問都歡迎留言討論。若是個人分享可以幫助到你,也請記得幫忙點個贊吧,鼓勵我繼續寫下去,謝謝。