從源代碼分析Android Universal ImageLoader的緩存處理機制
通過本文帶大家一起看過UIL這個國內(nèi)外大牛都追捧的圖片緩存類庫的緩存處理機制??戳薝IL中的緩存實現(xiàn),才發(fā)現(xiàn)其實這個東西不難,沒有太多的進程調(diào)度,沒有各種內(nèi)存讀取控制機制、沒有各種異常處理。反正UIL中不單代碼寫的簡單,連處理都簡單。但是這個類庫這么好用,又有這么多人用,那么非常有必要看看他是怎么實現(xiàn)的。先了解UIL中緩存流程的原理圖。
原理示意圖
主體有三個,分別是UI,緩存模塊和數(shù)據(jù)源(網(wǎng)絡)。它們之間的關系如下:
① UI:請求數(shù)據(jù),使用唯一的Key值索引Memory Cache中的Bitmap。
② 內(nèi)存緩存:緩存搜索,如果能找到Key值對應的Bitmap,則返回數(shù)據(jù)。否則執(zhí)行第三步。
③ 硬盤存儲:使用唯一Key值對應的文件名,檢索SDCard上的文件。
④ 如果有對應文件,使用BitmapFactory.decode*方法,解碼Bitmap并返回數(shù)據(jù),同時將數(shù)據(jù)寫入緩存。如果沒有對應文件,執(zhí)行第五步。
⑤ 下載圖片:啟動異步線程,從數(shù)據(jù)源下載數(shù)據(jù)(Web)。
⑥ 若下載成功,將數(shù)據(jù)同時寫入硬盤和緩存,并將Bitmap顯示在UI中。
接下來,我們回顧一下UIL中緩存的配置(具體的見《UNIVERSAL IMAGE LOADER.PART 2》)。重點關注注釋部分,我們可以根據(jù)自己需要配置內(nèi)存、磁盤緩存的實現(xiàn)。
File cacheDir = StorageUtils.getCacheDirectory(context, "UniversalImageLoader/Cache"); ImageLoaderConfiguration config = new ImageLoaderConfiguration .Builder(getApplicationContext()) .maxImageWidthForMemoryCache() .maxImageHeightForMemoryCache() .httpConnectTimeout() .httpReadTimeout() .threadPoolSize() .threadPriority(Thread.MIN_PRIORITY + ) .denyCacheImageMultipleSizesInMemory() .memoryCache(new UsingFreqLimitedCache()) // 你可以傳入自己的內(nèi)存緩存 .discCache(new UnlimitedDiscCache(cacheDir)) // 你可以傳入自己的磁盤緩存 .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) .build();
UIL中的內(nèi)存緩存策略
1. 只使用的是強引用緩存
•LruMemoryCache(這個類就是這個開源框架默認的內(nèi)存緩存類,緩存的是bitmap的強引用,下面我會從源碼上面分析這個類)
2.使用強引用和弱引用相結合的緩存有
UsingFreqLimitedMemoryCache(如果緩存的圖片總量超過限定值,先刪除使用頻率最小的bitmap)
•LRULimitedMemoryCache(這個也是使用的lru算法,和LruMemoryCache不同的是,他緩存的是bitmap的弱引用)
•FIFOLimitedMemoryCache(先進先出的緩存策略,當超過設定值,先刪除最先加入緩存的bitmap)
•LargestLimitedMemoryCache(當超過緩存限定值,先刪除最大的bitmap對象)
•LimitedAgeMemoryCache(當 bitmap加入緩存中的時間超過我們設定的值,將其刪除)
3.只使用弱引用緩存
WeakMemoryCache(這個類緩存bitmap的總大小沒有限制,唯一不足的地方就是不穩(wěn)定,緩存的圖片容易被回收掉)
我們直接選擇UIL中的默認配置緩存策略進行分析。
ImageLoaderConfiguration config = ImageLoaderConfiguration.createDefault(context);
ImageLoaderConfiguration.createDefault(…)這個方法最后是調(diào)用Builder.build()方法創(chuàng)建默認的配置參數(shù)的。默認的內(nèi)存緩存實現(xiàn)是LruMemoryCache,磁盤緩存是UnlimitedDiscCache。
LruMemoryCache解析
LruMemoryCache:一種使用強引用來保存有數(shù)量限制的Bitmap的cache(在空間有限的情況,保留最近使用過的Bitmap)。每次Bitmap被訪問時,它就被移動到一個隊列的頭部。當Bitmap被添加到一個空間已滿的cache時,在隊列末尾的Bitmap會被擠出去并變成適合被GC回收的狀態(tài)。
注意:這個cache只使用強引用來保存Bitmap。
LruMemoryCache實現(xiàn)MemoryCache,而MemoryCache繼承自MemoryCacheAware。
public interface MemoryCache extends MemoryCacheAware<String, Bitmap>
下面給出繼承關系圖
LruMemoryCache.get(…)
我相信接下去你看到這段代碼的時候會跟我一樣驚訝于代碼的簡單,代碼中除了異常判斷,就是利用synchronized進行同步控制。
/** * Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head * of the queue. This returns null if a Bitmap is not cached. */ @Override public final Bitmap get(String key) { if (key == null) { throw new NullPointerException("key == null"); } synchronized (this) { return map.get(key); } }
我們會好奇,這不是就簡簡單單將Bitmap從map中取出來嗎?但LruMemoryCache聲稱保留在空間有限的情況下保留最近使用過的Bitmap。不急,讓我們細細觀察一下map。他是一個LinkedHashMap<String, Bitmap>型的對象。
LinkedHashMap中的get()方法不僅返回所匹配的值,并且在返回前還會將所匹配的key對應的entry調(diào)整在列表中的順序(LinkedHashMap使用雙鏈表來保存數(shù)據(jù)),讓它處于列表的最后。當然,這種情況必須是在LinkedHashMap中accessOrder==true的情況下才生效的,反之就是get()方法不會改變被匹配的key對應的entry在列表中的位置。
@Override public V get(Object key) { /* * This method is overridden to eliminate the need for a polymorphic * invocation in superclass at the expense of code duplication. */ if (key == null) { HashMapEntry<K, V> e = entryForNullKey; if (e == null) return null; if (accessOrder) makeTail((LinkedEntry<K, V>) e); return e.value; } // Replace with Collections.secondaryHash when the VM is fast enough (http://b/). int hash = secondaryHash(key); HashMapEntry<K, V>[] tab = table; for (HashMapEntry<K, V> e = tab[hash & (tab.length - )]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { if (accessOrder) makeTail((LinkedEntry<K, V>) e); return e.value; } } return null; }
代碼第11行的makeTail()就是調(diào)整entry在列表中的位置,其實就是雙向鏈表的調(diào)整。它判斷accessOrder。到現(xiàn)在我們就清楚LruMemoryCache使用LinkedHashMap來緩存數(shù)據(jù),在LinkedHashMap.get()方法執(zhí)行后,LinkedHashMap中entry的順序會得到調(diào)整。那么我們怎么保證最近使用的項不會被剔除呢?接下去,讓我們看看LruMemoryCache.put(...)。
LruMemoryCache.put(...)
注意到代碼第8行中的size+= sizeOf(key, value),這個size是什么呢?我們注意到在第19行有一個trimToSize(maxSize),trimToSize(...)這個函數(shù)就是用來限定LruMemoryCache的大小不要超過用戶限定的大小,cache的大小由用戶在LruMemoryCache剛開始初始化的時候限定。
@Override public final boolean put(String key, Bitmap value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } synchronized (this) { size += sizeOf(key, value); //map.put()的返回值如果不為空,說明存在跟key對應的entry,put操作只是更新原有key對應的entry Bitmap previous = map.put(key, value); if (previous != null) { size -= sizeOf(key, previous); } } trimToSize(maxSize); return true; }
其實不難想到,當Bitmap緩存的大小超過原來設定的maxSize時應該是在trimToSize(...)這個函數(shù)中做到的。這個函數(shù)做的事情也簡單,遍歷map,將多余的項(代碼中對應toEvict)剔除掉,直到當前cache的大小等于或小于限定的大小。
private void trimToSize(int maxSize) { while (true) { String key; Bitmap value; synchronized (this) { if (size < || (map.isEmpty() && size != )) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (size <= maxSize || map.isEmpty()) { break; } Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next(); if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= sizeOf(key, value); } } }
這時候我們會有一個以為,為什么遍歷一下就可以將使用最少的bitmap緩存給剔除,不會誤刪到最近使用的bitmap緩存嗎?首先,我們要清楚,LruMemoryCache定義的最近使用是指最近用get或put方式操作到的bitmap緩存。其次,之前我們直到LruMemoryCache的get操作其實是通過其內(nèi)部字段LinkedHashMap.get(...)實現(xiàn)的,當LinkedHashMap的accessOrder==true時,每一次get或put操作都會將所操作項(圖中第3項)移動到鏈表的尾部(見下圖,鏈表頭被認為是最少使用的,鏈表尾被認為是最常使用的。),每一次操作到的項我們都認為它是最近使用過的,當內(nèi)存不夠的時候被剔除的優(yōu)先級最低。需要注意的是一開始的LinkedHashMap鏈表是按插入的順序構成的,也就是第一個插入的項就在鏈表頭,最后一個插入的就在鏈表尾。假設只要剔除圖中的1,2項就能讓LruMemoryCache小于原先限定的大小,那么我們只要從鏈表頭遍歷下去(從1→最后一項)那么就可以剔除使用最少的項了。
至此,我們就知道了LruMemoryCache緩存的整個原理,包括他怎么put、get、剔除一個元素的的策略。接下去,我們要開始分析默認的磁盤緩存策略了。
UIL中的磁盤緩存策略
像新浪微博、花瓣這種應用需要加載很多圖片,本來圖片的加載就慢了,如果下次打開的時候還需要再一次下載上次已經(jīng)有過的圖片,相信用戶的流量會讓他們的叫罵聲很響亮。對于圖片很多的應用,一個好的磁盤緩存直接決定了應用在用戶手機的留存時間。我們自己實現(xiàn)磁盤緩存,要考慮的太多,幸好UIL提供了幾種常見的磁盤緩存策略,當然如果你覺得都不符合你的要求,你也可以自己去擴展
•FileCountLimitedDiscCache(可以設定緩存圖片的個數(shù),當超過設定值,刪除掉最先加入到硬盤的文件)
•LimitedAgeDiscCache(設定文件存活的最長時間,當超過這個值,就刪除該文件)
•TotalSizeLimitedDiscCache(設定緩存bitmap的最大值,當超過這個值,刪除最先加入到硬盤的文件)
•UnlimitedDiscCache(這個緩存類沒有任何的限制)
在UIL中有著比較完整的存儲策略,根據(jù)預先指定的空間大小,使用頻率(生命周期),文件個數(shù)的約束條件,都有著對應的實現(xiàn)策略。最基礎的接口DiscCacheAware和抽象類BaseDiscCache
UnlimitedDiscCache解析
UnlimitedDiscCache實現(xiàn)disk cache接口,是ImageLoaderConfiguration中默認的磁盤緩存處理。用它的時候,磁盤緩存的大小是不受限的。
接下來我們來看看實現(xiàn)UnlimitedDiscCache的源代碼,通過源代碼我們發(fā)現(xiàn)他其實就是繼承了BaseDiscCache,這個類內(nèi)部沒有實現(xiàn)自己獨特的方法,也沒有重寫什么,那么我們就直接看BaseDiscCache這個類。在分析這個類之前,我們先想想自己實現(xiàn)一個磁盤緩存需要做多少麻煩的事情:
1、圖片的命名會不會重。你沒有辦法知道用戶下載的圖片原始的文件名是怎么樣的,因此很可能因為文件重名將有用的圖片給覆蓋掉了。
2、當應用卡頓或網(wǎng)絡延遲的時候,同一張圖片反復被下載。
3、處理圖片寫入磁盤可能遇到的延遲和同步問題。
BaseDiscCache構造函數(shù)
首先,我們看一下BaseDiscCache的構造函數(shù):
cacheDir:文件緩存目錄
reserveCacheDir:備用的文件緩存目錄,可以為null。它只有當cacheDir不能用的時候才有用。
fileNameGenerator:文件名生成器。為緩存的文件生成文件名。
public BaseDiscCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) { if (cacheDir == null) { throw new IllegalArgumentException("cacheDir" + ERROR_ARG_NULL); } if (fileNameGenerator == null) { throw new IllegalArgumentException("fileNameGenerator" + ERROR_ARG_NULL); } this.cacheDir = cacheDir; this.reserveCacheDir = reserveCacheDir; this.fileNameGenerator = fileNameGenerator; }
我們可以看到一個fileNameGenerator,接下來我們來了解UIL具體是怎么生成不重復的文件名的。UIL中有3種文件命名策略,這里我們只對默認的文件名策略進行分析。默認的文件命名策略在DefaultConfigurationFactory.createFileNameGenerator()。它是一個HashCodeFileNameGenerator。真的是你意想不到的簡單,就是運用String.hashCode()進行文件名的生成。
public class HashCodeFileNameGenerator implements FileNameGenerator { @Override public String generate(String imageUri) { return String.valueOf(imageUri.hashCode()); } }
BaseDiscCache.save()
分析完了命名策略,再看一下BaseDiscCache.save(...)方法。注意到第2行有一個getFile()函數(shù),它主要用于生成一個指向緩存目錄中的文件,在這個函數(shù)里面調(diào)用了剛剛介紹過的fileNameGenerator來生成文件名。注意第3行的tmpFile,它是用來寫入bitmap的臨時文件(見第8行),然后就把這個文件給刪除了。大家可能會困惑,為什么在save()函數(shù)里面沒有判斷要寫入的bitmap文件是否存在的判斷,我們不由得要看看UIL中是否有對它進行判斷。還記得我們在《從代碼分析Android-Universal-Image-Loader的圖片加載、顯示流程》介紹的,UIL加載圖片的一般流程是先判斷內(nèi)存中是否有對應的Bitmap,再判斷磁盤(disk)中是否有,如果沒有就從網(wǎng)絡中加載。最后根據(jù)原先在UIL中的配置判斷是否需要緩存Bitmap到內(nèi)存或磁盤中。也就是說,當需要調(diào)用BaseDiscCache.save(...)之前,其實已經(jīng)判斷過這個文件不在磁盤中。
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException { File imageFile = getFile(imageUri); File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX); boolean loaded = false; try { OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize); try { loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize); } finally { IoUtils.closeSilently(os); } } finally { IoUtils.closeSilently(imageStream); if (loaded && !tmpFile.renameTo(imageFile)) { loaded = false; } if (!loaded) { tmpFile.delete(); } } return loaded; }
BaseDiscCache.get()
BaseDiscCache.get()方法內(nèi)部調(diào)用了BaseDiscCache.getFile(...)方法,讓我們來分析一下這個在之前碰過的函數(shù)。 第2行就是利用fileNameGenerator生成一個唯一的文件名。第3~8行是指定緩存目錄,這時候你就可以清楚地看到cacheDir和reserveCacheDir之間的關系了,當cacheDir不可用的時候,就是用reserveCachedir作為緩存目錄了。
最后返回一個指向文件的對象,但是要注意當File類型的對象指向的文件不存在時,file會為null,而不是報錯。
protected File getFile(String imageUri) { String fileName = fileNameGenerator.generate(imageUri); File dir = cacheDir; if (!cacheDir.exists() && !cacheDir.mkdirs()) { if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) { dir = reserveCacheDir; } } return new File(dir, fileName); }
總結
現(xiàn)在,我們已經(jīng)分析了UIL的緩存機制。其實從UIL的緩存機制的實現(xiàn)并不是很復雜,雖然有各種緩存機制,但是簡單地說:內(nèi)存緩存其實就是利用Map接口的對象在內(nèi)存中進行緩存,可能有不同的存儲機制。磁盤緩存其實就是將文件寫入磁盤。
相關文章
Android隱私協(xié)議提示彈窗的實現(xiàn)流程詳解
這篇文章主要介紹了Android隱私協(xié)議提示彈窗的實現(xiàn)流程,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-01-01Android中TextView和ImageView實現(xiàn)傾斜效果
這篇文章主要為大家詳細介紹了Android中TextView和ImageView實現(xiàn)傾斜效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08Android ViewPager與radiogroup實現(xiàn)關聯(lián)示例
本篇文章主要介紹了Android ViewPager與radiogroup實現(xiàn)關聯(lián)示例,具有一定的參考價值,有興趣的可以了解一下。2017-03-03Android getActivity()為空的問題解決辦法
這篇文章主要介紹了Android getActivity()為空的問題解決辦法的相關資料,導致apk空指針崩潰問題,很嚴重的問題,為了解決這問題,上網(wǎng)搜索了很多資料,需要的朋友可以參考下2017-07-07神經(jīng)網(wǎng)絡API、Kotlin支持,那些你必須知道的Android 8.1預覽版和Android Studio 3.0新特
這篇文章主要介紹了神經(jīng)網(wǎng)絡API、Kotlin支持,那些你必須了解的Android 8.1預覽版和Android Studio 3.0新特性,需要的朋友可以參考下2017-10-10Android布局ConstraintLayout代碼修改約束及輔助功能
這篇文章主要為大家介紹了Android布局ConstraintLayout代碼修改約束及輔助功能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09Android RelativeLayout相對布局屬性簡析
在Android應用開發(fā)過程中,為了界面的美觀考慮,經(jīng)常會使用到布局方面的屬性,本文就以此問題深入解析,詳解一下Android RelativeLayout相對布局屬性在實際開發(fā)中的應用,需要的朋友可以參考下2012-11-11android實用工具類分享(獲取內(nèi)存/檢查網(wǎng)絡/屏幕高度/手機分辨率)
這篇文章主要介紹了android實用工具類,包括獲取內(nèi)存、檢查網(wǎng)絡、屏幕高度、手機分辨率、獲取版本號等功能,需要的朋友可以參考下2014-03-03Android如何給Textview添加菜單項詳解(Java)
TextView是android里面用的最多的控件,TextView類似一般UI中的Label,TextBlock等控件,只是為了單純的顯示一行或多行文本,下面這篇文章主要給大家介紹了關于Android如何給Textview添加菜單項的相關資料,需要的朋友可以參考下2022-01-01