引言
ArrayMap
,SparseArray
都是Google针对Android平台而推出的特有的数据结构。早年Android设备配置比较低,内存比较宝贵,ArrayMap
是Android专门针对内存优化而设计的,用于取代Java API中的HashMap数据结构。
本文从源码分析ArrayMap
,SparseArray
的实现原理,并分析一下使用场景。
SparseArray
SparseArray是一种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;
}
默认构造方法将初始容量定义为10,mKeys
和mValues
两个数组分别存储键和值。
put
public void put(int key, E value) {
//二分查找确认下标
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
//数组中已经存在对应的key 直接覆盖
mValues[i] = value;
} else {
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++;
}
}
put
方法的精髓就是binarySearch
的实现了,Google在不少地方都用到了这个设计,来看看具体源码:
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
}
binarySearch
的实现其实并不复杂,会根据数组的实际大小进行查找,关键在于未找到对应数值时候的return ~lo
,这时候已经查找过整个数组,最后停留在lo
没找到这个数据,所以这个数据在数组中的下标也确定为lo
,然后取反返回,方便外部判断。如果返回的数据为负数,说明数组中没有这个元素,那么直接插入的返回值取反的位置即可。因为是根据size
进行查找,因此插入的元素比数组中所有元素还要大的时候,它的下标总会是实际size+1
,也就是说put
插入始终是按顺序插入,数组中的数据都是连续
的。
这里再看一下GrowingArrayUtils.insert
方法的实现:
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
//容量还够用
if (currentSize + 1 <= array.length) {
//需要把大于此元素的数据向右侧拷贝
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
//扩容为原来的两倍
int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
//拷贝index左侧数据
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
insert
方法会将元素插入到指定位置,并将数据中大于此数据的元素向右侧拷贝。当容量不够用的时候,会将数组扩容一倍,再整体进行拷贝。
append
public void append(int key, E value) {
//如果key没有key数组里的最大的数值大则调用put方法
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
}
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
如果能确定要插入的元素的key
已有元素的key
都要大的时候,则可以调用append
方法插入新元素,可以避免进行二分查找而直接插入,提高效率。
remove/delete
remove
方法会调用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;
}
}
}
remove
方法并不会将key
移除,而是把对应的位置位置的value设置为DELETED
并标记为回收,保持了数组的连续性。
removeAt
removeAt
方法在于你确认了数据位置的时候使用,不用进行search操作,直接移除对应下标的元素,因而复杂度会是O(1)。
get
get
方法会使用二分查找进行查找,其复杂度为O(log2n),如果未找到对应的元素,则返回用户设定的默认数据。
使用场景
SparseArray
使用了基础类型int当key,避免了自动拆装箱引起的性能损耗,类似的封装还有SparseIntArray
、SparseBooleanArray
,原理都一样。采用二分查找
进行数据查找,在数据量不是很大的情况下存取性能都还算可以,数据量比较大的时候因为存在数组扩容和数据拷贝等情况,性能会有所损耗。同时存在回收机制,使数组大小不会太大,比较节省内存。在Android
中,可以用在RecyclerView
列表复用保存控件状态等情况使用。
ArrayMap
ArrayMap
的实现原理和思路和SparseArray
很相象。SparseArray
采用int当key,那我们计算Object的hashcode
,得到的也是int类型,因此也可以采用类似的原理。
ArrayMap
底层还是采用了两个数组
来保存数据,mHashes
数用来保存key
的hash,mArray
数组则用来保存键值对。
构造
public ArrayMap(int capacity, boolean identityHashCode) {
mIdentityHashCode = identityHashCode;
// If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS
// instance instead of the usual EmptyArray.INT. The reference
// is checked later to see if the array is allowed to grow.
if (capacity < 0) {
mHashes = EMPTY_IMMUTABLE_INTS;
mArray = EmptyArray.OBJECT;
} else if (capacity == 0) {
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
} else {
allocArrays(capacity);
}
mSize = 0;
}
ArrayMap
默认构造创建的是2个空数组,如果我们自行传入capacity
则会调用allocArrays
方法创建两个数组,其中mArray
会是mHashes
两倍的容量。
allocArrays
分配数组大小。ArrayMap
存在缓存机制,如果分配的数组大小为BASE_SIZE
或者BASE_SIZE
的两倍,则会触发这个缓存机制。BASE_SIZE
的大小为4。
synchronized (sBaseCacheLock) {
//之前有缓存数据则尝试获取
if (mBaseCache != null) {
final Object[] array = mBaseCache;
mArray = array;
try {
mBaseCache = (Object[]) array[0];
mHashes = (int[]) array[1];
if (mHashes != null) {
array[0] = array[1] = null;
mBaseCacheSize--;
if (DEBUG) {
Log.d(TAG, "Retrieving 1x cache " + mHashes
+ " now have " + mBaseCacheSize + " entries");
}
return;
}
} catch (ClassCastException e) {
}
// Whoops! Someone trampled the array (probably due to not protecting
// their access with a lock). Our cache is corrupt; report and give up.
Slog.wtf(TAG, "Found corrupt ArrayMap cache: [0]=" + array[0]
+ " [1]=" + array[1]);
mBaseCache = null;
mBaseCacheSize = 0;
}
}
put
put
方法会先对key
取hashcode
,然后根据hashcode
从mHashes
数组里进行查找对应的index。
int indexOf(Object key, int hash) {
final int N = mSize;
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
int index = binarySearchHashes(mHashes, N, hash);
if (index < 0) {
return index;
}
// 如果找到了 则用equals判断是否为同一个key 如果是则返回
if (key.equals(mArray[index<<1])) {
return index;
}
// 尝试在index之后找到对应的key
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
if (key.equals(mArray[end << 1])) return end;
}
// 尝试在index之前找到对应的key
for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
if (key.equals(mArray[i << 1])) return i;
}
// Key not found -- return negative value indicating where a
// new entry for this key should go. We use the end of the
// hash chain to reduce the number of array entries that will
// need to be copied when inserting.
return ~end;
}
indexOf
方法还是沿用了之前的思路,先尝试从mHashes
数组里匹配,如果hash匹配上了,则从mArray
中通过equals
方法匹配key。mArray
中关联的key和value是连着排列的。找到关联的index,说明存在这个key,则会覆盖掉value。
index = ~index;//取反 获取应该插入的下标
if (osize >= mHashes.length) {
//hash表容量不够了 触发扩容
final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
: (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
//复制数据到新的hash表
if (mHashes.length > 0) {
if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
//释放并缓存旧数据
freeArrays(ohashes, oarray, osize);
}
if (index < osize) {
if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
+ " to " + (index+1));
//复制key更大的元素到该元素的右边
System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
if (osize != mSize || index >= mHashes.length) {
throw new ConcurrentModificationException();
}
}
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
如果需要新插入数据,则会判断当前hash数组的大小是否可以容纳更多元素,如果不够放了则会触发扩容,然后将旧数组整体复制到新数组中。同时,会对旧数据进行缓存。最后,如果插入的元素不是最大的,会将数组中更大的部分整体拷贝到其右侧,保持有序性。
append
append
方法类似于SparseArray
中的实现,也是在你比较确定key的hash值比较大的时候使用。可以免去查找过程直接插入的数组中,提高效率。
remove
remove
方法也需要先进行查找,如果hash命中,则会调用removeAt
方法从对应的下标移除数据。removeAt
中有一系列内存优化的操作。如果数组当前的mHashes
数组长度大于8并且实际容量小于mHashes
的数组长度的三分之一,则会触发收缩操作。在进行计算后,会重新分配一个合适的数组大小,然后进行数组拷贝。节约内存。
//...
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);
//...
get
get
无外乎是一个查找功能,使用的方法也还是二分查找,因为可能存在hash碰撞,所以最后确定下标还得依据equals
方法是否返回一致来确认。
使用场景
ArrayMap
的出现,是因为当时Android设备配置较低,所以设计上存在一定的时间换空间。底层采用数组
实现,插入数据和删除数据都会存在数据拷贝的情况。因而在数据量比较大的时候,性能是不如HashMap
的。因而可以得出结论,当设备配置较低,或者存储数据量不大的时候,采用ArrayMap
可能会有更好的性能和更优的内存占用,但是数据量较大的时候,还是老老实实的用HashMap
吧。