SparseArray源碼分析
-
SparseArray(稀疏數組)是什么?
類似于map,可以儲存key-value鍵值對,與HashMap不同因為其key只能是整型int而且內部存儲結構是數組(最新HashMap存儲結構為紅黑樹+鏈表+數組);為android的工具類,用于優化HashMap<Integer, V>這種情況。由于其內部使用數組來存儲key和value,且數組內容可能大部分都并未使用,因此叫稀疏數組。
-
SparseArray的優勢劣勢?
與HashMap相對比,SparseArray的內存效率更高,因為它避免了自動裝箱鍵,并且沒有HashMap中額外的節點(鏈表節點,紅黑樹節點)內存開銷。但是由于SparseArray將key放在一個數組結構中,且使用二分查找,因此其不適用于大數據量的儲存。例如儲存數百個數據,速度和HashMap相當,性能差異不明顯,并且兩者之間性能差異小于50%。相比于HashMap,SparseArray更節省內存,但是在大數據量的時候,插入和查找的效率都會比HashMap低。
-
SparseArray的應用場景?
適用于儲存小容量數據,key必須為int型
下面分析源碼:
類成員變量:
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
- DELETED:用于標記已刪除的key-value
- mGarbage:用于標記是否需要回收被刪除的數據
- mKeys:用于儲存key
- mValues:用于儲存values
- mSize:當前包含非空元素(value)的條目(key-value),未調用gc前,包括value為DELETED的情況,這里的gc并非為java中的System.gc(),而是SparseArray中的gc方法。
構造函數
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;
}
- 默認構造函數:調用帶參構造函數,指定容量為10
- 帶參(指定數組容量)構造函數:容量為空則賦值mKeys和mValues為長度為0的數組,否則創建最小長度為initialCapacity的Unpadded Object數組,再將key數組的大小指定為value數組的大小。
關于EmptyArray:
//版本名稱: Oreo API Level: 26
public final class EmptyArray {
private EmptyArray() {}
public static final boolean[] BOOLEAN = new boolean[0];
public static final byte[] BYTE = new byte[0];
public static final char[] CHAR = new char[0];
public static final double[] DOUBLE = new double[0];
public static final float[] FLOAT = new float[0];
public static final int[] INT = new int[0];
public static final long[] LONG = new long[0];
public static final Class<?>[] CLASS = new Class[0];
public static final Object[] OBJECT = new Object[0];
public static final String[] STRING = new String[0];
public static final Throwable[] THROWABLE = new Throwable[0];
public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
public static final java.lang.reflect.Type[] TYPE = new java.lang.reflect.Type[0];
public static final java.lang.reflect.TypeVariable[] TYPE_VARIABLE =
new java.lang.reflect.TypeVariable[0];
}
關于ArrayUtils.newUnpaddedObjectArray:
// 版本名稱: Oreo API Level: 26
public static Object[] newUnpaddedObjectArray(int minLen) {
return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen);
}
其中調用了VMRuntime.getRuntime().newUnpaddedArray來分配object數組內存,該方法字面意思為new一個un padded的數組,即new一個不填充的數組。返回一個至少為minLen長度的數組,但有可能會更大。增加的大小用于避免數組排列時填充的大小,填充量取決于組件類型和內存分配器實現。因為編譯器會根據數組對齊,在申請obj內存時,往往會分配大于該obj的實際內存而保證自然對齊使CPU讀寫更加高效,但是這往往需要填充一些沒有用到的空間,因此,為了不浪費這個空間,array變得更大了。
關于數據結構對齊可以看wiki描述:https://en.wikipedia.org/wiki/Data_structure_alignment
put
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key); //二叉查找,可以看后面的方法解釋
if (i >= 0) {
mValues[i] = value; //數組中存在該key,直接更新值
} else {
i = ~i; //取到要插入的數組下標
//如果要插入的下標小于mSize且被標志為已刪除的話
//重新將key賦值,value賦值
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果標記為mBarbage且mSize大于或者等于key數組大小
if (mGarbage && mSize >= mKeys.length) {
//gc方法回收已刪除的數據
gc();
//重新進行二叉搜索,因為索引可能已經發生變化了
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//插入key
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
//插入value
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
//mSize加1
mSize++;
}
}
- 首先通過二分查找是否在數組中存在該key,若找到,則返回對應的數組下標,否則,返回要插入的數組下標的負數
- 如果存在的話,直接將對應的value數組下標賦值
- 如果不存在的話,返回要插入的數組下標的負數,因此這里重新取反,拿到要插入數組下標的整數。再根據條件判斷key是否為已刪除數據,是否要進行數組壓縮,最后插入key和value到各自數組中。
關于ContainerHelpers.binarySearch:
//該方法要求array必須為有序數組
//array:要查找的數組
//size:數組中有效大小,從下表0開始,到size-1為數組的有效范圍
//value:要查找的值
static int binarySearch(int[] array, int size, int value) {
//定義low低數組下標,用于數組中往上查找,最小為0
int lo = 0;
//定義high高數組下標,用于數組中往下查找,最大為size-1
int hi = size - 1;
//如果lo <= hi,則循環
while (lo <= hi) {
//取數組中間下標
//右移1位即等同于除以2的1次方即除2,無符號右移,空位都以0補齊,與運算比除預算效率高
final int mid = (lo + hi) >>> 1;
//取數組中間下標的值
final int midVal = array[mid];
//若中間值小于value,則將low賦值為mid+1,繼續往上查找
if (midVal < value) {
lo = mid + 1;
//若中間值大于value,則將high賦值為mid-1,繼續往下查找
} else if (midVal > value) {
hi = mid - 1;
//已找到,返回數組下標
} else {
return mid; // value found
}
}
//否則返回一個小于0的值,且該值還有另外一個含義:
//要插入的位置下標,SparseArray拿到這個返回值后,便可知道要插入的下標
//該值還有另外一個作用就是可以保證數組的有序性
return ~lo; // value not present
}
關于gc
該方法為SparseArray的回收方法,用于將刪除的數據從mKeys數組和mValues數組中刪除,并將剩下的數據往前移動
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize; //當前數組有效大小,因為mkey大小和mValues大小相同,因此這里mSize是對兩者而言的
int o = 0; //數組中非null的數量,即有效數據數量,起始值為0
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
//不是被刪除的元素
if (val != DELETED) {
//若i 不等于 o的話
//證明o為被刪除的下標,因此這里需要將下標為o的key數組的值
//賦值為下標為i的值,且將下標為o的value數組的值賦值為當前val
//最后將已經移走的當前下標i對應的value數組的值置為null
//但是這里可以看到,key[i]并沒有置為0,因為不設置為0的話
//不會影響二叉查找,因為后面mSize為非null數據數量
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
//有效數據+1
o++;
}
}
mGarbage = false; //gc后將回收標志置為false
mSize = o; //將mSize賦值為數組中非null的數量
// Log.e("SparseArray", "gc end with " + mSize);
}
關于GrowingArrayUtils.insert
//array: 要插入的數組
//currentSize: 數組中的元素數量,小于或等于array.length
//index: 要插入的下標
//element: 要插入的元素
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
//如果數組中的元素數量大于數組長度的話,拋出異常
assert currentSize <= array.length;
//如果在當前數組再插入一個元素,并且不超過數組的大小的話
if (currentSize + 1 <= array.length) {
//將原本數據從index開始,往后移動currentSize - index個數據
System.arraycopy(array, index, array, index + 1, currentSize - index);
//將index對應的值賦值為要插入的元素
array[index] = element;
return array;
}
@SuppressWarnings("unchecked")
//否則需要重新創建數組,且將原本數組的內容賦值到新數組中
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;
}
append
直接put一個key-value到數組中,針對key大于mKeys數組所有現有鍵的情況進行優化。
public void append(int key, E value) {
//若當前mSize不為0且key小于mKeys數組中的最大值
if (mSize != 0 && key <= mKeys[mSize - 1]) {
//直接put
put(key, value);
return;
}
//mGarbage回收標志為true且mSize大于或等于mKeys數組長度
if (mGarbage && mSize >= mKeys.length) {
//調用gc方法
gc();
}
//插入key
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
//插入value
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
setValueAt
按照給定的index替換value
public void setValueAt(int index, E value) {
//mGarbage回收標志為true的話調用gc方法
if (mGarbage) {
gc();
}
//將對應index下表的值賦值為value
mValues[index] = value;
}
get
//根據key獲取value
public E get(int key) {
return get(key, null);
}
//根據key獲取value,若該key不存在或者已刪除,返回valueIfKeyNotFound
public E get(int key, E valueIfKeyNotFound) {
//首先進行二分查找 找到key的數組下表
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//i小于0代表mKeys數組中不包含該Key,或者該key-value已被刪除
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
remove
//刪除key對應的數據
public void remove(int key) {
delete(key);
}
//刪除key對應的數據
public void delete(int key) {
//先通過二叉查找是否存在該key
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//i >= 0代表存在該key且i為其在mKeys數組中的下標
if (i >= 0) {
//且該key對應的value還沒被刪除
if (mValues[i] != DELETED) {
//將對應mValues數組下表的值置位DELETED已刪除
mValues[i] = DELETED;
//將回收標志置為true,在調用size(), put(), append()等方法時需要
//調用gc方法將被標記為已刪除的數據,即對應的value值為DELETED
//從數組移除,且將后面的沒被刪除的數據往前移動
mGarbage = true;
}
}
}
//刪除對應下表的數據
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
//將回收標志置為true
mGarbage = true;
}
}
//刪除從對應下表開始,大小為size的數據
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
removeAt(i);
}
}
查詢
indexOfKey
根據key獲取其在數組中的下表
public int indexOfKey(int key) {
//mGarbage回收標志為true的話調用gc方法
if (mGarbage) {
gc();
}
//若數組中存在該key,則返回對應下表
//否則返回一個負數
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
indexOfValue
根據value獲取其在數組中的下表
public int indexOfValue(E value) {
//mGarbage回收標志為true的話調用gc方法
if (mGarbage) {
gc();
}
//遍歷mValues數組,判斷是否相等
for (int i = 0; i < mSize; i++) {
if (mValues[i] == value) {
//若相等,則返回i
return i;
}
}
//否則返回-1
return -1;
}
keyAt
獲取下表為index的key
public int keyAt(int index) {
//mGarbage回收標志為true的話調用gc方法
if (mGarbage) {
gc();
}
//返回mKeys數組數據
return mKeys[index];
}
valueAt
獲取下表為index的value
public E valueAt(int index) {
//mGarbage回收標志為true的話調用gc方法
if (mGarbage) {
gc();
}
//返回mValues/數組數據
return (E) mValues[index];
}