我们知道android为了减小空间的占用,在系统中提供了特有的容器,这里主要是替换了键值对的存取容器,如果能明确知道在使用的过程中键是整型,则替代该类型的Map容器的就是sparse家族了,这里主要有以下几种sparse容器,SparseBooleanArray,SparseIntArray,SparseLongArray,SparseArray.
SparseBooleanArray主要是替换的是值为Boolean类型,SparseIntArray主要替换的是值为Int类型,SparseLongArray主要替换的是值为Long类型,SparseArray是终极大招,存储的是一个泛型E,因此他不仅可以用来替换上面出现的容器,还能存储值为其它类型的值。
这里我们主要来解剖一下SparseBooleanArray,其它的类型大同小异,主要解剖其中不同的地方。
SparseBooleanArray主要有两个构造函数:构造函数如下
/** * Creates a new SparseBooleanArray containing no mappings. */
public SparseArray() {
this(10);
}
/** * Creates a new SparseBooleanArray containing no mappings that will not * require any additional memory allocation to store the specified * number of mappings. If you supply an initial capacity of 0, the * sparse array will be initialized with a light-weight representation * not requiring any additional array allocations. */
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.BOOLEAN;
} else {
mKeys = ArrayUtils.newUnpaddedIntArray(initialCapacity);
mValues = new boolean[mKeys.length];
}
mSize = 0;
}
无参构造函数最终调用了有参构造函数,并且指定了初始容量为10(这个主要是看的23的源码,不确定其他版本也是该值),在有参构造函数中根据容量进行不同的初始化。当initialCapacity为0时,mKeys初始化为一个INT类型的空数组,mValues初始化为一个BOOLEAN类型的空数组,当initialCapacity为其它值时mKeys由以下代码进行初始化
public static int[] newUnpaddedIntArray(int minLen) {
return (int[])VMRuntime.getRuntime().newUnpaddedArray(int.class, minLen);
}
主要用当前的运行新环境分配一个int类型的,指定长度的数组,mValues分配一个boolean类型的数组。mSize当前已经存储的内容的数量。
/** * Adds a mapping from the specified key to the specified value, * replacing the previous mapping from the specified key if there * was one. */
public void put(int key, boolean value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
在存储的时候,先根据指定的key从已有的mKeys数组中二分查找对应的key是否已经存在,如果存在则i返回的是对应key存储的下标,否则返回的对应key应该存储位置的i取反值,当i大于0时即存在时,则覆盖对应下标mValues的值,否则,再将值取反,将key于values分别存储在i所对应的下标中。
二分查找如下:
// 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) {
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
}
如果存在对应的key,则返回对应key存储的下标,否则取反。
插入key与values的代码如下:
/**
* Primitive int version of {@link #insert(Object[], int, int, Object)}.
*/
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));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
当插入的值数量小于当前数组的容量,则将i下标以后的数据均向后移动一位,这是采用System.arraycopy来实现的,再将elemen存储在i对应的下标中,如果容量已经大于顶于当前数组的容量,则先计算一个容量增长后的值,当当前值小于等于4时,容量为8,否则容量翻倍,比如默认构造函数中的10,经过增长后就变成20。之后当前运行时再次分配一个新的数组,并且采用System.arraycopy将i下标以前的值拷贝到新数据,i下标存储当前的值,再拷贝i下标以后的值到新数组,同时返回新的数组,到此存储已经完成。
/** * Given the current size of an array, returns an ideal size to which the array should grow. * This is typically double the given size, but should not be relied upon to do so in the * future. */
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
System.arraycopy如下:
/** * The int[] specialized version of arraycopy(). * * @hide internal use only */
public static void arraycopy(int[] src, int srcPos, int[] dst, int dstPos, int length) {
if (src == null) {
throw new NullPointerException("src == null");
}
if (dst == null) {
throw new NullPointerException("dst == null");
}
if (srcPos < 0 || dstPos < 0 || length < 0 ||srcPos > src.length - length || dstPos > dst.length - length) {
throw new ArrayIndexOutOfBoundsException("src.length=" + src.length + " srcPos=" + srcPos +" dst.length=" + dst.length + " dstPos=" + dstPos + " length=" + length);
}
if (length <= ARRAYCOPY_SHORT_INT_ARRAY_THRESHOLD) {
// Copy int by int for shorter arrays.
if (src == dst && srcPos < dstPos && dstPos < srcPos + length) {
// Copy backward (to avoid overwriting elements before
// they are copied in case of an overlap on the same
// array.)
for (int i = length - 1; i >= 0; --i) {
dst[dstPos + i] = src[srcPos + i];
}
} else {
// Copy forward.
for (int i = 0; i < length; ++i) {
dst[dstPos + i] = src[srcPos + i];
}
}
} else {
// Call the native version for longer arrays.
arraycopyIntUnchecked(src, srcPos, dst, dstPos, length);
}
}
/** * Gets the boolean mapped from the specified key, or <code>false</code> * if no such mapping has been made. */
public boolean get(int key) {
return get(key, false);
}
/** * Gets the boolean mapped from the specified key, or the specified value * if no such mapping has been made. */
public boolean get(int key, boolean valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0) {
return valueIfKeyNotFound;
} else {
return mValues[i];
}
}
获取值比存储值简单很多,主要有两个重载函数,单参数和两个参数的获取函数,单参数最终调用了两个参数的函数,默认值指定为false,在获取时,先根据指定的key获取key在对应mKeys中的下标,如果返回值小于0表示不存在对应的key,也就说明了不存在对应的values,因此返回传入的默认值,否则返回对应下标的values值,
/** * Removes the mapping from the specified key, if there was any. */
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
System.arraycopy(mKeys, i + 1, mKeys, i, mSize - (i + 1));
System.arraycopy(mValues, i + 1, mValues, i, mSize - (i + 1));
mSize--;
}
}
/** @hide */
public void removeAt(int index) {
System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
mSize--;
}
删除也有两函数,第一个函数是根据指定的key,如果存在指定的key,则根据key的下标删除mKeys与mValues中对应的值,同时,下标以后的值分别向前挪动一个位置,同时mSize减一。第二个函数是删除对应下标的值,但是这个函数是hide类型的,程序中不能直接使用。
到此我们就解析完成SparseBooleanArray的解析,但是其实上述代码还有可以继续采用时间换空间来更进一步减少所占用的空间。虽然boolean类型所占存储空间的大小没有明确指定,仅定义为能够取字面值true或false,但是jvm虚拟机中还是要明确占用的大小boolean
类型占用空间,如果是四个字节或者1个字节那空间都还可以继续优化占用空间。
上面的实现方式以及够简单,并且占用空间少,存储查找效率高,那能否更进一步对上述容器进行优化呐,比如存储查找的效率提升,空间占用的减少?这里主要来探讨一下空间占用的减少,下面的代码都是伪代码,优化方式如下:
mValues = ArrayUtils.newUnpaddedIntArray((initialCapacity+16)/32);
mVlues默认初始化为0,这样values只占用了之前的1/8的空间,每一个bit位来代表初始值,存在为1,否则为0,由于现在只采用一个bit来代表true/false,因此存储时需要改写为以下方式:
public void put(int key, boolean value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
int value = mValues[i/32];
value |= 1<< i%32;
mValues[i] = value;
} else {
i = ~i;
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
// 从最后一位数值分别向后移动一bit
if(mSize % 32==0 && mSize/32==mValues.length){
int []temp = ArrayUtils.newUnpaddedIntArray((mSize+16)/32);
System.arraycopy(mValues, 0, temp, 0, mSize/32);
mValues = temp;
}
for(int j = mValues.length;j>0;--j){
int covert = mValues[j];
covert = covert>>1 | (mValues[j-1]<<31)
mValues[j] = convert;
}
mSize++;
}
}
因为存储的方式不再是boolean类型的数组,而是int类型的数组,每一位代表了存储的值,因此获取值改为以下方式:
public boolean get(int key, boolean valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0) {
return valueIfKeyNotFound;
} else {
int value = mValues[i/32];
return value |= 1<< i%32 > 0;
}
}
如果不进行改造,在初始化时尽量指定初始大小,因为每次在空间不够时都是成倍增长,因此比如初始明确知道要存储11个值,如果指定了大小则只需要11个大小的空间,不指定则最终分配了20个大小的空间,浪费了9个大小的空间。
SparseIntArray,SparseLongArray,SparseArray.与SparseBooleanArray一致,只是初始话初始的mValues类型不同,SparseIntArray初始化为int类型的values,SparseLongArray初始化为Long类型的values,SparseArray初始化为object类型的values。
那为什么spares的类型会节省空间呐?
这主要是数据存储的格式不一样,Sparse里面是采用了两个数组来存储的键值,而比如HashMap里面采用了HashMapEntry来存储一个节点,HashMapEntry结构如下,里面包含了更多的字段,因此占用了更多的空间。这也是为什么Sparse占用空间更少的原因,且存储查找效率更高。
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
@Override
public final boolean equals(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry<?, ?> e = (Entry<?, ?>) o;
return Objects.equal(e.getKey(), key)
&& Objects.equal(e.getValue(), value);
}
@Override
public final int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
@Override
public final String toString() {
return key + "=" + value;
}
}