Android緩存淺析
By吳思博
1、引言
2、常見的幾種緩存算法
3、Android緩存的機(jī)制
4、LruCache的使用(內(nèi)存緩存)
5、云閱讀書籍解析緩存實(shí)現(xiàn)(內(nèi)存緩存實(shí)例,可跳過本節(jié))
6、DiskLruCache的使用(磁盤緩存)
1、引言
我們都聽過緩存,當(dāng)問你什么是緩存的時(shí)候,相信你能馬上給出一個(gè)完美的答案。?可是當(dāng)問你緩存是怎么構(gòu)建的,或者有一些怎樣緩存算法和框架?android中的緩存機(jī)制 ?,你可能會(huì)心神恍惚。
2、Android緩存的機(jī)制
最近在開發(fā)書籍解析章節(jié)中遇到了一些問題,經(jīng)過調(diào)試發(fā)現(xiàn)一部分是緩存的問題。云閱讀老版本中,解析書籍正文后緩存使用的是SoftReference,很容易被回收。Android中的緩存分為內(nèi)存緩存和文件緩存(磁盤緩存)。在早期常用的內(nèi)存緩存方式是軟引用(SoftReference)和弱引用(WeakReference),如大部分的使用方式:HashMap> ?imageCache;這種形式。從Android?2.3(Level 9)開始,垃圾回收器更傾向于回收SoftReference或WeakReference對(duì)象,這使得SoftReference和WeakReference變得不是那么實(shí)用有效。(到了Android?3.0(Level 11)之后,圖片數(shù)據(jù)Bitmap被放置到了內(nèi)存的堆區(qū)域,而堆區(qū)域的內(nèi)存是由GC管理的,開發(fā)者不需要進(jìn)行圖片資源的釋放工作,但這也使得圖片數(shù)據(jù)的釋放無法預(yù)知,增加了造成OOM的可能)。在Android3.1以后,Android推出了LruCache這個(gè)內(nèi)存緩存類,LruCache中的對(duì)象是強(qiáng)引用的。
3、常見的幾種緩存算法:
緩存算法有很多種,但是那種適合我們呢,下面來看看幾種常見的緩存算法。
3.1:FIFO(先進(jìn)先出)
先進(jìn)先出,原則簡單、且符合人們的慣性思維,具備公平性,并且實(shí)現(xiàn)起來簡單,直接使用數(shù)據(jù)結(jié)構(gòu)中的隊(duì)列即可實(shí)現(xiàn)。如果一個(gè)數(shù)據(jù)最先進(jìn)入緩存中,則應(yīng)該最早淘汰掉。也就是說,當(dāng)緩存滿的時(shí)候,應(yīng)當(dāng)把最先進(jìn)入緩存的數(shù)據(jù)給淘汰掉。實(shí)現(xiàn)方式:雙向鏈表(LinkedList)保存數(shù)據(jù),當(dāng)來了新的數(shù)據(jù)之后便添加到鏈表末尾,如果Cache存滿數(shù)據(jù),則把鏈表頭部數(shù)據(jù)刪除,然后把新的數(shù)據(jù)添加到鏈表末尾。在訪問數(shù)據(jù)的時(shí)候,如果在Cache中存在該數(shù)據(jù)的話,則返回對(duì)應(yīng)的value值;否則返回-1。
3.2:LRU(最少使用緩存算法)
把最近最少使用的緩存對(duì)象給淘汰。LRU總是需要去了解在什么時(shí)候,用了哪個(gè)緩存對(duì)象。瀏覽器就是使用了LRU作為緩存算法。新的對(duì)象會(huì)被放在緩存的頂部,當(dāng)緩存達(dá)到了容量極限,會(huì)把底部的對(duì)象淘汰。實(shí)現(xiàn)思路:把最新被訪問的緩存對(duì)象,放到緩存池的頂部。當(dāng)緩存達(dá)到了容量極限,會(huì)把底部的對(duì)象淘汰。所以,經(jīng)常被讀取的緩存對(duì)象就會(huì)一直呆在緩存池中。實(shí)現(xiàn)方式:使用array或者是linked list。LRU2和2Q,他們就是為了完善LRU而存在的。
Android 推薦:LruCache是Android 3.1所提供的一個(gè)緩存類,所以在Android中可以直接使用LruCache實(shí)現(xiàn)內(nèi)存緩存。而DisLruCache目前在Android 還不是Android SDK的一部分,但Android官方文檔推薦使用該算法來實(shí)現(xiàn)硬盤緩存。
3.3:LFU(最近使用頻率最少算法)
最近使用頻率最少算法;注意LFU和LRU算法的不同之處,LRU的淘汰規(guī)則是基于訪問時(shí)間,而LFU是基于訪問次數(shù)的。思想:當(dāng)緩存滿的時(shí)候,應(yīng)當(dāng)把訪問頻次最少的數(shù)據(jù)給淘汰掉。實(shí)現(xiàn):利用一個(gè)數(shù)組存儲(chǔ)數(shù)據(jù)項(xiàng),用HashMap存儲(chǔ)每個(gè)數(shù)據(jù)項(xiàng)在數(shù)組中對(duì)應(yīng)的位置,然后為每個(gè)數(shù)據(jù)項(xiàng)設(shè)計(jì)一個(gè)訪問頻次,當(dāng)數(shù)據(jù)項(xiàng)被命中時(shí),訪問頻次自增,在淘汰的時(shí)候淘汰訪問頻次最少的數(shù)據(jù)。在插入數(shù)據(jù)(插到數(shù)組的尾端)和訪問數(shù)據(jù)(數(shù)組隨機(jī)訪問)的時(shí)候都能達(dá)到O(1)的時(shí)間復(fù)雜度;淘汰數(shù)據(jù)的時(shí)候,通過選擇算法得到應(yīng)該淘汰的數(shù)據(jù)項(xiàng)在數(shù)組中的索引,并將該索引位置的內(nèi)容替換為新來的數(shù)據(jù)內(nèi)容即可,這樣的話,淘汰數(shù)據(jù)的操作時(shí)間復(fù)雜度為O(n)。
下面幾種算法都是上面的變種(可跳過):
3.4 LRU2(Least Recently Used 2):
Least Recently Used 2,又叫最近最少使用twice。LRU2把被兩次訪問過的對(duì)象放入緩存池,當(dāng)緩存池滿了之后,把有兩次最少使用的緩存對(duì)象淘汰。因?yàn)樾枰檶?duì)象2次,訪問負(fù)載就會(huì)隨著緩存池的增加而增加。如果把LRU2用在大容量的緩存池中,就會(huì)有問題。另外,LRU2還需要跟蹤不在緩存的對(duì)象,因?yàn)樗麄冞€沒有被第二次讀取。LRU2比LRU好,且是adoptive to access模式。
3.5 2Q(Two Queues):
2Q把被訪問的數(shù)據(jù)放到LRU的緩存中,如果這個(gè)對(duì)象再一次被訪問,2Q就把他轉(zhuǎn)移到第二個(gè)、更大的LRU緩存。2Q淘汰緩存對(duì)象是為了保持第一個(gè)緩存池是第二個(gè)緩存池的1/3。當(dāng)緩存的訪問負(fù)載是固定的時(shí)候,把LRU換成LRU2,就比增加緩存的容量更好。這種機(jī)制使得2Q比LRU2更好,2Q也是LRU家族中的一員,而且是adoptive to access模式。
3.6? ARC(Adaptive Replacement Cache):
ARC是介于LRU和LFU之間,為了提高效果,由2個(gè)LRU組成。第一個(gè)(L1)包含的條目是最近只被使用過一次的,而第二個(gè)LRU(L2)包含的是最近被使用過兩次的條目。(因此,L1放的是新的對(duì)象,而L2放的是常用的對(duì)象。)所以,才會(huì)認(rèn)為ARC是介于LRU和LFU之間的。
ARC被認(rèn)為是性能最好的緩存算法之一,能夠自調(diào),并且是低負(fù)載的。保存著歷史對(duì)象,這樣就可以記住那些被移除的對(duì)象。
…..? …..
4、LruCache的使用
LruCache是Android 3.1所提供的一個(gè)緩存類,所以在Android中可以直接使用LruCache實(shí)現(xiàn)內(nèi)存緩存。而DisLruCache目前在Android還不是Android SDK的一部分,但Android官方文檔推薦使用該算法來實(shí)現(xiàn)硬盤緩存。
LruCache的使用很簡單:
或者:
①設(shè)置LruCache緩存的大小,一般為當(dāng)前進(jìn)程可用容量的1/8。
②重寫sizeOf方法,計(jì)算出要緩存的每張圖片的大小。
注意:緩存的總?cè)萘亢兔總€(gè)緩存對(duì)象的大小所用單位要一致。
通過下面構(gòu)造函數(shù)來指定LinkedHashMap中雙向鏈表的結(jié)構(gòu)是訪問順序還是插入順序。
其中accessOrder設(shè)置為true則為訪問順序,為false,則為插入順序。
當(dāng)設(shè)置為true時(shí)
輸出結(jié)果:
0:0 3:3 4:4 5:5 6:6 1:1 2:2
即最近訪問的最后輸出,那么這就正好滿足的LRU緩存算法的思想。可見LruCache巧妙實(shí)現(xiàn),就是利用了LinkedHashMap的這種數(shù)據(jù)結(jié)構(gòu)。
下面我們?cè)贚ruCache源碼中具體看看,怎么應(yīng)用LinkedHashMap來實(shí)現(xiàn)緩存的添加,獲得和刪除的。
從LruCache的構(gòu)造函數(shù)中可以看到正是用了LinkedHashMap的訪問順序。
put()方法
可以看到put()方法并沒有什么難點(diǎn),重要的就是在添加過緩存對(duì)象后,調(diào)用trimToSize()方法,來判斷緩存是否已滿,如果滿了就要?jiǎng)h除近期最少使用的算法。
trimToSize()方法
trimToSize()方法不斷地刪除LinkedHashMap中隊(duì)尾的元素,即近期最少訪問的,直到緩存大小小于最大值。
當(dāng)調(diào)用LruCache的get()方法獲取集合中的緩存對(duì)象時(shí),就代表訪問了一次該元素,將會(huì)更新隊(duì)列,保持整個(gè)隊(duì)列是按照訪問順序排序。這個(gè)更新過程就是在LinkedHashMap中的get()方法中完成的。
get()方法:
其中LinkedHashMap的get()方法如下:
調(diào)用recordAccess()方法如下:
由此可見LruCache中維護(hù)了一個(gè)集合LinkedHashMap,該LinkedHashMap是以訪問順序排序的。當(dāng)調(diào)用put()方法時(shí),就會(huì)在結(jié)合中添加元素,并調(diào)用trimToSize()判斷緩存是否已滿,如果滿了就用LinkedHashMap的迭代器刪除隊(duì)尾元素,即近期最少訪問的元素。當(dāng)調(diào)用get()方法訪問緩存對(duì)象時(shí),就會(huì)調(diào)用LinkedHashMap的get()方法獲得對(duì)應(yīng)集合元素,同時(shí)會(huì)更新該元素到隊(duì)頭。
5、云閱讀書籍解析緩存(內(nèi)存緩存實(shí)例,可跳過本節(jié))
loadChapter()是章節(jié)解析函數(shù),首先從緩存中獲取章節(jié)解析,緩存中獲取章節(jié)解析不為null,直接返回緩存數(shù)據(jù),否則在mIGetChapterContentListener接口的onGetChapterContent()方法中獲取章節(jié)內(nèi)容,再進(jìn)行異步解析。
mIGetChapterContentListener的onGetChapterContent()方法中使用AsyncTask對(duì)章節(jié)進(jìn)行異步解析。
解析結(jié)束后,如果成功,并且當(dāng)前頁面為普通頁或者標(biāo)題頁則加入緩存。
緩存函數(shù):
、
具體實(shí)現(xiàn)在CacheManager類中。(如果緩存算法改變,只需要修改CacheManager類)
當(dāng)前CacheManager中使用LruCache算法實(shí)現(xiàn)。
6、DiskLruCache的使用
6.1創(chuàng)建
DISK_CACHE_SIZE緩存大小。
6.2添加
DishLruCache緩存添加的操作通過Eidtor完成,Editor為一個(gè)緩存對(duì)象的編輯對(duì)象。首先需要獲取圖片的url所對(duì)應(yīng)的key,根據(jù)key利用edit()來獲取Editor對(duì)象。若此時(shí)這個(gè)緩存正在被編輯,edit()會(huì)返回null。DiskLruCache不允許同時(shí)編輯同一個(gè)緩存對(duì)象。之所以把url轉(zhuǎn)換成key,因?yàn)閳D片的url中可能存在特殊字符,會(huì)影響使用,一般將url的md5值作為key
將url轉(zhuǎn)成key,利用這key值獲取Editor對(duì)象。若這個(gè)key的Editor對(duì)象不存在,edit()方法就創(chuàng)建一個(gè)新的出來。通過Editor對(duì)象可以獲取一個(gè)輸出流對(duì)象。DiskLruCache的open()方法中,一個(gè)節(jié)點(diǎn)只能有一個(gè)數(shù)據(jù),edit.newOutputStream(DISK_CACHE_INDEX)參數(shù)設(shè)置為0
這個(gè)文件輸出流,從網(wǎng)絡(luò)加載一個(gè)圖片后,通過這個(gè)OutputStream outputStream寫入文件系統(tǒng)。
上面的代碼并沒有將圖片寫入文件系統(tǒng),還需要通過Editor.commit()提交寫入操作,若寫入失敗,調(diào)用abort()方法,進(jìn)行回退整個(gè)操作。
這時(shí),圖片已經(jīng)正確寫入文件系統(tǒng),接下來的圖片獲取就不需要請(qǐng)求網(wǎng)絡(luò)
6.3緩存查找
查找過程,也需要將url轉(zhuǎn)換為key,然后通過DiskLruCache的get方法得到一個(gè)Snapshot對(duì)象,再通過Snapshot對(duì)象可得到緩存的文件輸入流,有了輸入流就可以得到Bitmap對(duì)象。為了避免oom,會(huì)使用ImageResizer進(jìn)行縮放。若直接對(duì)FileInputStream進(jìn)行操作,縮放會(huì)出現(xiàn)問題。FileInputStream是有序的文件流,兩次decodeStream調(diào)用會(huì)影響文件流的位置屬性。可以通過文件流得到其所對(duì)應(yīng)的文件描述符,利用BitmapFactory.decodeFileDescriptor()方法進(jìn)行縮放
在查找得到Bitmap后,把key,bitmap添加到內(nèi)存緩存中。