講到緩存,平時流水線上的碼農一定覺得這是一個高大上的東西。看過網上各種講緩存原理的文章,總感覺那些文章講的就是玩具,能用嗎?這次我将帶你一起看過uil這個國内外大牛都追捧的圖檔緩存類庫的緩存處理機制。看了uil中的緩存實作,才發現其實這個東西不難,沒有太多的程序排程,沒有各種記憶體讀取控制機制、沒有各種異常處理。反正uil中不單代碼寫的簡單,連處理都簡單。但是這個類庫這麼好用,又有這麼多人用,那麼非常有必要看看他是怎麼實作的。先了解uil中緩存流程的原理圖。
原理示意圖
主體有三個,分别是ui,緩存子產品和資料源(網絡)。它們之間的關系如下:
① ui:請求資料,使用唯一的key值索引memory cache中的bitmap。
② 記憶體緩存:緩存搜尋,如果能找到key值對應的bitmap,則傳回資料。否則執行第三步。
③ 硬碟存儲:使用唯一key值對應的檔案名,檢索sdcard上的檔案。
④ 如果有對應檔案,使用bitmapfactory.decode*方法,解碼bitmap并傳回資料,同時将資料寫入緩存。如果沒有對應檔案,執行第五步。
⑤ 下載下傳圖檔:啟動異步線程,從資料源下載下傳資料(web)。
⑥ 若下載下傳成功,将資料同時寫入硬碟和緩存,并将bitmap顯示在ui中。
uil中的記憶體緩存政策
1. 隻使用的是強引用緩存
lrumemorycache(這個類就是這個開源架構預設的記憶體緩存類,緩存的是bitmap的強引用,下面我會從源碼上面分析這個類)
2.使用強引用和弱引用相結合的緩存有
usingfreqlimitedmemorycache(如果緩存的圖檔總量超過限定值,先删除使用頻率最小的bitmap)
lrulimitedmemorycache(這個也是使用的lru算法,和lrumemorycache不同的是,他緩存的是bitmap的弱引用)
fifolimitedmemorycache(先進先出的緩存政策,當超過設定值,先删除最先加入緩存的bitmap)
largestlimitedmemorycache(當超過緩存限定值,先删除最大的bitmap對象)
limitedagememorycache(當 bitmap加入緩存中的時間超過我們設定的值,将其删除)
3.隻使用弱引用緩存
weakmemorycache(這個類緩存bitmap的總大小沒有限制,唯一不足的地方就是不穩定,緩存的圖檔容易被回收掉)
我們直接選擇uil中的預設配置緩存政策進行分析。
imageloaderconfiguration.createdefault(…)這個方法最後是調用builder.build()方法建立預設的配置參數的。預設的記憶體緩存實作是lrumemorycache,磁盤緩存是unlimiteddisccache。
lrumemorycache:一種使用強引用來儲存有數量限制的bitmap的cache(在空間有限的情況,保留最近使用過的bitmap)。每次bitmap被通路時,它就被移動到一個隊列的頭部。當bitmap被添加到一個空間已滿的cache時,在隊列末尾的bitmap會被擠出去并變成适合被gc回收的狀态。
注意:這個cache隻使用強引用來儲存bitmap。
lrumemorycache實作memorycache,而memorycache繼承自memorycacheaware。
下面給出繼承關系圖
lrumemorycache.get(…)
我相信接下去你看到這段代碼的時候會跟我一樣驚訝于代碼的簡單,代碼中除了異常判斷,就是利用synchronized進行同步控制。
我們會好奇,這不是就簡簡單單将bitmap從map中取出來嗎?但lrumemorycache聲稱保留在空間有限的情況下保留最近使用過的bitmap。不急,讓我們細細觀察一下map。他是一個linkedhashmap<string, bitmap>型的對象。
linkedhashmap中的get()方法不僅傳回所比對的值,并且在傳回前還會将所比對的key對應的entry調整在清單中的順序(linkedhashmap使用雙連結清單來儲存資料),讓它處于清單的最後。當然,這種情況必須是在linkedhashmap中accessorder==true的情況下才生效的,反之就是get()方法不會改變被比對的key對應的entry在清單中的位置。
代碼第11行的maketail()就是調整entry在清單中的位置,其實就是雙向連結清單的調整。它判斷accessorder
。到現在我們就清楚lrumemorycache使用linkedhashmap來緩存資料,在linkedhashmap.get()方法執行後,linkedhashmap中entry的順序會得到調整。那麼我們怎麼保證最近使用的項不會被剔除呢?接下去,讓我們看看lrumemorycache.put(...)。
lrumemorycache.put(...)
注意到代碼第8行中的size+= sizeof(key, value),這個size是什麼呢?我們注意到在第19行有一個trimtosize(maxsize),trimtosize(...)這個函數就是用來限定lrumemorycache的大小不要超過使用者限定的大小,cache的大小由使用者在lrumemorycache剛開始初始化的時候限定。
其實不難想到,當bitmap緩存的大小超過原來設定的maxsize時應該是在trimtosize(...)這個函數中做到的。這個函數做的事情也簡單,周遊map,将多餘的項(代碼中對應toevict)剔除掉,直到目前cache的大小等于或小于限定的大小。
這時候我們會有一個以為,為什麼周遊一下就可以将使用最少的bitmap緩存給剔除,不會誤删到最近使用的bitmap緩存嗎?首先,我們要清楚,lrumemorycache定義的最近使用是指最近用get或put方式操作到的bitmap緩存。其次,之前我們直到lrumemorycache的get操作其實是通過其内部字段linkedhashmap.get(...)實作的,當linkedhashmap的accessorder==true時,每一次get或put操作都會将所操作項(圖中第3項)移動到連結清單的尾部(見下圖,連結清單頭被認為是最少使用的,連結清單尾被認為是最常使用的。),每一次操作到的項我們都認為它是最近使用過的,當記憶體不夠的時候被剔除的優先級最低。需要注意的是一開始的linkedhashmap連結清單是按插入的順序構成的,也就是第一個插入的項就在連結清單頭,最後一個插入的就在連結清單尾。假設隻要剔除圖中的1,2項就能讓lrumemorycache小于原先限定的大小,那麼我們隻要從連結清單頭周遊下去(從1→最後一項)那麼就可以剔除使用最少的項了。
至此,我們就知道了lrumemorycache緩存的整個原理,包括他怎麼put、get、剔除一個元素的的政策。接下去,我們要開始分析預設的磁盤緩存政策了。
uil中的磁盤緩存政策
像新浪微網誌、花瓣這種應用需要加載很多圖檔,本來圖檔的加載就慢了,如果下次打開的時候還需要再一次下載下傳上次已經有過的圖檔,相信使用者的流量會讓他們的叫罵聲很響亮。對于圖檔很多的應用,一個好的磁盤緩存直接決定了應用在使用者手機的留存時間。我們自己實作磁盤緩存,要考慮的太多,幸好uil提供了幾種常見的磁盤緩存政策,當然如果你覺得都不符合你的要求,你也可以自己去擴充
filecountlimiteddisccache(可以設定緩存圖檔的個數,當超過設定值,删除掉最先加入到硬碟的檔案)
limitedagedisccache(設定檔案存活的最長時間,當超過這個值,就删除該檔案)
totalsizelimiteddisccache(設定緩存bitmap的最大值,當超過這個值,删除最先加入到硬碟的檔案)
unlimiteddisccache(這個緩存類沒有任何的限制)
在uil中有着比較完整的存儲政策,根據預先指定的空間大小,使用頻率(生命周期),檔案個數的限制條件,都有着對應的實作政策。最基礎的接口disccacheaware和抽象類basedisccache
接下來我們來看看實作unlimiteddisccache的源代碼,通過源代碼我們發現他其實就是繼承了basedisccache,這個類内部沒有實作自己獨特的方法,也沒有重寫什麼,那麼我們就直接看basedisccache這個類。在分析這個類之前,我們先想想自己實作一個磁盤緩存需要做多少麻煩的事情:
1、圖檔的命名會不會重。你沒有辦法知道使用者下載下傳的圖檔原始的檔案名是怎麼樣的,是以很可能因為檔案重名将有用的圖檔給覆寫掉了。
2、當應用卡頓或網絡延遲的時候,同一張圖檔反複被下載下傳。
3、處理圖檔寫入磁盤可能遇到的延遲和同步問題。
basedisccache構造函數
首先,我們看一下basedisccache的構造函數:
cachedir:檔案緩存目錄
reservecachedir:備用的檔案緩存目錄,可以為null。它隻有當cachedir不能用的時候才有用。
filenamegenerator:檔案名生成器。為緩存的檔案生成檔案名。
我們可以看到一個filenamegenerator,接下來我們來了解uil具體是怎麼生成不重複的檔案名的。uil中有3種檔案命名政策,這裡我們隻對預設的檔案名政策進行分析。預設的檔案命名政策在defaultconfigurationfactory.createfilenamegenerator()。它是一個hashcodefilenamegenerator。真的是你意想不到的簡單,就是運用string.hashcode()進行檔案名的生成。
basedisccache.save()
basedisccache.get()
basedisccache.get()方法内部調用了basedisccache.getfile(...)方法,讓我們來分析一下這個在之前碰過的函數。 第2行就是利用filenamegenerator生成一個唯一的檔案名。第3~8行是指定緩存目錄,這時候你就可以清楚地看到cachedir和reservecachedir之間的關系了,當cachedir不可用的時候,就是用reservecachedir作為緩存目錄了。
最後傳回一個指向檔案的對象,但是要注意當file類型的對象指向的檔案不存在時,file會為null,而不是報錯。
現在,我們已經分析了uil的緩存機制。其實從uil的緩存機制的實作并不是很複雜,雖然有各種緩存機制,但是簡單地說:記憶體緩存其實就是利用map接口的對象在記憶體中進行緩存,可能有不同的存儲機制。磁盤緩存其實就是将檔案寫入磁盤。
參考連結:
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。
轉載:http://www.cnblogs.com/kissazi2/p/3931400.html