全面解析Android的開源圖片框架Universal-Image-Loader

全面解析Android的開源圖片框架Universal-Image-Loader
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

相信大家平時做Android應用的時候,多少會接觸到非同步載入圖片,或者載入大量圖片的問題,而載入圖片我們常常會遇到許多的問題,比如說圖片的錯亂,OOM等問題,對於新手來說,這些問題解決起來會比較吃力,所以就有很多的開源圖片載入框架應運而生,比較著名的就是Universal-Image-Loader,相信很多朋友都聽過或者使用過這個強大的圖片載入框架,今天這篇文章就是對這個框架的基本介紹以及使用,主要是幫助那些沒有使用過這個框架的朋友們。該專案存在於Github上面https://github.com/nostra13/Android-Universal-Image-Loader,我們可以先看看這個開源庫存在哪些特徵

多執行緒下載圖片,圖片可以來源於網路,檔案系統,專案資料夾assets中以及drawable中等
支援隨意的配置ImageLoader,例如執行緒池,圖片下載器,記憶體快取策略,硬碟快取策略,圖片顯示選項以及其他的一些配置
支援圖片的記憶體快取,檔案系統快取或者SD卡快取
支援圖片下載過程的監聽
根據控制元件(ImageView)的大小對Bitmap進行裁剪,減少Bitmap佔用過多的記憶體
較好的控制圖片的載入過程,例如暫停圖片載入,重新開始載入圖片,一般使用在ListView,GridView中
動過程中暫停載入圖片
停止滑動的時候去載入圖片
供在較慢的網路下對圖片進行載入。

當然上面列舉的特性可能不全,要想了解一些其他的特性只能通過我們的使用慢慢去發現了,接下來我們就看看這個開源庫的簡單使用吧。

新建一個Android專案,下載JAR包新增到工程libs目錄下。
新建一個MyApplication繼承Application,並在onCreate()中建立ImageLoader的配置引數,並初始化到ImageLoader中程式碼如下:


package com.example.uil; 
import com.nostra13.universalimageloader.core.ImageLoader; 
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; 
import android.app.Application; 
public class MyApplication extends Application { 
@Override 
public void onCreate() { 
super.onCreate(); 
//建立預設的ImageLoader配置引數 
ImageLoaderConfiguration configuration = ImageLoaderConfiguration 
.createDefault(this); 
//Initialize ImageLoader with configuration. 
ImageLoader.getInstance().init(configuration); 
} 
} 

ImageLoaderConfiguration是圖片載入器ImageLoader的配置引數,使用了建造者模式,這裡是直接使用了createDefault()方法建立一個預設的ImageLoaderConfiguration,當然我們還可以自己設定ImageLoaderConfiguration,設定如下


File cacheDir = StorageUtils.getCacheDirectory(context); 
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context) 
.memoryCacheExtraOptions(480, 800) // default = device screen dimensions 
.diskCacheExtraOptions(480, 800, CompressFormat.JPEG, 75, null) 
.taskExecutor(...) 
.taskExecutorForCachedImages(...) 
.threadPoolSize(3) // default 
.threadPriority(Thread.NORM_PRIORITY - 1) // default 
.tasksProcessingOrder(QueueProcessingType.FIFO) // default 
.denyCacheImageMultipleSizesInMemory() 
.memoryCache(new LruMemoryCache(2 * 1024 * 1024)) 
.memoryCacheSize(2 * 1024 * 1024) 
.memoryCacheSizePercentage(13) // default 
.diskCache(new UnlimitedDiscCache(cacheDir)) // default 
.diskCacheSize(50 * 1024 * 1024) 
.diskCacheFileCount(100) 
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default 
.imageDownloader(new BaseImageDownloader(context)) // default 
.imageDecoder(new BaseImageDecoder()) // default 
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default 
.writeDebugLogs() 
.build(); 

上面的這些就是所有的選項配置,我們在專案中不需要每一個都自己設定,一般使用createDefault()建立的ImageLoaderConfiguration就能使用,然後呼叫ImageLoader的init()方法將ImageLoaderConfiguration引數傳遞進去,ImageLoader使用單例模式。

配置Android Manifest檔案


<manifest> 
<uses-permission android:name="android.permission.INTERNET" /> 
<!-- Include next permission if you want to allow UIL to cache images on SD card --> 
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 
... 
<application android:name="MyApplication"> 
... 
</application> 
</manifest> 

接下來我們就可以來載入圖片了,首先我們定義好Activity的佈局檔案


<?xml version="1.0" encoding="utf-8"?> 
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="fill_parent" 
android:layout_height="fill_parent"> 
<ImageView 
android:layout_gravity="center" 
android:id="@ id/image" 
android:src="@drawable/ic_empty" 
android:layout_width="wrap_content" 
android:layout_height="wrap_content" /> 
</FrameLayout> 

裡面只有一個ImageView,很簡單,接下來我們就去載入圖片,我們會發現ImageLader提供了幾個圖片載入的方法,主要是這幾個displayImage(), loadImage(),loadImageSync(),loadImageSync()方法是同步的,android4.0有個特性,網路操作不能在主執行緒,所以loadImageSync()方法我們就不去使用
.
loadimage()載入圖片

我們先使用ImageLoader的loadImage()方法來載入網路圖片


final ImageView mImageView = (ImageView) findViewById(R.id.image); 
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg"; 
ImageLoader.getInstance().loadImage(imageUrl, new ImageLoadingListener() { 
@Override 
public void onLoadingStarted(String imageUri, View view) { 
} 
@Override 
public void onLoadingFailed(String imageUri, View view, 
FailReason failReason) { 
} 
@Override 
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { 
mImageView.setImageBitmap(loadedImage); 
} 
@Override 
public void onLoadingCancelled(String imageUri, View view) { 
} 
}); 

傳入圖片的url和ImageLoaderListener, 在回撥方法onLoadingComplete()中將loadedImage設定到ImageView上面就行了,如果你覺得傳入ImageLoaderListener太複雜了,我們可以使用SimpleImageLoadingListener類,該類提供了ImageLoaderListener介面方法的空實現,使用的是預設介面卡模式


final ImageView mImageView = (ImageView) findViewById(R.id.image); 
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg"; 
ImageLoader.getInstance().loadImage(imageUrl, new SimpleImageLoadingListener(){ 
@Override 
public void onLoadingComplete(String imageUri, View view, 
Bitmap loadedImage) { 
super.onLoadingComplete(imageUri, view, loadedImage); 
mImageView.setImageBitmap(loadedImage); 
} 
}); 

如果我們要指定圖片的大小該怎麼辦呢,這也好辦,初始化一個ImageSize物件,指定圖片的寬和高,程式碼如下


final ImageView mImageView = (ImageView) findViewById(R.id.image); 
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg"; 
ImageSize mImageSize = new ImageSize(100, 100); 
ImageLoader.getInstance().loadImage(imageUrl, mImageSize, new SimpleImageLoadingListener(){ 
@Override 
public void onLoadingComplete(String imageUri, View view, 
Bitmap loadedImage) { 
super.onLoadingComplete(imageUri, view, loadedImage); 
mImageView.setImageBitmap(loadedImage); 
} 
}); 

上面只是很簡單的使用ImageLoader來載入網路圖片,在實際的開發中,我們並不會這麼使用,那我們平常會怎麼使用呢?我們會用到DisplayImageOptions,他可以配置一些圖片顯示的選項,比如圖片在載入中ImageView顯示的圖片,是否需要使用記憶體快取,是否需要使用檔案快取等等,可供我們選擇的配置如下


DisplayImageOptions options = new DisplayImageOptions.Builder() 
.showImageOnLoading(R.drawable.ic_stub) // resource or drawable 
.showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable 
.showImageOnFail(R.drawable.ic_error) // resource or drawable 
.resetViewBeforeLoading(false) // default 
.delayBeforeLoading(1000) 
.cacheInMemory(false) // default 
.cacheOnDisk(false) // default 
.preProcessor(...) 
.postProcessor(...) 
.extraForDownloader(...) 
.considerExifParams(false) // default 
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default 
.bitmapConfig(Bitmap.Config.ARGB_8888) // default 
.decodingOptions(...) 
.displayer(new SimpleBitmapDisplayer()) // default 
.handler(new Handler()) // default 
.build(); 

我們將上面的程式碼稍微修改下


final ImageView mImageView = (ImageView) findViewById(R.id.image); 
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg"; 
ImageSize mImageSize = new ImageSize(100, 100); 
//顯示圖片的配置 
DisplayImageOptions options = new DisplayImageOptions.Builder() 
.cacheInMemory(true) 
.cacheOnDisk(true) 
.bitmapConfig(Bitmap.Config.RGB_565) 
.build(); 
ImageLoader.getInstance().loadImage(imageUrl, mImageSize, options, new SimpleImageLoadingListener(){ 
@Override 
public void onLoadingComplete(String imageUri, View view, 
Bitmap loadedImage) { 
super.onLoadingComplete(imageUri, view, loadedImage); 
mImageView.setImageBitmap(loadedImage); 
} 
}); 

我們使用了DisplayImageOptions來配置顯示圖片的一些選項,這裡我新增了將圖片快取到記憶體中已經快取圖片到檔案系統中,這樣我們就不用擔心每次都從網路中去載入圖片了,是不是很方便呢,但是DisplayImageOptions選項中有些選項對於loadImage()方法是無效的,比如showImageOnLoading, showImageForEmptyUri等,

displayImage()載入圖片

接下來我們就來看看網路圖片載入的另一個方法displayImage(),程式碼如下


final ImageView mImageView = (ImageView) findViewById(R.id.image); 
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg"; 
//顯示圖片的配置 
DisplayImageOptions options = new DisplayImageOptions.Builder() 
.showImageOnLoading(R.drawable.ic_stub) 
.showImageOnFail(R.drawable.ic_error) 
.cacheInMemory(true) 
.cacheOnDisk(true) 
.bitmapConfig(Bitmap.Config.RGB_565) 
.build(); 
ImageLoader.getInstance().displayImage(imageUrl, mImageView, options); 

從上面的程式碼中,我們可以看出,使用displayImage()比使用loadImage()方便很多,也不需要新增ImageLoadingListener介面,我們也不需要手動設定ImageView顯示Bitmap物件,直接將ImageView作為引數傳遞到displayImage()中就行了,圖片顯示的配置選項中,我們新增了一個圖片載入中ImageVIew上面顯示的圖片,以及圖片載入出現錯誤顯示的圖片,效果如下,剛開始顯示ic_stub圖片,如果圖片載入成功顯示圖片,載入產生錯誤顯示ic_error

https://codertw.com/wp-content/uploads/2018/07/20180703141017-5b3b83c90aeb6.gif (417×615)https://codertw.com/wp-content/uploads/2018/07/20180703141017-5b3b83c985ac1.gif (417×615)

這個方法使用起來比較方便,而且使用displayImage()方法 他會根據控制元件的大小和imageScaleType來自動裁剪圖片,我們修改下MyApplication,開啟Log列印


public class MyApplication extends Application { 
@Override 
public void onCreate() { 
super.onCreate(); 
//建立預設的ImageLoader配置引數 
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this) 
.writeDebugLogs() //列印log資訊 
.build(); 
//Initialize ImageLoader with configuration. 
ImageLoader.getInstance().init(configuration); 
} 
} 

我們來看下圖片載入的Log資訊

https://codertw.com/wp-content/uploads/2018/07/20180703141017-5b3b83c9ddfdb.jpg (862×288)

第一條資訊中,告訴我們開始載入圖片,列印出圖片的url以及圖片的最大寬度和高度,圖片的寬高預設是裝置的寬高,當然如果我們很清楚圖片的大小,我們也可以去設定這個大小,在ImageLoaderConfiguration的選項中memoryCacheExtraOptions(int maxImageWidthForMemoryCache, int maxImageHeightForMemoryCache)
第二條資訊顯示我們載入的圖片來源於網路
第三條資訊顯示圖片的原始大小為1024 x 682 經過裁剪變成了512 x 341
第四條顯示圖片加入到了記憶體快取中,我這裡沒有加入到sd卡中,所以沒有加入檔案快取的Log

我們在載入網路圖片的時候,經常有需要顯示圖片下載進度的需求,Universal-Image-Loader當然也提供這樣的功能,只需要在displayImage()方法中傳入ImageLoadingProgressListener介面就行了,程式碼如下


imageLoader.displayImage(imageUrl, mImageView, options, new SimpleImageLoadingListener(), new ImageLoadingProgressListener() { 
@Override 
public void onProgressUpdate(String imageUri, View view, int current, 
int total) { 
} 
}); 

由於displayImage()方法中帶ImageLoadingProgressListener引數的方法都有帶ImageLoadingListener引數,所以我這裡直接new 一個SimpleImageLoadingListener,然後我們就可以在回撥方法onProgressUpdate()得到圖片的載入進度。

載入其他來源的圖片

使用Universal-Image-Loader框架不僅可以載入網路圖片,還可以載入sd卡中的圖片,Content provider等,使用也很簡單,只是將圖片的url稍加的改變下就行了,下面是載入檔案系統的圖片


//顯示圖片的配置 
DisplayImageOptions options = new DisplayImageOptions.Builder() 
.showImageOnLoading(R.drawable.ic_stub) 
.showImageOnFail(R.drawable.ic_error) 
.cacheInMemory(true) 
.cacheOnDisk(true) 
.bitmapConfig(Bitmap.Config.RGB_565) 
.build(); 
final ImageView mImageView = (ImageView) findViewById(R.id.image); 
String imagePath = "/mnt/sdcard/image.png"; 
String imageUrl = Scheme.FILE.wrap(imagePath); 
//  String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; 
imageLoader.displayImage(imageUrl, mImageView, options); 

當然還有來源於Content provider,drawable,assets中,使用的時候也很簡單,我們只需要給每個圖片來源的地方加上Scheme包裹起來(Content provider除外),然後當做圖片的url傳遞到imageLoader中,Universal-Image-Loader框架會根據不同的Scheme獲取到輸入流


//圖片來源於Content provider 
String contentprividerUrl = "content://media/external/audio/albumart/13"; 
//圖片來源於assets 
String assetsUrl = Scheme.ASSETS.wrap("image.png"); 
//圖片來源於 
String drawableUrl = Scheme.DRAWABLE.wrap("R.drawable.image"); 

GirdView,ListView載入圖片

相信大部分人都是使用GridView,ListView來顯示大量的圖片,而當我們快速滑動GridView,ListView,我們希望能停止圖片的載入,而在GridView,ListView停止滑動的時候載入當前介面的圖片,這個框架當然也提供這個功能,使用起來也很簡單,它提供了PauseOnScrollListener這個類來控制ListView,GridView滑動過程中停止去載入圖片,該類使用的是代理模式
[java] view plain copy 在CODE上檢視程式碼片派生到我的程式碼片
listView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling)); 
        gridView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling)); 
第一個引數就是我們的圖片載入物件ImageLoader, 第二個是控制是否在滑動過程中暫停載入圖片,如果需要暫停傳true就行了,第三個引數控制猛的滑動介面的時候圖片是否載入

OutOfMemoryError

雖然這個框架有很好的快取機制,有效的避免了OOM的產生,一般的情況下產生OOM的概率比較小,但是並不能保證OutOfMemoryError永遠不發生,這個框架對於OutOfMemoryError做了簡單的catch,保證我們的程式遇到OOM而不被crash掉,但是如果我們使用該框架經常發生OOM,我們應該怎麼去改善呢?
減少執行緒池中執行緒的個數,在ImageLoaderConfiguration中的(.threadPoolSize)中配置,推薦配置1-5
在DisplayImageOptions選項中配置bitmapConfig為Bitmap.Config.RGB_565,因為預設是ARGB_8888, 使用RGB_565會比使用ARGB_8888少消耗2倍的記憶體
在ImageLoaderConfiguration中配置圖片的記憶體快取為memoryCache(new WeakMemoryCache()) 或者不使用記憶體快取
在DisplayImageOptions選項中設定.imageScaleType(ImageScaleType.IN_SAMPLE_INT)或者imageScaleType(ImageScaleType.EXACTLY)
通過上面這些,相信大家對Universal-Image-Loader框架的使用已經非常的瞭解了,我們在使用該框架的時候儘量的使用displayImage()方法去載入圖片,loadImage()是將圖片物件回撥到ImageLoadingListener介面的onLoadingComplete()方法中,需要我們手動去設定到ImageView上面,displayImage()方法中,對ImageView物件使用的是Weak references,方便垃圾回收器回收ImageView物件,如果我們要載入固定大小的圖片的時候,使用loadImage()方法需要傳遞一個ImageSize物件,而displayImage()方法會根據ImageView物件的測量值,或者android:layout_width and android:layout_height設定的值,或者android:maxWidth and/or android:maxHeight設定的值來裁剪圖片。

記憶體快取

首先我們來了解下什麼是強引用和什麼是弱引用?
強引用是指建立一個物件並把這個物件賦給一個引用變數, 強引用有引用變數指向時永遠不會被垃圾回收。即使記憶體不足的時候寧願報OOM也不被垃圾回收器回收,我們new的物件都是強引用
弱引用通過weakReference類來實現,它具有很強的不確定性,如果垃圾回收器掃描到有著WeakReference的物件,就會將其回收釋放記憶體

現在我們來看Universal-Image-Loader有哪些記憶體快取策略
1. 只使用的是強引用快取
LruMemoryCache(這個類就是這個開源框架預設的記憶體快取類,快取的是bitmap的強引用,下面我會從原始碼上面分析這個類)
2.使用強引用和弱引用相結合的快取有
UsingFreqLimitedMemoryCache(如果快取的圖片總量超過限定值,先刪除使用頻率最小的bitmap)
LRULimitedMemoryCache(這個也是使用的lru演算法,和LruMemoryCache不同的是,他快取的是bitmap的弱引用)
FIFOLimitedMemoryCache(先進先出的快取策略,當超過設定值,先刪除最先加入快取的bitmap)
LargestLimitedMemoryCache(當超過快取限定值,先刪除最大的bitmap物件)
LimitedAgeMemoryCache(當 bitmap加入快取中的時間超過我們設定的值,將其刪除)
3.只使用弱引用快取
WeakMemoryCache(這個類快取bitmap的總大小沒有限制,唯一不足的地方就是不穩定,快取的圖片容易被回收掉)
上面介紹了Universal-Image-Loader所提供的所有的記憶體快取的類,當然我們也可以使用我們自己寫的記憶體快取類,我們還要看看要怎麼將這些記憶體快取加入到我們的專案中,我們只需要配置ImageLoaderConfiguration.memoryCache(…),如下


ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this) 
.memoryCache(new WeakMemoryCache()) 
.build(); 

下面我們來分析LruMemoryCache這個類的原始碼


package com.nostra13.universalimageloader.cache.memory.impl; 
import android.graphics.Bitmap; 
import com.nostra13.universalimageloader.cache.memory.MemoryCacheAware; 
import java.util.Collection; 
import java.util.HashSet; 
import java.util.LinkedHashMap; 
import java.util.Map; 
/** 
* A cache that holds strong references to a limited number of Bitmaps. Each time a Bitmap is accessed, it is moved to 
* the head of a queue. When a Bitmap is added to a full cache, the Bitmap at the end of that queue is evicted and may 
* become eligible for garbage collection.<br /> 
* <br /> 
* <b>NOTE:</b> This cache uses only strong references for stored Bitmaps. 
* 
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com) 
* @since 1.8.1 
*/ 
public class LruMemoryCache implements MemoryCacheAware<String, Bitmap> { 
private final LinkedHashMap<String, Bitmap> map; 
private final int maxSize; 
/** Size of this cache in bytes */ 
private int size; 
/** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */ 
public LruMemoryCache(int maxSize) { 
if (maxSize <= 0) { 
throw new IllegalArgumentException("maxSize <= 0"); 
} 
this.maxSize = maxSize; 
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true); 
} 
/** 
* 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); 
} 
} 
/** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */ 
@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); 
Bitmap previous = map.put(key, value); 
if (previous != null) { 
size -= sizeOf(key, previous); 
} 
} 
trimToSize(maxSize); 
return true; 
} 
/** 
* Remove the eldest entries until the total of remaining entries is at or below the requested size. 
* 
* @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements. 
*/ 
private void trimToSize(int maxSize) { 
while (true) { 
String key; 
Bitmap value; 
synchronized (this) { 
if (size < 0 || (map.isEmpty() && size != 0)) { 
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); 
} 
} 
} 
/** Removes the entry for {@code key} if it exists. */ 
@Override 
public final void remove(String key) { 
if (key == null) { 
throw new NullPointerException("key == null"); 
} 
synchronized (this) { 
Bitmap previous = map.remove(key); 
if (previous != null) { 
size -= sizeOf(key, previous); 
} 
} 
} 
@Override 
public Collection<String> keys() { 
synchronized (this) { 
return new HashSet<String>(map.keySet()); 
} 
} 
@Override 
public void clear() { 
trimToSize(-1); // -1 will evict 0-sized elements 
} 
/** 
* Returns the size {@code Bitmap} in bytes. 
* <p/> 
* An entry's size must not change while it is in the cache. 
*/ 
private int sizeOf(String key, Bitmap value) { 
return value.getRowBytes() * value.getHeight(); 
} 
@Override 
public synchronized final String toString() { 
return String.format("LruCache[maxSize=%d]", maxSize); 
} 
} 

我們可以看到這個類中維護的是一個LinkedHashMap,在LruMemoryCache建構函式中我們可以看到,我們為其設定了一個快取圖片的最大值maxSize,並例項化LinkedHashMap, 而從LinkedHashMap建構函式的第三個引數為ture,表示它是按照訪問順序進行排序的,
我們來看將bitmap加入到LruMemoryCache的方法put(String key, Bitmap value),  第61行,sizeOf()是計算每張圖片所佔的byte數,size是記錄當前快取bitmap的總大小,如果該key之前就快取了bitmap,我們需要將之前的bitmap減掉去,接下來看trimToSize()方法,我們直接看86行,如果當前快取的bitmap總數小於設定值maxSize,不做任何處理,如果當前快取的bitmap總數大於maxSize,刪除LinkedHashMap中的第一個元素,size中減去該bitmap對應的byte數
我們可以看到該快取類比較簡單,邏輯也比較清晰,如果大家想知道其他記憶體快取的邏輯,可以去分析分析其原始碼,在這裡我簡單說下FIFOLimitedMemoryCache的實現邏輯,該類使用的HashMap來快取bitmap的弱引用,然後使用LinkedList來儲存成功加入到FIFOLimitedMemoryCache的bitmap的強引用,如果加入的FIFOLimitedMemoryCache的bitmap總數超過限定值,直接刪除LinkedList的第一個元素,所以就實現了先進先出的快取策略,其他的快取都類似,有興趣的可以去看看。

硬碟快取

接下來就給大家分析分析硬碟快取的策略,這個框架也提供了幾種常見的快取策略,當然如果你覺得都不符合你的要求,你也可以自己去擴充套件

FileCountLimitedDiscCache(可以設定快取圖片的個數,當超過設定值,刪除掉最先加入到硬碟的檔案)
LimitedAgeDiscCache(設定檔案存活的最長時間,當超過這個值,就刪除該檔案)
TotalSizeLimitedDiscCache(設定快取bitmap的最大值,當超過這個值,刪除最先加入到硬碟的檔案)
UnlimitedDiscCache(這個快取類沒有任何的限制)

下面我們就來分析分析TotalSizeLimitedDiscCache的原始碼實現


/******************************************************************************* 
* Copyright 2011-2013 Sergey Tarasevich 
* 
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0 
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed under the License is distributed on an "AS IS" BASIS, 
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License. 
*******************************************************************************/ 
package com.nostra13.universalimageloader.cache.disc.impl; 
import com.nostra13.universalimageloader.cache.disc.LimitedDiscCache; 
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; 
import com.nostra13.universalimageloader.core.DefaultConfigurationFactory; 
import com.nostra13.universalimageloader.utils.L; 
import java.io.File; 
/** 
* Disc cache limited by total cache size. If cache size exceeds specified limit then file with the most oldest last 
* usage date will be deleted. 
* 
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com) 
* @see LimitedDiscCache 
* @since 1.0.0 
*/ 
public class TotalSizeLimitedDiscCache extends LimitedDiscCache { 
private static final int MIN_NORMAL_CACHE_SIZE_IN_MB = 2; 
private static final int MIN_NORMAL_CACHE_SIZE = MIN_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024; 
/** 
* @param cacheDir  Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's 
*      needed for right cache limit work. 
* @param maxCacheSize Maximum cache directory size (in bytes). If cache size exceeds this limit then file with the 
*      most oldest last usage date will be deleted. 
*/ 
public TotalSizeLimitedDiscCache(File cacheDir, int maxCacheSize) { 
this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), maxCacheSize); 
} 
/** 
* @param cacheDir   Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's 
*       needed for right cache limit work. 
* @param fileNameGenerator Name generator for cached files 
* @param maxCacheSize  Maximum cache directory size (in bytes). If cache size exceeds this limit then file with the 
*       most oldest last usage date will be deleted. 
*/ 
public TotalSizeLimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int maxCacheSize) { 
super(cacheDir, fileNameGenerator, maxCacheSize); 
if (maxCacheSize < MIN_NORMAL_CACHE_SIZE) { 
L.w("You set too small disc cache size (less than %1$d Mb)", MIN_NORMAL_CACHE_SIZE_IN_MB); 
} 
} 
@Override 
protected int getSize(File file) { 
return (int) file.length(); 
} 
} 

這個類是繼承LimitedDiscCache,除了兩個建構函式之外,還重寫了getSize()方法,返回檔案的大小,接下來我們就來看看LimitedDiscCache


/******************************************************************************* 
* Copyright 2011-2013 Sergey Tarasevich 
* 
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0 
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed under the License is distributed on an "AS IS" BASIS, 
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License. 
*******************************************************************************/ 
package com.nostra13.universalimageloader.cache.disc; 
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; 
import com.nostra13.universalimageloader.core.DefaultConfigurationFactory; 
import java.io.File; 
import java.util.Collections; 
import java.util.HashMap; 
import java.util.Map; 
import java.util.Map.Entry; 
import java.util.Set; 
import java.util.concurrent.atomic.AtomicInteger; 
/** 
* Abstract disc cache limited by some parameter. If cache exceeds specified limit then file with the most oldest last 
* usage date will be deleted. 
* 
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com) 
* @see BaseDiscCache 
* @see FileNameGenerator 
* @since 1.0.0 
*/ 
public abstract class LimitedDiscCache extends BaseDiscCache { 
private static final int INVALID_SIZE = -1; 
//記錄快取檔案的大小 
private final AtomicInteger cacheSize; 
//快取檔案的最大值 
private final int sizeLimit; 
private final Map<File, Long> lastUsageDates = Collections.synchronizedMap(new HashMap<File, Long>()); 
/** 
* @param cacheDir Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's 
*     needed for right cache limit work. 
* @param sizeLimit Cache limit value. If cache exceeds this limit then file with the most oldest last usage date 
*     will be deleted. 
*/ 
public LimitedDiscCache(File cacheDir, int sizeLimit) { 
this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), sizeLimit); 
} 
/** 
* @param cacheDir   Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's 
*       needed for right cache limit work. 
* @param fileNameGenerator Name generator for cached files 
* @param sizeLimit   Cache limit value. If cache exceeds this limit then file with the most oldest last usage date 
*       will be deleted. 
*/ 
public LimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int sizeLimit) { 
super(cacheDir, fileNameGenerator); 
this.sizeLimit = sizeLimit; 
cacheSize = new AtomicInteger(); 
calculateCacheSizeAndFillUsageMap(); 
} 
/** 
* 另開執行緒計算cacheDir裡面檔案的大小,並將檔案和最後修改的毫秒數加入到Map中 
*/ 
private void calculateCacheSizeAndFillUsageMap() { 
new Thread(new Runnable() { 
@Override 
public void run() { 
int size = 0; 
File[] cachedFiles = cacheDir.listFiles(); 
if (cachedFiles != null) { // rarely but it can happen, don't know why 
for (File cachedFile : cachedFiles) { 
//getSize()是一個抽象方法,子類自行實現getSize()的邏輯 
size  = getSize(cachedFile); 
//將檔案的最後修改時間加入到map中 
lastUsageDates.put(cachedFile, cachedFile.lastModified()); 
} 
cacheSize.set(size); 
} 
} 
}).start(); 
} 
/** 
* 將檔案新增到Map中,並計算快取檔案的大小是否超過了我們設定的最大快取數 
* 超過了就刪除最先加入的那個檔案 
*/ 
@Override 
public void put(String key, File file) { 
//要加入檔案的大小 
int valueSize = getSize(file); 
//獲取當前快取檔案大小總數 
int curCacheSize = cacheSize.get(); 
//判斷是否超過設定的最大快取值 
while (curCacheSize   valueSize > sizeLimit) { 
int freedSize = removeNext(); 
if (freedSize == INVALID_SIZE) break; // cache is empty (have nothing to delete) 
curCacheSize = cacheSize.addAndGet(-freedSize); 
} 
cacheSize.addAndGet(valueSize); 
Long currentTime = System.currentTimeMillis(); 
file.setLastModified(currentTime); 
lastUsageDates.put(file, currentTime); 
} 
/** 
* 根據key生成檔案 
*/ 
@Override 
public File get(String key) { 
File file = super.get(key); 
Long currentTime = System.currentTimeMillis(); 
file.setLastModified(currentTime); 
lastUsageDates.put(file, currentTime); 
return file; 
} 
/** 
* 硬碟快取的清理 
*/ 
@Override 
public void clear() { 
lastUsageDates.clear(); 
cacheSize.set(0); 
super.clear(); 
} 
/** 
* 獲取最早加入的快取檔案,並將其刪除 
*/ 
private int removeNext() { 
if (lastUsageDates.isEmpty()) { 
return INVALID_SIZE; 
} 
Long oldestUsage = null; 
File mostLongUsedFile = null; 
Set<Entry<File, Long>> entries = lastUsageDates.entrySet(); 
synchronized (lastUsageDates) { 
for (Entry<File, Long> entry : entries) { 
if (mostLongUsedFile == null) { 
mostLongUsedFile = entry.getKey(); 
oldestUsage = entry.getValue(); 
} else { 
Long lastValueUsage = entry.getValue(); 
if (lastValueUsage < oldestUsage) { 
oldestUsage = lastValueUsage; 
mostLongUsedFile = entry.getKey(); 
} 
} 
} 
} 
int fileSize = 0; 
if (mostLongUsedFile != null) { 
if (mostLongUsedFile.exists()) { 
fileSize = getSize(mostLongUsedFile); 
if (mostLongUsedFile.delete()) { 
lastUsageDates.remove(mostLongUsedFile); 
} 
} else { 
lastUsageDates.remove(mostLongUsedFile); 
} 
} 
return fileSize; 
} 
/** 
* 抽象方法,獲取檔案大小 
* @param file 
* @return 
*/ 
protected abstract int getSize(File file); 
} 

在構造方法中,第69行有一個方法calculateCacheSizeAndFillUsageMap(),該方法是計算cacheDir的檔案大小,並將檔案和檔案的最後修改時間加入到Map中
然後是將檔案加入硬碟快取的方法put(),在106行判斷當前檔案的快取總數加上即將要加入快取的檔案大小是否超過快取設定值,如果超過了執行removeNext()方法,接下來就來看看這個方法的具體實現,150-167中找出最先加入硬碟的檔案,169-180中將其從檔案硬碟中刪除,並返回該檔案的大小,刪除成功之後成員變數cacheSize需要減掉改檔案大小。
FileCountLimitedDiscCache這個類實現邏輯跟TotalSizeLimitedDiscCache是一樣的,區別在於getSize()方法,前者返回1,表示為檔案數是1,後者返回檔案的大小。
等我寫完了這篇文章,我才發現FileCountLimitedDiscCache和TotalSizeLimitedDiscCache在最新的原始碼中已經刪除了,加入了LruDiscCache,由於我的是之前的原始碼,所以我也不改了,大家如果想要了解LruDiscCache可以去看最新的原始碼,我這裡就不介紹了,還好記憶體快取的沒變化,下面分析的是最新的原始碼中的部分,我們在使用中可以不自行配置硬碟快取策略,直接用DefaultConfigurationFactory中的就行了
我們看DefaultConfigurationFactory這個類的createDiskCache()方法


/** 
* Creates default implementation of {@link DiskCache} depends on incoming parameters 
*/ 
public static DiskCache createDiskCache(Context context, FileNameGenerator diskCacheFileNameGenerator, 
long diskCacheSize, int diskCacheFileCount) { 
File reserveCacheDir = createReserveDiskCacheDir(context); 
if (diskCacheSize > 0 || diskCacheFileCount > 0) { 
File individualCacheDir = StorageUtils.getIndividualCacheDirectory(context); 
LruDiscCache diskCache = new LruDiscCache(individualCacheDir, diskCacheFileNameGenerator, diskCacheSize, 
diskCacheFileCount); 
diskCache.setReserveCacheDir(reserveCacheDir); 
return diskCache; 
} else { 
File cacheDir = StorageUtils.getCacheDirectory(context); 
return new UnlimitedDiscCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator); 
} 
} 

如果我們在ImageLoaderConfiguration中配置了diskCacheSize和diskCacheFileCount,他就使用的是LruDiscCache,否則使用的是UnlimitedDiscCache,在最新的原始碼中還有一個硬碟快取類可以配置,那就是LimitedAgeDiscCache,可以在ImageLoaderConfiguration.diskCache(…)配置。

原始碼解讀


ImageView mImageView = (ImageView) findViewById(R.id.image); 
String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg"; 
//顯示圖片的配置 
DisplayImageOptions options = new DisplayImageOptions.Builder() 
.showImageOnLoading(R.drawable.ic_stub) 
.showImageOnFail(R.drawable.ic_error) 
.cacheInMemory(true) 
.cacheOnDisk(true) 
.bitmapConfig(Bitmap.Config.RGB_565) 
.build(); 

            
        ImageLoader.getInstance().displayImage(imageUrl, mImageView, options);   
大部分的時候我們都是使用上面的程式碼去載入圖片,我們先看下


public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) { 
displayImage(uri, new ImageViewAware(imageView), options, null, null); 
} 

從上面的程式碼中,我們可以看出,它會將ImageView轉換成ImageViewAware, ImageViewAware主要是做什麼的呢?該類主要是將ImageView進行一個包裝,將ImageView的強引用變成弱引用,當記憶體不足的時候,可以更好的回收ImageView物件,還有就是獲取ImageView的寬度和高度。這使得我們可以根據ImageView的寬高去對圖片進行一個裁剪,減少記憶體的使用。
接下來看具體的displayImage方法啦,由於這個方法程式碼量蠻多的,所以這裡我分開來讀


checkConfiguration(); 
if (imageAware == null) { 
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS); 
} 
if (listener == null) { 
listener = emptyListener; 
} 
if (options == null) { 
options = configuration.defaultDisplayImageOptions; 
} 
if (TextUtils.isEmpty(uri)) { 
engine.cancelDisplayTaskFor(imageAware); 
listener.onLoadingStarted(uri, imageAware.getWrappedView()); 
if (options.shouldShowImageForEmptyUri()) { 
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources)); 
} else { 
imageAware.setImageDrawable(null); 
} 
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null); 
return; 
} 

第1行程式碼是檢查ImageLoaderConfiguration是否初始化,這個初始化是在Application中進行的
12-21行主要是針對url為空的時候做的處理,第13行程式碼中,ImageLoaderEngine中存在一個HashMap,用來記錄正在載入的任務,載入圖片的時候會將ImageView的id和圖片的url加上尺寸加入到HashMap中,載入完成之後會將其移除,然後將DisplayImageOptions的imageResForEmptyUri的圖片設定給ImageView,最後回撥給ImageLoadingListener介面告訴它這次任務完成了。


ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize()); 
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize); 
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey); 
listener.onLoadingStarted(uri, imageAware.getWrappedView()); 
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey); 
if (bmp != null && !bmp.isRecycled()) { 
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey); 
if (options.shouldPostProcess()) { 
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, 
options, listener, progressListener, engine.getLockForUri(uri)); 
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo, 
defineHandler(options)); 
if (options.isSyncLoading()) { 
displayTask.run(); 
} else { 
engine.submit(displayTask); 
} 
} else { 
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE); 
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp); 
} 
} 

第1行主要是將ImageView的寬高封裝成ImageSize物件,如果獲取ImageView的寬高為0,就會使用手機螢幕的寬高作為ImageView的寬高,我們在使用ListView,GridView去載入圖片的時候,第一頁獲取寬度是0,所以第一頁使用的手機的螢幕寬高,後面的獲取的都是控制元件本身的大小了
第7行從記憶體快取中獲取Bitmap物件,我們可以再ImageLoaderConfiguration中配置記憶體快取邏輯,預設使用的是LruMemoryCache,這個類我在前面的文章中講過
第11行中有一個判斷,我們如果在DisplayImageOptions中設定了postProcessor就進入true邏輯,不過預設postProcessor是為null的,BitmapProcessor介面主要是對Bitmap進行處理,這個框架並沒有給出相對應的實現,如果我們有自己的需求的時候可以自己實現BitmapProcessor介面(比如將圖片設定成圓形的)
第22 -23行是將Bitmap設定到ImageView上面,這裡我們可以在DisplayImageOptions中配置顯示需求displayer,預設使用的是SimpleBitmapDisplayer,直接將Bitmap設定到ImageView上面,我們可以配置其他的顯示邏輯, 他這裡提供了FadeInBitmapDisplayer(透明度從0-1)RoundedBitmapDisplayer(4個角是圓弧)等, 然後回撥到ImageLoadingListener介面


if (options.shouldShowImageOnLoading()) { 
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources)); 
} else if (options.isResetViewBeforeLoading()) { 
imageAware.setImageDrawable(null); 
} 
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, 
options, listener, progressListener, engine.getLockForUri(uri)); 
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo, 
defineHandler(options)); 
if (options.isSyncLoading()) { 
displayTask.run(); 
} else { 
engine.submit(displayTask); 
} 

這段程式碼主要是Bitmap不在記憶體快取,從檔案中或者網路裡面獲取bitmap物件,例項化一個LoadAndDisplayImageTask物件,LoadAndDisplayImageTask實現了Runnable,如果配置了isSyncLoading為true, 直接執行LoadAndDisplayImageTask的run方法,表示同步,預設是false,將LoadAndDisplayImageTask提交給執行緒池物件
接下來我們就看LoadAndDisplayImageTask的run(), 這個類還是蠻複雜的,我們還是一段一段的分析


if (waitIfPaused()) return; 
if (delayIfNeed()) return; 

如果waitIfPaused(), delayIfNeed()返回true的話,直接從run()方法中返回了,不執行下面的邏輯, 接下來我們先看看


waitIfPaused()
private boolean waitIfPaused() { 
AtomicBoolean pause = engine.getPause(); 
if (pause.get()) { 
synchronized (engine.getPauseLock()) { 
if (pause.get()) { 
L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey); 
try { 
engine.getPauseLock().wait(); 
} catch (InterruptedException e) { 
L.e(LOG_TASK_INTERRUPTED, memoryCacheKey); 
return true; 
} 
L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey); 
} 
} 
} 
return isTaskNotActual(); 
} 

這個方法是幹嘛用呢,主要是我們在使用ListView,GridView去載入圖片的時候,有時候為了滑動更加的流暢,我們會選擇手指在滑動或者猛地一滑動的時候不去載入圖片,所以才提出了這麼一個方法,那麼要怎麼用呢?  這裡用到了PauseOnScrollListener這個類,使用很簡單ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling )), pauseOnScroll控制我們緩慢滑動ListView,GridView是否停止載入圖片,pauseOnFling 控制猛的滑動ListView,GridView是否停止載入圖片
除此之外,這個方法的返回值由isTaskNotActual()決定,我們接著看看isTaskNotActual()的原始碼


private boolean isTaskNotActual() { 
return isViewCollected() || isViewReused(); 
} 

isViewCollected()是判斷我們ImageView是否被垃圾回收器回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了,isViewReused()判斷該ImageView是否被重用,被重用run()方法也直接返回,為什麼要用isViewReused()方法呢?主要是ListView,GridView我們會複用item物件,假如我們先去載入ListView,GridView第一頁的圖片的時候,第一頁圖片還沒有全部載入完我們就快速的滾動,isViewReused()方法就會避免這些不可見的item去載入圖片,而直接載入當前介面的圖片


ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock; 
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey); 
if (loadFromUriLock.isLocked()) { 
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey); 
} 
loadFromUriLock.lock(); 
Bitmap bmp; 
try { 
checkTaskNotActual(); 
bmp = configuration.memoryCache.get(memoryCacheKey); 
if (bmp == null || bmp.isRecycled()) { 
bmp = tryLoadBitmap(); 
if (bmp == null) return; // listener callback already was fired 
checkTaskNotActual(); 
checkTaskInterrupted(); 
if (options.shouldPreProcess()) { 
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey); 
bmp = options.getPreProcessor().process(bmp); 
if (bmp == null) { 
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey); 
} 
} 
if (bmp != null && options.isCacheInMemory()) { 
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey); 
configuration.memoryCache.put(memoryCacheKey, bmp); 
} 
} else { 
loadedFrom = LoadedFrom.MEMORY_CACHE; 
L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey); 
} 
if (bmp != null && options.shouldPostProcess()) { 
L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey); 
bmp = options.getPostProcessor().process(bmp); 
if (bmp == null) { 
L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey); 
} 
} 
checkTaskNotActual(); 
checkTaskInterrupted(); 
} catch (TaskCancelledException e) { 
fireCancelEvent(); 
return; 
} finally { 
loadFromUriLock.unlock(); 
} 

第1行程式碼有一個loadFromUriLock,這個是一個鎖,獲取鎖的方法在ImageLoaderEngine類的getLockForUri()方法中


ReentrantLock getLockForUri(String uri) { 
ReentrantLock lock = uriLocks.get(uri); 
if (lock == null) { 
lock = new ReentrantLock(); 
uriLocks.put(uri, lock); 
} 
return lock; 
} 

從上面可以看出,這個鎖物件與圖片的url是相互對應的,為什麼要這麼做?也行你還有點不理解,不知道大家有沒有考慮過一個場景,假如在一個ListView中,某個item正在獲取圖片的過程中,而此時我們將這個item滾出介面之後又將其滾進來,滾進來之後如果沒有加鎖,該item又會去載入一次圖片,假設在很短的時間內滾動很頻繁,那麼就會出現多次去網路上面請求圖片,所以這裡根據圖片的Url去對應一個ReentrantLock物件,讓具有相同Url的請求就會在第7行等待,等到這次圖片載入完成之後,ReentrantLock就被釋放,剛剛那些相同Url的請求就會繼續執行第7行下面的程式碼
來到第12行,它們會先從記憶體快取中獲取一遍,如果記憶體快取中沒有在去執行下面的邏輯,所以ReentrantLock的作用就是避免這種情況下重複的去從網路上面請求圖片。
第14行的方法tryLoadBitmap(),這個方法確實也有點長,我先告訴大家,這裡面的邏輯是先從檔案快取中獲取有沒有Bitmap物件,如果沒有在去從網路中獲取,然後將bitmap儲存在檔案系統中,我們還是具體分析下


File imageFile = configuration.diskCache.get(uri); 
if (imageFile != null && imageFile.exists()) { 
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey); 
loadedFrom = LoadedFrom.DISC_CACHE; 
checkTaskNotActual(); 
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath())); 
} 

先判斷檔案快取中有沒有該檔案,如果有的話,直接去呼叫decodeImage()方法去解碼圖片,該方法裡面呼叫BaseImageDecoder類的decode()方法,根據ImageView的寬高,ScaleType去裁剪圖片,具體的程式碼我就不介紹了,大家自己去看看,我們接下往下看tryLoadBitmap()方法


if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { 
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey); 
loadedFrom = LoadedFrom.NETWORK; 
String imageUriForDecoding = uri; 
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) { 
imageFile = configuration.diskCache.get(uri); 
if (imageFile != null) { 
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath()); 
} 
} 
checkTaskNotActual(); 
bitmap = decodeImage(imageUriForDecoding); 
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { 
fireFailEvent(FailType.DECODING_ERROR, null); 
} 
} 

第1行表示從檔案快取中獲取的Bitmap為null,或者寬高為0,就去網路上面獲取Bitmap,來到第6行程式碼是否配置了DisplayImageOptions的isCacheOnDisk,表示是否需要將Bitmap物件儲存在檔案系統中,一般我們需要配置為true, 預設是false這個要注意下,然後就是執行tryCacheImageOnDisk()方法,去伺服器上面拉取圖片並儲存在本地檔案中


private Bitmap decodeImage(String imageUri) throws IOException { 
ViewScaleType viewScaleType = imageAware.getScaleType(); 
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType, 
getDownloader(), options); 
return decoder.decode(decodingInfo); 
} 
/** @return <b>true</b> - if image was downloaded successfully; <b>false</b> - otherwise */ 
private boolean tryCacheImageOnDisk() throws TaskCancelledException { 
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey); 
boolean loaded; 
try { 
loaded = downloadImage(); 
if (loaded) { 
int width = configuration.maxImageWidthForDiskCache; 
int height = configuration.maxImageHeightForDiskCache; 
if (width > 0 || height > 0) { 
L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey); 
resizeAndSaveImage(width, height); // TODO : process boolean result 
} 
} 
} catch (IOException e) { 
L.e(e); 
loaded = false; 
} 
return loaded; 
} 
private boolean downloadImage() throws IOException { 
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader()); 
return configuration.diskCache.save(uri, is, this); 
} 

第6行的downloadImage()方法是負責下載圖片,並將其保持到檔案快取中,將下載儲存Bitmap的進度回撥到IoUtils.CopyListener介面的onBytesCopied(int current, int total)方法中,所以我們可以設定ImageLoadingProgressListener介面來獲取圖片下載儲存的進度,這裡儲存在檔案系統中的圖片是原圖
第16-17行,獲取ImageLoaderConfiguration是否設定儲存在檔案系統中的圖片大小,如果設定了maxImageWidthForDiskCache和maxImageHeightForDiskCache,會呼叫resizeAndSaveImage()方法對圖片進行裁剪然後在替換之前的原圖,儲存裁剪後的圖片到檔案系統的,之前有同學問過我說這個框架儲存在檔案系統的圖片都是原圖,怎麼才能儲存縮圖,只要在Application中例項化ImageLoaderConfiguration的時候設定maxImageWidthForDiskCache和maxImageHeightForDiskCache就行了


if (bmp == null) return; // listener callback already was fired 
checkTaskNotActual(); 
checkTaskInterrupted(); 
if (options.shouldPreProcess()) { 
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey); 
bmp = options.getPreProcessor().process(bmp); 
if (bmp == null) { 
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey); 
} 
} 
if (bmp != null && options.isCacheInMemory()) { 
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey); 
configuration.memoryCache.put(memoryCacheKey, bmp); 
} 

接下來這裡就簡單了,6-12行是否要對Bitmap進行處理,這個需要自行實現,14-17就是將圖片儲存到記憶體快取中去


DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom); 
runTask(displayBitmapTask, syncLoading, handler, engine); 

最後這兩行程式碼就是一個顯示任務,直接看DisplayBitmapTask類的run()方法


@Override 
public void run() { 
if (imageAware.isCollected()) { 
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey); 
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView()); 
} else if (isViewWasReused()) { 
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey); 
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView()); 
} else { 
L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey); 
displayer.display(bitmap, imageAware, loadedFrom); 
engine.cancelDisplayTaskFor(imageAware); 
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap); 
} 
} 

假如ImageView被回收了或者被重用了,回撥給ImageLoadingListener介面,否則就呼叫BitmapDisplayer去顯示Bitmap
文章寫到這裡就已經寫完了,不知道大家對這個開源框架有沒有進一步的理解,這個開源框架設計也很靈活,用了很多的設計模式,比如建造者模式,裝飾模式,代理模式,策略模式等等,這樣方便我們去擴充套件,實現我們想要的功能。

您可能感興趣的文章:

Android App中實現圖片非同步載入的例項分享Android實現圖片非同步載入並快取到本地Android實現圖片非同步載入及本地快取在Android的應用中實現網路圖片非同步載入的方法Android圖片非同步載入框架Android-Universal-Image-Loader

相關文章

Android 開發 最新文章