最近,總算有時間去做些平時喜歡而沒空去做的事情。一直覺得項目中使用的Image Loader適用性不強,昨晚在github隨便逛逛,發現一個開源項目Android-Universal-Image-Loader十分火熱。代碼並不十分復雜,卻寫的不錯,決定記錄和分享一下。
Android-Universal-Image-Loader是一個針對圖片加載、緩存的開源項目。github: https://github.com/nostra13/Android-Universal-Image-Loader
@author nostra13頻繁的更新記錄來看,Android-Universal-Image-Loader的后期維護比較好。從Applications using Universal Image Loader來看,還是相當強大的,如:EyeEm Camera, UPnP/DLNA Browser, Facebook Photo/Album,淘寶天貓,京東商城等。到底有何種魅力讓大家紛至沓來?
01. 項目包結構
圖 1
去掉無實際意義的包名:com.nostra13.universalimageloader,一切變顯得比較清楚了。
(1)cache主要是磁盤緩存及內存緩存預定的接口和常規實現類,包含的算法較多(並不復雜),如FIFO算法、LRU算法等。
(2)core明顯是整個Image Loader的核心包,圖片下載、適配顯示,並向上層應用提供各種接口,默認模板,還包括很多關鍵枚舉類、工具類。
(3)utils比較簡單些,常規工具類,如ImageSizeUtils、StorageUtils等。
02. 文件存儲策略
針對手機,流量、電量及體驗的要求,不斷向網絡發起重復的請求,都是不恰當的。常規存儲方式包括:文件存儲、數據庫存儲、SharedPreference、雲存儲(網絡存儲)及保存至內存等幾種手段。顯然,需要在本地進行持久化,就圖片而已,以文件的方式保存無疑是最好的選擇。至此,完成了雲端-->本地的持久化,但是如果簡單的每次都去”本地存儲“請求,請求不到再到雲端請求,無疑無法解決頻繁請求圖片所需要的速度和體驗。請記住,用戶對圖片的渴望程序是相當高,試想一下,ListView上下滾動時,每次直接到”本地存儲“請求時,整個界面多處在Loading,顯然體驗效果比較一般。因此,圖片一般都會存儲至內存中。從內存中讀取圖片的速度比本地加載的快得多,體驗也會更好。這就是平時通常說的三步:內存-->本地文件-->雲端。
在Android-Universal-Image-Loader,單純從包結構上就可以看出,cache.disc是負責硬盤存儲,而cache.memory是負責內存存儲的(ps.具體存儲實現方式還是在core包內)。而網絡請求應該在core.download中,core包在Part3.2會深入分析。
事實上,我們似乎忽略了一些東西,如果我發起請求的是本地圖片呢?如果是R.drawable.*的圖片呢?優點1:Android-Universal-Image-Loader設計得比較細致,在ImageDownloader接口枚舉類Scheme,明確指出所支持的請求類型:
HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN("");
021. 存儲空間、時間、數量策略
在cache.disc下,有着比較完整的存儲策略,根據預先指定的空間大小,使用頻率(生命周期),文件個數的約束條件,都有着對應的實現策略。最基礎的接口DiscCacheAware和抽象類BaseDiscCache。簡單類圖如下:
圖2
從類圖中,明顯可以看出,整個disc存儲的實現方式有四種:文件總數,文件總大小(或目錄),生命周期,無限制(即空間、時間維度的限制)。分析重點,是接口/抽象類。
public interface DiscCacheAware { /** * This method must not to save file on file system in fact. It is called after image was cached in cache directory * and it was decoded to bitmap in memory. Such order is required to prevent possible deletion of file after it was * cached on disc and before it was tried to decode to bitmap. */ void put(String key, File file); /** * Returns {@linkplain File file object} appropriate incoming key.<br /> * <b>NOTE:</b> Must <b>not to return</b> a null. Method must return specific {@linkplain File file object} for * incoming key whether file exists or not. */ File get(String key); /** Clears cache directory */ void clear(); }
具體看英文注釋,比較好理解,此處就不翻譯了。
022. 圖片文件的命名
如果你是心思細膩的用戶,在圖片瀏覽器中是否會經常看到一些小圖片甚至於廣告圖片。在一定程序上,本人挺鄙視這些圖片來源的應用。為什么把一些臨時下載的圖片,並無實際意義的圖片,占據如此重要的位置。對此,我只想說:我會直接刪掉這個目錄,如果知道是哪個應用的話,估計我也會卸載。我不禁猜想:如果所有應用緩存的圖片,都是可以被媒體庫掃描出來的話,這是什么境況呢?慢,肯定的。
因此,圖片保存時,以什么樣的文件名進行存儲是相當重要的。這個必須要根據需求進行定制生成的策略。舉三個例子:
(1)如果要做一個“雲相冊”,保存圖片后,當然希望能夠使用“美圖秀秀”、“相機360”等進行編輯。
(2)如果要做一個“淘寶客戶端”,所有瀏覽過的商品圖片都存儲,並可被其它圖片瀏覽器感知,這似乎不恰當吧。
(3)如果要做一個“私密相冊”,保存本地的圖片,自然不希望別人去感知。
結論:用戶有意識去保存的圖片,應該可被感知;用戶無意識緩存的圖片,不應該被“外界”感知,但也有例外如第3種。當然,第三種權限的控制也是可行的。
Android-Universal-Image-Loader在保存圖片時,需要一個FileNameGenerator(譯:文件名生產者)。抽象類BaseDiscCache中存在兩個構造器:
public BaseDiscCache(File cacheDir) { this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator()); } public BaseDiscCache(File cacheDir, FileNameGenerator fileNameGenerator) { if (cacheDir == null) { throw new IllegalArgumentException(String.format(ERROR_ARG_NULL, "cacheDir")); } if (fileNameGenerator == null) { throw new IllegalArgumentException(String.format(ERROR_ARG_NULL, "fileNameGenerator")); } this.cacheDir = cacheDir; this.fileNameGenerator = fileNameGenerator; }
DefaultConfigurationFactory提供默認文件名生產者為HashCodeFileNameGenerator,默認實現比較簡單:返回字符串的hashCode。
核心類圖:
圖3
Md5FileNameGenerator相關原理可查詢MD5加密算法,JDK自帶類java.security.MessageDigest。
03. 內存存儲
這部分涉及的知識面比較多,算法、鏈表、堆棧、隊列、泛型等。Android-Universal-Image-Loader在“開閉原則”上做得很好,接口簡潔、實用,基於接口,都可以自行擴展功能。
直接看下接口MemoryCacheAware的定義:
public interface MemoryCacheAware<K, V> { /** * Puts value into cache by key * * @return <b>true</b> - if value was put into cache successfully, <b>false</b> - if value was <b>not</b> put into * cache */ boolean put(K key, V value); /** Returns value by key. If there is no value for key then null will be returned. */ V get(K key); /** Removes item by key */ void remove(K key); /** Returns all keys of cache */ Collection<K> keys(); /** Remove all items from cache */ void clear(); }
如果不熟悉泛型的話,建議復習一下,或者直接把MemoryCacheAware<K, V>當MemoryCacheAware<String, Bitmap>看,會方便些。
核心類圖:
圖4
大體結構上以文件存儲比較相近,關鍵性的區別在於BaseMemoryCache子類使用的都是WeakReference(弱引用),FuzzyKeyMemoryCache、LimitedAgeMemoryCache、LruMemoryCache使用的是強引用。本節對內存存儲方式,比較關鍵的概念:強引用、軟引用、弱引用及虛引用。
回顧:
(1)StrongReference(強引用)
強引用就是平時經常使用的,如常規new Object()。如果一個對象具有強引用,那垃圾回收器絕不會回收。內存不足,甚至出現OOM時,也不會隨意回收強引用的對象。
(2)SoftReference(軟引用)
在內存空間足夠,垃圾回收器不會回收它;如果內存空間不足,垃圾回收器就會回收軟引用的對象。
(3)WeakReference(弱引用)
弱引用相對軟引用,具有更短暫的生命周期。常規的GC,只要被掃描到,都會直接被回收。
(4)PhantomReference(虛引用)
虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用主要用來跟蹤對象被垃圾回收器回收的活動。沒用過。。。
選其一FIFOLimitedMemoryCache進行分析,構造時,必須要指定大小(單位:字節)。當設置的大小超過16MB(Android默認分配的大小好像也是這個)時,會有警告。
public LimitedMemoryCache(int sizeLimit) { this.sizeLimit = sizeLimit; cacheSize = new AtomicInteger(); if (sizeLimit > MAX_NORMAL_CACHE_SIZE) { L.w("You set too large memory cache size (more than %1$d Mb)", MAX_NORMAL_CACHE_SIZE_IN_MB); } }
而FIFOLimitedMemoryCache的實現也相對簡單,只要你清楚FIFO的基本概念:先進先出。
public class FIFOLimitedMemoryCache extends LimitedMemoryCache<String, Bitmap> { private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>()); public FIFOLimitedMemoryCache(int sizeLimit) { super(sizeLimit); } @Override public boolean put(String key, Bitmap value) { if (super.put(key, value)) { queue.add(value); return true; } else { return false; } } @Override public void remove(String key) { Bitmap value = super.get(key); if (value != null) { queue.remove(value); } super.remove(key); } @Override public void clear() { queue.clear(); super.clear(); } @Override protected int getSize(Bitmap value) { return value.getRowBytes() * value.getHeight(); } @Override protected Bitmap removeNext() { return queue.remove(0); } @Override protected Reference<Bitmap> createReference(Bitmap value) { return new WeakReference<Bitmap>(value); } }
在removeNext()方法中,可以看到每次都是移除隊列中的第一個對象。
Part3.1就此結束。
原來,用心寫一篇文章,還是相當的困難。前前后后,走讀,分析,類圖,補充部分遺漏的知識點,足足用了半天的時間。不過,收獲良多。
Part3.2主要是對core包中的下載設計技巧進行分析、研究,core.assist、core.display只會簡單過一下。
Part3.3重在如何在實際項目中運用Android-Universal-Image-Loader,如何自行擴展部分功能。
估計,Part3.2/3.3會晚些日子才會更新,后面會對自己做的一些項目進行一定的總結。希望大家多多支持。
有任何問題,請郵件至:Osmondy@sina.com