ArrayMap及SparseArray是android的系統(tǒng)API,是專門為移動(dòng)設(shè)備而定制的。用于在一定情況下取代HashMap而達(dá)到節(jié)省內(nèi)存的目的。
一.源碼分析(由于篇幅限制,源碼分析部分會(huì)放在單獨(dú)的文章中)
二.實(shí)現(xiàn)原理及數(shù)據(jù)結(jié)構(gòu)對(duì)比
三.性能測(cè)試對(duì)比
四.總結(jié)
一.源碼分析
稍后會(huì)在下一篇文章中補(bǔ)充(都寫在一篇,篇幅太長(zhǎng)了)
二.實(shí)現(xiàn)原理及數(shù)據(jù)結(jié)構(gòu)對(duì)比
1. hashMap
從hashMap的結(jié)構(gòu)中可以看出,首先對(duì)key值求hash,根據(jù)hash結(jié)果確定在table數(shù)組中的位置,當(dāng)出現(xiàn)哈希沖突時(shí)采用開放鏈地址法進(jìn)行處理。Map.Entity的數(shù)據(jù)結(jié)構(gòu)如下:
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
}
具體的hashmap源碼細(xì)節(jié)會(huì)在其他文章中進(jìn)行分析,這里可以看出來的是,從空間的角度分析,HashMap中會(huì)有一個(gè)利用率不超過負(fù)載因子(默認(rèn)為0.75)的table數(shù)組,其次,對(duì)于HashMap的每一條數(shù)據(jù)都會(huì)用一個(gè)HashMapEntry進(jìn)行記錄,除了記錄key,value外,還會(huì)記錄下hash值,及下一個(gè)entity的指針。
時(shí)間效率方面,利用hash算法,插入和查找等操作都很快,且一般情況下,每一個(gè)數(shù)組值后面不會(huì)存在很長(zhǎng)的鏈表(因?yàn)槌霈F(xiàn)hash沖突畢竟占比較小的比例),所以不考慮空間利用率的話,HashMap的效率非常高。
2.ArrayMap
ArrayMap利用兩個(gè)數(shù)組,mHashes用來保存每一個(gè)key的hash值,mArrray大小為mHashes的2倍,依次保存key和value。源碼的細(xì)節(jié)方面會(huì)在下一篇文章中說明。現(xiàn)在我們先拋開細(xì)節(jié)部分,只看關(guān)鍵語句:
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
相信看到這大家都明白了原理了。但是它怎么查詢呢?答案是二分查找。當(dāng)插入時(shí),根據(jù)key的hashcode()方法得到hash值,計(jì)算出在mArrays的index位置,然后利用二分查找找到對(duì)應(yīng)的位置進(jìn)行插入,當(dāng)出現(xiàn)哈希沖突時(shí),會(huì)在index的相鄰位置插入。
總結(jié)一下,空間角度考慮,ArrayMap每存儲(chǔ)一條信息,需要保存一個(gè)hash值,一個(gè)key值,一個(gè)value值。對(duì)比下HashMap 粗略的看,只是減少了一個(gè)指向下一個(gè)entity的指針。還有就是節(jié)省了一部分可見空間上的內(nèi)存節(jié)省也不是特別明顯。是不是這樣呢?后面會(huì)驗(yàn)證。
時(shí)間效率上看,插入和查找的時(shí)候因?yàn)槎加玫亩址ǎ檎业臅r(shí)候應(yīng)該是沒有hash查找快,插入的時(shí)候呢,如果順序插入的話效率肯定高,但如果是隨機(jī)插入,肯定會(huì)涉及到大量的數(shù)組搬移,數(shù)據(jù)量大,肯定不行,再想一下,如果是不湊巧,每次插入的hash值都比上一次的小,那就得次次搬移,效率一下就扛不住了的感腳。
3.SparseArray
sparseArray相對(duì)來說就簡(jiǎn)單的多了,但是不要以為它可以取代前兩種,sparseArray只能在key為int的時(shí)候才能使用,注意是int而不是Integer,這也是sparseArray效率提升的一個(gè)點(diǎn),去掉了裝箱的操作!。
因?yàn)閗ey為int也就不需要什么hash值了,只要int值相等,那就是同一個(gè)對(duì)象,簡(jiǎn)單粗暴。插入和查找也是基于二分法,所以原理和Arraymap基本一致,這里就不多說了。
總結(jié)一下:空間上對(duì)比,與HashMap,去掉了Hash值的存儲(chǔ)空間,沒有next的指針占用,還有其他一些小的內(nèi)存占用,看著節(jié)省了不少。
時(shí)間上對(duì)比:插入和查找的情形和Arraymap基本一致,可能存在大量的數(shù)組搬移。但是它避免了裝箱的環(huán)節(jié),不要小看裝箱過程,還是很費(fèi)時(shí)的。所以從源碼上來看,效率誰快,就看數(shù)據(jù)量大小了。
好啦,說半天都是分析,下面來點(diǎn)實(shí)際的,用數(shù)據(jù)說話!
三.性能測(cè)試對(duì)比
我們從插入和查詢兩方面來比對(duì)試試看。
1.插入性能時(shí)間對(duì)比
測(cè)試代碼:
long start = System.currentTimeMillis();
Map<Integer, String> hash = new HashMap<Integer, String>();
for (int i = 0; i < MAX; i++) {
hash.put(i, i+"");
}
long ts = System.currentTimeMillis() - start;
就貼這一段吧,其他兩段代碼無非就是把HashMap換掉,通過改變Max值就行對(duì)比。
分析:從結(jié)果上來看,數(shù)據(jù)量小的時(shí)候,差異并不大(當(dāng)然了,數(shù)據(jù)量小,時(shí)間基準(zhǔn)小,內(nèi)容太多,就不貼數(shù)據(jù)表了,確實(shí)差異不大),當(dāng)數(shù)據(jù)量大于5000左右,SparseArray,最快,HashMap最慢,乍一看,好像SparseArray是最快的,但是要注意,這是順序插入的。也就是SparseArray和Arraymap最理想的情況。
來個(gè)逆序插入的試試
long start = System.currentTimeMillis();
HashMap<Integer, String> hash = new HashMap<Integer, String>();
for (int i = 0; i < MAX; i++) {
hash.put(MAX-1-i, i+"");
}
long ts = System.currentTimeMillis() - start;
分析:從結(jié)果上來看,果然,HashMap遠(yuǎn)超Arraymap和SparseArray,也前面分析一致。
當(dāng)然了,數(shù)據(jù)量小的時(shí)候,例如1000以下,這點(diǎn)時(shí)間差異也是可以忽略的。
下面來看看空間對(duì)比:先說一下測(cè)試方法,因?yàn)闇y(cè)試內(nèi)存,所以尤其要注意的一點(diǎn),就是測(cè)試的過程不要發(fā)生GC,如果發(fā)生了GC,那數(shù)據(jù)就不準(zhǔn)了,想了想,用了個(gè)比較簡(jiǎn)單的方法:
Runtime.getRuntime().totalMemory()//獲取應(yīng)用已經(jīng)申請(qǐng)到的總的內(nèi)存
Runtime.getRuntime().freeMemory()//獲取應(yīng)用內(nèi)存的free部分
兩個(gè)方法的差值就是應(yīng)用已經(jīng)使用的內(nèi)存部分。
值得注意的是當(dāng)MAX值很大的時(shí)候,可能在代碼執(zhí)行過程發(fā)生GC,此時(shí)可以同時(shí)用Android Monitor的Memory窗口監(jiān)視內(nèi)存,沒有發(fā)生gc的過程結(jié)果才有效。假設(shè)數(shù)據(jù)量比較大的時(shí)候,每測(cè)完一次手動(dòng)GC一次,這樣基本上每次都能測(cè)試成功;因?yàn)閿?shù)據(jù)量也不是特別大,只有很少一部分情況測(cè)試過程會(huì)發(fā)生GC,所以也沒有去進(jìn)一步探究其他方式,比如設(shè)置虛擬機(jī)參數(shù)來延長(zhǎng)GC時(shí)間,有空了可以搞一下。上數(shù)據(jù):
可見,SparseArray在內(nèi)存占用方面的確要優(yōu)于HashMap和ArrayMap不少,通過數(shù)據(jù)觀察,大致節(jié)省30%左右,而ArrayMap的表現(xiàn)正如前面說的,優(yōu)化作用有限,幾乎和HashMap相同。
2.查找性能對(duì)比
long start = System.currentTimeMillis();
SparseArray<String> hash = new SparseArray<String>();
for (int i = 0; i < MAX; i++) {
hash.get(i);
}
long ts = System.currentTimeMillis() - start;
發(fā)現(xiàn)SparseArray比HashMap要快,和前面假設(shè)的不符,二分查找難道比Hash快?
再一想,因?yàn)橛眠@樣的代碼測(cè)試有點(diǎn)不公平,因?yàn)镾parseArray沒有裝箱,HashMap有個(gè)裝箱的過程,似乎不太公平。那么想個(gè)辦法再來測(cè)試下,
ArrayList<IntEntity> intEntityList=new ArrayList<IntEntity>();
private void boxing(){
for(int i=0;i<MAX;i++){
IntEntity entity=new IntEntity();
entity.i1=i;
entity.i2=Integer.valueOf(i);
intEntityList.add(entity);
}
}
class IntEntity{
int i1;
Integer i2;
}
給HashMap和ArrayMap的時(shí)候給它提前裝箱,這樣似乎公平些。
long start = System.currentTimeMillis();
HashMap<Integer, String> hash = new HashMap<Integer, String>();
for (int i = 0; i < MAX; i++) {
// hash.get(i);
hash.get(intEntityList.get(i).i2);
}
long ts = System.currentTimeMillis() - start;
果然結(jié)果不一樣了,HashMap才是查詢最快的,這才符合邏輯嘛,但是我們正常用的時(shí)候是不管裝不裝箱的,所以綜合起來還是使用SparseArray效率最高。
扯了這么多,終于到了該總結(jié)的時(shí)候了。
四、總結(jié)
1.在數(shù)據(jù)量小的時(shí)候一般認(rèn)為1000以下,當(dāng)你的key為int的時(shí)候,使用SparseArray確實(shí)是一個(gè)很不錯(cuò)的選擇,內(nèi)存大概能節(jié)省30%,相比用HashMap,因?yàn)樗黭ey值不需要裝箱,所以時(shí)間性能平均來看也優(yōu)于HashMap,建議使用!
2.ArrayMap相對(duì)于SparseArray,特點(diǎn)就是key值類型不受限,任何情況下都可以取代HashMap,但是通過研究和測(cè)試發(fā)現(xiàn),ArrayMap的內(nèi)存節(jié)省并不明顯,也就在10%左右,但是時(shí)間性能確是最差的,當(dāng)然了,1000以內(nèi)的數(shù)據(jù)量也無所謂了,加上它只有在API>=19才可以使用,個(gè)人建議沒必要使用!還不如用HashMap放心。估計(jì)這也是為什么我們?cè)賜ew一個(gè)HashMap的時(shí)候google也沒有提示讓我們使用的原因吧。
目前本人在公司負(fù)責(zé)熱修復(fù)相關(guān)的工作,主要是基于robust的熱修復(fù)相關(guān)工作。感興趣的同學(xué)歡迎進(jìn)群交流。