Android緩存淺析
By吳思博
1藕溅、引言
2询吴、常見(jiàn)的幾種緩存算法
3齿尽、Android緩存的機(jī)制
4沽损、LruCache的使用(內(nèi)存緩存)
5、云閱讀書(shū)籍解析緩存實(shí)現(xiàn)(內(nèi)存緩存實(shí)例循头,可跳過(guò)本節(jié))
6绵估、DiskLruCache的使用(磁盤(pán)緩存)
1、引言
我們都聽(tīng)過(guò)緩存卡骂,當(dāng)問(wèn)你什么是緩存的時(shí)候国裳,相信你能馬上給出一個(gè)完美的答案。?可是當(dāng)問(wèn)你緩存是怎么構(gòu)建的全跨,或者有一些怎樣緩存算法和框架缝左?android中的緩存機(jī)制 ?浓若,你可能會(huì)心神恍惚渺杉。
2、Android緩存的機(jī)制
最近在開(kāi)發(fā)書(shū)籍解析章節(jié)中遇到了一些問(wèn)題挪钓,經(jīng)過(guò)調(diào)試發(fā)現(xiàn)一部分是緩存的問(wèn)題是越。云閱讀老版本中,解析書(shū)籍正文后緩存使用的是SoftReference碌上,很容易被回收倚评。Android中的緩存分為內(nèi)存緩存和文件緩存(磁盤(pán)緩存)。在早期常用的內(nèi)存緩存方式是軟引用(SoftReference)和弱引用(WeakReference)馏予,如大部分的使用方式:HashMap> ?imageCache;這種形式天梧。從Android?2.3(Level 9)開(kāi)始,垃圾回收器更傾向于回收SoftReference或WeakReference對(duì)象霞丧,這使得SoftReference和WeakReference變得不是那么實(shí)用有效呢岗。(到了Android?3.0(Level 11)之后,圖片數(shù)據(jù)Bitmap被放置到了內(nèi)存的堆區(qū)域蚯妇,而堆區(qū)域的內(nèi)存是由GC管理的,開(kāi)發(fā)者不需要進(jìn)行圖片資源的釋放工作暂筝,但這也使得圖片數(shù)據(jù)的釋放無(wú)法預(yù)知箩言,增加了造成OOM的可能)。在Android3.1以后焕襟,Android推出了LruCache這個(gè)內(nèi)存緩存類(lèi)陨收,LruCache中的對(duì)象是強(qiáng)引用的。
3、常見(jiàn)的幾種緩存算法:
緩存算法有很多種务漩,但是那種適合我們呢拄衰,下面來(lái)看看幾種常見(jiàn)的緩存算法。
3.1:FIFO(先進(jìn)先出)
先進(jìn)先出饵骨,原則簡(jiǎn)單翘悉、且符合人們的慣性思維,具備公平性居触,并且實(shí)現(xiàn)起來(lái)簡(jiǎn)單妖混,直接使用數(shù)據(jù)結(jié)構(gòu)中的隊(duì)列即可實(shí)現(xiàn)。如果一個(gè)數(shù)據(jù)最先進(jìn)入緩存中轮洋,則應(yīng)該最早淘汰掉制市。也就是說(shuō),當(dāng)緩存滿(mǎn)的時(shí)候弊予,應(yīng)當(dāng)把最先進(jìn)入緩存的數(shù)據(jù)給淘汰掉祥楣。實(shí)現(xiàn)方式:雙向鏈表(LinkedList)保存數(shù)據(jù),當(dāng)來(lái)了新的數(shù)據(jù)之后便添加到鏈表末尾汉柒,如果Cache存滿(mǎn)數(shù)據(jù)误褪,則把鏈表頭部數(shù)據(jù)刪除,然后把新的數(shù)據(jù)添加到鏈表末尾竭翠。在訪(fǎng)問(wèn)數(shù)據(jù)的時(shí)候振坚,如果在Cache中存在該數(shù)據(jù)的話(huà),則返回對(duì)應(yīng)的value值斋扰;否則返回-1渡八。
3.2:LRU(最少使用緩存算法)
把最近最少使用的緩存對(duì)象給淘汰。LRU總是需要去了解在什么時(shí)候传货,用了哪個(gè)緩存對(duì)象屎鳍。瀏覽器就是使用了LRU作為緩存算法。新的對(duì)象會(huì)被放在緩存的頂部问裕,當(dāng)緩存達(dá)到了容量極限逮壁,會(huì)把底部的對(duì)象淘汰。實(shí)現(xiàn)思路:把最新被訪(fǎng)問(wè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è)緩存類(lèi)朵诫,所以在Android中可以直接使用LruCache實(shí)現(xiàn)內(nèi)存緩存。而DisLruCache目前在Android 還不是Android SDK的一部分,但Android官方文檔推薦使用該算法來(lái)實(shí)現(xiàn)硬盤(pán)緩存困檩。
3.3:LFU(最近使用頻率最少算法)
最近使用頻率最少算法槽棍;注意LFU和LRU算法的不同之處逼友,LRU的淘汰規(guī)則是基于訪(fǎng)問(wèn)時(shí)間榴嗅,而LFU是基于訪(fǎng)問(wèn)次數(shù)的。思想:當(dāng)緩存滿(mǎn)的時(shí)候赖瞒,應(yīng)當(dāng)把訪(fǎng)問(wèn)頻次最少的數(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è)訪(fǎng)問(wèn)頻次吧兔,當(dāng)數(shù)據(jù)項(xiàng)被命中時(shí),訪(fǎng)問(wèn)頻次自增袍嬉,在淘汰的時(shí)候淘汰訪(fǎng)問(wèn)頻次最少的數(shù)據(jù)境蔼。在插入數(shù)據(jù)(插到數(shù)組的尾端)和訪(fǎng)問(wèn)數(shù)據(jù)(數(shù)組隨機(jī)訪(fǎng)問(wèn))的時(shí)候都能達(dá)到O(1)的時(shí)間復(fù)雜度;淘汰數(shù)據(jù)的時(shí)候伺通,通過(guò)選擇算法得到應(yīng)該淘汰的數(shù)據(jù)項(xiàng)在數(shù)組中的索引箍土,并將該索引位置的內(nèi)容替換為新來(lái)的數(shù)據(jù)內(nèi)容即可,這樣的話(huà)罐监,淘汰數(shù)據(jù)的操作時(shí)間復(fù)雜度為O(n)吴藻。
下面幾種算法都是上面的變種(可跳過(guò)):
3.4 LRU2(Least Recently Used 2):
Least Recently Used 2,又叫最近最少使用twice弓柱。LRU2把被兩次訪(fǎng)問(wèn)過(guò)的對(duì)象放入緩存池沟堡,當(dāng)緩存池滿(mǎn)了之后,把有兩次最少使用的緩存對(duì)象淘汰矢空。因?yàn)樾枰檶?duì)象2次航罗,訪(fǎng)問(wèn)負(fù)載就會(huì)隨著緩存池的增加而增加。如果把LRU2用在大容量的緩存池中屁药,就會(huì)有問(wèn)題粥血。另外,LRU2還需要跟蹤不在緩存的對(duì)象酿箭,因?yàn)樗麄冞€沒(méi)有被第二次讀取复亏。LRU2比LRU好,且是adoptive to access模式缭嫡。
3.5 2Q(Two Queues):
2Q把被訪(fǎng)問(wèn)的數(shù)據(jù)放到LRU的緩存中缔御,如果這個(gè)對(duì)象再一次被訪(fǎng)問(wèn),2Q就把他轉(zhuǎn)移到第二個(gè)械巡、更大的LRU緩存刹淌。2Q淘汰緩存對(duì)象是為了保持第一個(gè)緩存池是第二個(gè)緩存池的1/3。當(dāng)緩存的訪(fǎng)問(wèn)負(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)包含的條目是最近只被使用過(guò)一次的茁裙,而第二個(gè)LRU(L2)包含的是最近被使用過(guò)兩次的條目塘砸。(因此,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è)緩存類(lèi)北救,所以在Android中可以直接使用LruCache實(shí)現(xiàn)內(nèi)存緩存。而DisLruCache目前在Android還不是Android SDK的一部分大猛,但Android官方文檔推薦使用該算法來(lái)實(shí)現(xiàn)硬盤(pán)緩存扭倾。
LruCache的使用很簡(jiǎn)單:
或者:
①設(shè)置LruCache緩存的大小,一般為當(dāng)前進(jìn)程可用容量的1/8挽绩。
②重寫(xiě)sizeOf方法膛壹,計(jì)算出要緩存的每張圖片的大小。
注意:緩存的總?cè)萘亢兔總€(gè)緩存對(duì)象的大小所用單位要一致唉堪。
通過(guò)下面構(gòu)造函數(shù)來(lái)指定LinkedHashMap中雙向鏈表的結(jié)構(gòu)是訪(fǎng)問(wèn)順序還是插入順序模聋。
其中accessOrder設(shè)置為true則為訪(fǎng)問(wèn)順序,為false唠亚,則為插入順序链方。
當(dāng)設(shè)置為true時(shí)
輸出結(jié)果:
0:0 3:3 4:4 5:5 6:6 1:1 2:2
即最近訪(fǎng)問(wèn)的最后輸出,那么這就正好滿(mǎn)足的LRU緩存算法的思想灶搜∷钍矗可見(jiàn)LruCache巧妙實(shí)現(xiàn)工窍,就是利用了LinkedHashMap的這種數(shù)據(jù)結(jié)構(gòu)。
下面我們?cè)贚ruCache源碼中具體看看前酿,怎么應(yīng)用LinkedHashMap來(lái)實(shí)現(xiàn)緩存的添加患雏,獲得和刪除的。
從LruCache的構(gòu)造函數(shù)中可以看到正是用了LinkedHashMap的訪(fǎng)問(wèn)順序罢维。
put()方法
可以看到put()方法并沒(méi)有什么難點(diǎn)淹仑,重要的就是在添加過(guò)緩存對(duì)象后,調(diào)用trimToSize()方法肺孵,來(lái)判斷緩存是否已滿(mǎn)匀借,如果滿(mǎn)了就要?jiǎng)h除近期最少使用的算法。
trimToSize()方法
trimToSize()方法不斷地刪除LinkedHashMap中隊(duì)尾的元素平窘,即近期最少訪(fǎng)問(wèn)的吓肋,直到緩存大小小于最大值。
當(dāng)調(diào)用LruCache的get()方法獲取集合中的緩存對(duì)象時(shí)瑰艘,就代表訪(fǎng)問(wèn)了一次該元素蓬坡,將會(huì)更新隊(duì)列,保持整個(gè)隊(duì)列是按照訪(fǎng)問(wèn)順序排序磅叛。這個(gè)更新過(guò)程就是在LinkedHashMap中的get()方法中完成的屑咳。
get()方法:
其中LinkedHashMap的get()方法如下:
調(diào)用recordAccess()方法如下:
由此可見(jiàn)LruCache中維護(hù)了一個(gè)集合LinkedHashMap,該LinkedHashMap是以訪(fǎng)問(wèn)順序排序的弊琴。當(dāng)調(diào)用put()方法時(shí)兆龙,就會(huì)在結(jié)合中添加元素,并調(diào)用trimToSize()判斷緩存是否已滿(mǎn)敲董,如果滿(mǎn)了就用LinkedHashMap的迭代器刪除隊(duì)尾元素紫皇,即近期最少訪(fǎng)問(wèn)的元素。當(dāng)調(diào)用get()方法訪(fǎng)問(wèn)緩存對(duì)象時(shí)腋寨,就會(huì)調(diào)用LinkedHashMap的get()方法獲得對(duì)應(yīng)集合元素聪铺,同時(shí)會(huì)更新該元素到隊(duì)頭。
5萄窜、云閱讀書(shū)籍解析緩存(內(nèi)存緩存實(shí)例铃剔,可跳過(guò)本節(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)前頁(yè)面為普通頁(yè)或者標(biāo)題頁(yè)則加入緩存夷磕。
緩存函數(shù):
、
具體實(shí)現(xiàn)在CacheManager類(lèi)中仔沿。(如果緩存算法改變企锌,只需要修改CacheManager類(lèi))
當(dāng)前CacheManager中使用LruCache算法實(shí)現(xiàn)。
6于未、DiskLruCache的使用
6.1創(chuàng)建
DISK_CACHE_SIZE緩存大小。
6.2添加
DishLruCache緩存添加的操作通過(guò)Eidtor完成陡鹃,Editor為一個(gè)緩存對(duì)象的編輯對(duì)象烘浦。首先需要獲取圖片的url所對(duì)應(yīng)的key,根據(jù)key利用edit()來(lái)獲取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è)新的出來(lái)萄传。通過(guò)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è)圖片后蹭睡,通過(guò)這個(gè)OutputStream outputStream寫(xiě)入文件系統(tǒng)衍菱。
上面的代碼并沒(méi)有將圖片寫(xiě)入文件系統(tǒng),還需要通過(guò)Editor.commit()提交寫(xiě)入操作肩豁,若寫(xiě)入失敗脊串,調(diào)用abort()方法,進(jìn)行回退整個(gè)操作清钥。
這時(shí)洪规,圖片已經(jīng)正確寫(xiě)入文件系統(tǒng),接下來(lái)的圖片獲取就不需要請(qǐng)求網(wǎng)絡(luò)
6.3緩存查找
查找過(guò)程循捺,也需要將url轉(zhuǎn)換為key斩例,然后通過(guò)DiskLruCache的get方法得到一個(gè)Snapshot對(duì)象,再通過(guò)Snapshot對(duì)象可得到緩存的文件輸入流从橘,有了輸入流就可以得到Bitmap對(duì)象念赶。為了避免oom础钠,會(huì)使用ImageResizer進(jìn)行縮放。若直接對(duì)FileInputStream進(jìn)行操作叉谜,縮放會(huì)出現(xiàn)問(wèn)題旗吁。FileInputStream是有序的文件流,兩次decodeStream調(diào)用會(huì)影響文件流的位置屬性停局『艿觯可以通過(guò)文件流得到其所對(duì)應(yīng)的文件描述符,利用BitmapFactory.decodeFileDescriptor()方法進(jìn)行縮放
在查找得到Bitmap后董栽,把key,bitmap添加到內(nèi)存緩存中码倦。