這是一個在github上很流行的異步圖檔加載架構,簡單友善實用,主要采用二級緩存來加載緩存圖檔(不僅僅是網上的,本地也可以),圖檔加載方式如下。

每次顯示圖檔時,先讀取緩存,當緩存沒有時,則讀取本地資料,隻有以上兩者都沒有的時候才從網絡擷取。
圖檔的二級緩存本質也是使用DiskLruCache進行讀取,這個東西可以看看郭大神的介紹http://blog.csdn.net/guolin_blog/article/details/28863651,下面來看一下如何使用它。
使用
隻需兩步,就能輕松實作圖檔的加載,
- 在Application設定全局參數。
public class UILApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initImageLoader(getApplicationContext());
}
public static void initImageLoader(Context context) {
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
config.threadPriority(Thread.NORM_PRIORITY - )//設定線程的優先權,0-10
.denyCacheImageMultipleSizesInMemory()//設定之後緩存同一URI不同尺寸的圖檔時,隻緩存最後加載的那一張
.diskCacheFileNameGenerator(new Md5FileNameGenerator())//圖檔的命名方式
.diskCacheSize( * * ) // 磁盤可以儲存多大的圖檔
.tasksProcessingOrder(QueueProcessingType.LIFO)//設定圖檔處理的順序為後進先出,預設為FIFO
.writeDebugLogs(); // 輸出log
//設定完ImageLoaderConfiguration之後,調用Imageloader的init()方法初始化
ImageLoader.getInstance().init(config.build());
}
}
通過建造者模式設定ImageLoaderConfiguration的屬性,然後調用ImageLoader初始化。
-
顯示圖檔
接下來,隻需要在調用displayImage方法,來顯示圖檔
//listener 加載周期的監聽
//progressListener 加載進度監聽
ImageLoader.getInstance().displayImage(String uri, ImageView imageView, DisplayImageOptions options,ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
注意,上面有一個參數DisplayImageOptions ,顧名思義是顯示這個圖檔的參數設定,我們需要自定義它
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub)//加載圖檔時要顯示的圖檔
.showImageForEmptyUri(R.drawable.ic_empty)//空的url時顯示的圖檔
.showImageOnFail(R.drawable.ic_error)//加載失敗時顯示的圖檔
.cacheInMemory(true)//是否緩存
.cacheOnDisk(true)//是否存到磁盤
.considerExifParams(true)//是否考慮exif格式
.displayer(new RoundedBitmapDisplayer())//圖檔展示的形狀
隻需要簡單的設定以上兩步,就能實作高效的圖檔加載了。點我,進入官方位址供下載下傳demo
源碼實作
整個使用過程中除了displayImage,其他的都是參數配置。現在來解析displayImage究竟怎麼實作的。
public synchronized void init(ImageLoaderConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
}
if (this.configuration == null) {
L.d(LOG_INIT_CONFIG);
engine = new ImageLoaderEngine(configuration);
this.configuration = configuration;
} else {
L.w(WARNING_RE_INIT_CONFIG);
}
}
在Application 的init初始化中.也就是指派了全局變量configuration ,和new了一個ImageLoaderEngine。
ImageLoaderEngine也就是一個線程池類,還記得在ImageLoaderConfiguration 中有taskExecutor(Executor executor) 和taskExecutorForCachedImages(Executor executorForCachedImages)方法嗎,該類就在這裡操作這兩個線程池,我們一般不定義那兩個方法,使用預設方法構造它們。
//一般使用這個方法,将imageView包裝成ImageViewAware
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
displayImage(uri, new ImageViewAware(imageView), options, listener, progressListener);
}
//最終實作的方法
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
//檢查ImageLoaderConfiguration 是否為空
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
if (TextUtils.isEmpty(uri)) {
//清除map中key為imageAware的hashcode。它和緩存圖檔的唯一值組成鍵值對
engine.cancelDisplayTaskFor(imageAware);
//回調
listener.onLoadingStarted(uri, imageAware.getWrappedView());
//shouldShowImageForEmptyUri在DisplayImageOptions 設定
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
}
//一般沒有設定目标圖檔大小,故設定為目前imageview的寬高
if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
//根據url和目标圖檔的寬高組成唯一值
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
//儲存到map
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);
//shouldPostProcess()由DisplayImageOptions 設定,自定義一個圖檔處理器,用來
//下載下傳和解析圖檔。一般使用系統預設的,他會下載下傳并将圖檔截到<=imageview長寬。
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 {
//一般沒自定義的情況都執行這裡,緩存有,是因為圖檔經過壓縮處理才加載到緩存的,是以不需
//要解析。直接顯示
//調用DisplayBitmapTask 的run方法顯示圖檔,
//本質也是調用DisplayImageOptions 設定的display(BitmapDisplay display)顯示圖檔
//架構為BitmapDisplay 接口實作了多個顯示形狀類
//位于core/display包
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//回調
}
} else {//接下來是緩存沒有的情況下,
if (options.shouldShowImageOnLoading()) {//DisplayImageOptions 設定,加載時顯示圖檔
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {//DisplayImageOptions設定,加載前置為空
imageAware.setImageDrawable(null);
}
//調用LoadAndDisplayImageTask run()方法,執行圖檔加載和展示。
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);
}
}
}
由以上方法可知道,先讀取緩存,緩存有直接顯示,沒有的話到LoadAndDisplayImageTask 執行下一步。
接下來,就來看看LoadAndDisplayImageTask 的run方法,看看是怎麼執行的。
public void run() {
//架構為每個url都配備了一個ReentrantLock ,防止同一時間加載同個url多次
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
loadFromUriLock.lock();
Bitmap bmp;
try {
checkTaskNotActual();//檢查imageview是否被回收
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
//緩存不存在,加載圖檔
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();
checkTaskInterrupted();
//注意和shouldPostProcess不同,DisplayImageOptions設定
if (options.shouldPreProcess()) {
bmp = options.getPreProcessor().process(bmp);
}
//是否要儲存到緩存。取決于DisplayImageOptions 設定,
if (bmp != null && options.isCacheInMemory()) {
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {//緩存有的話,修改個标示而已
loadedFrom = LoadedFrom.MEMORY_CACHE;
}
//DisplayImageOptions設定
if (bmp != null && options.shouldPostProcess()) {
bmp = options.getPostProcessor().process(bmp);
}
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
}
//設定要顯示的圖檔資訊
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
//調用DisplayBitmapTask 的run方法顯示圖檔,
//本質也是調用DisplayImageOptions 設定的display(BitmapDisplay display)顯示圖檔
//架構為BitmapDisplay 接口實作了多個顯示形狀類
//位于core/display包
runTask(displayBitmapTask, syncLoading, handler, engine);
}
上面的實作大體上和display()方法差不多,也是拿到圖檔并且顯示,位于的不同是那圖檔的方法tryLoadBitmap(),看一下是怎麼實作的。
//記憶體沒有圖檔的時候才會調用該方法的。
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
//是以一開始就是去磁盤拿
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists() && imageFile.length() > ) {
//草,終于發現,修改标示,給圖檔url加個file://字首,代表本地檔案
//突然發現,拿到圖檔像素很大,加載的時候記憶體嘩啦啦的掉
//不行,加載前得縮小它
//decodeImage有加載圖檔和裁剪圖檔的功效
loadedFrom = LoadedFrom.DISC_CACHE;
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= || bitmap.getHeight() <= ) {
//草你大爺,還是沒有,上網擷取,改個标示
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() <= || bitmap.getHeight() <= ) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
return bitmap;
}
tryLoadBitmap也就是根據url擷取到bitmap的執行個體,想知道怎麼擷取的,就看看decodeImage是怎麼寫的,媽的跳來跳去煩死了
private Bitmap decodeImage(String imageUri) throws IOException {
//擷取圖檔縮放方式,這裡他把所有縮放方法都歸為兩類CENTER_INSIDE和CROP
//個人覺得是1等比例縮放到最長邊等于imageview的一邊
//2等比例縮放到最短邊等于imageview的一邊,導緻會裁剪到最長邊的一部分
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
//然後調用架構的解析類,
//這裡隻有一個解析類位于包core/decode/BaseImageDecoder.java
//ImageLoaderConfiguration會預設設定的
return decoder.decode(decodingInfo);
}
現在,是以的矛頭都指向了BaseImageDecoder(預設)類,草蛋的搞完就收工了。
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
//根據url擷取輸入流,也就是調用core/download/BaseImageDownloader.java
//這個下載下傳類的getStream方法來實作下載下傳,可以去該類看看
InputStream imageStream = getImageStream(decodingInfo);
if (imageStream == null) {
return null;
}
try {
//下面這段代碼想表達的意思,設定BitmapFactory.Option的inSampleSize
//減少加載時記憶體的消耗,
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
}
if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
//在這裡設定圖檔的旋轉和縮放,inSampleSize不一定縮放到滿意的大小
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
在這裡需要将一下prepareDecodingOptions方法
protected Options prepareDecodingOptions(ImageSize imageSize, ImageDecodingInfo decodingInfo) {
//這個ImageScaleType 在DisplayImageOptions裡面設定。
//預設是ImageScaleType.IN_SAMPLE_POWER_OF_2這個參數的含義是什麼,看下面
ImageScaleType scaleType = decodingInfo.getImageScaleType();
int scale;
if (scaleType == ImageScaleType.NONE) {
scale = ;
} else if (scaleType == ImageScaleType.NONE_SAFE) {
scale = ImageSizeUtils.computeMinImageSampleSize(imageSize);
} else {
ImageSize targetSize = decodingInfo.getTargetSize();
//IN_SAMPLE_POWER_OF_2的話powerOf2 設為true
boolean powerOf2 = scaleType == ImageScaleType.IN_SAMPLE_POWER_OF_2;
//關于inSampleSize 的計算都在下面的方法中,咬咬牙進去看一下
scale = ImageSizeUtils.computeImageSampleSize(imageSize, targetSize, decodingInfo.getViewScaleType(), powerOf2);
}
Options decodingOptions = decodingInfo.getDecodingOptions();
decodingOptions.inSampleSize = scale;
return decodingOptions;
}
計算BitmapFactory.Option inSampleSize 的時候有原圖檔長寬已經目标圖檔長寬,目标圖檔時根封裝ImageView的ImageViewAmare計算出來的,通過這些資訊,計算出符合的圖檔長寬
public static int computeImageSampleSize(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
boolean powerOf2Scale) {
final int srcWidth = srcSize.getWidth();
final int srcHeight = srcSize.getHeight();
final int targetWidth = targetSize.getWidth();
final int targetHeight = targetSize.getHeight();
int scale = ;
//這個viewScaleType是在ImageViewAware中定義,他将圖檔的scaletype隻是分成兩類
switch (viewScaleType) {
//第一類,等比例縮放圖檔長寬最大值小于等于imageview長寬,這樣圖檔會全部顯示,
//imageview不一定被全部占用
case FIT_INSIDE:
//如果設定ImageScaleType.IN_SAMPLE_POWER_OF_2的話,inSampleSize 縮放值為2的平方數
//以5*10的imageview顯示100*100的圖檔為例,如果設定了,scale 為16,沒有設定的話,scale 為20
if (powerOf2Scale) {
final int halfWidth = srcWidth / ;
final int halfHeight = srcHeight / ;
while ((halfWidth / scale) > targetWidth || (halfHeight / scale) > targetHeight) {
scale *= ;
}
} else {
scale = Math.max(srcWidth / targetWidth, srcHeight / targetHeight); // max
}
break;
//第二類,等比例縮放圖檔長寬最小值小于等于imageview長寬,這樣imageview會全部占用,
//圖檔可能會被裁剪部分,預設使用這個參數
case CROP:
//以5*10的imageview顯示100*100的圖檔為例,如果設定了,scale 為8,沒有設定的話,scale 為10
if (powerOf2Scale) {
final int halfWidth = srcWidth / ;
final int halfHeight = srcHeight / ;
while ((halfWidth / scale) > targetWidth && (halfHeight / scale) > targetHeight) {
scale *= ;
}
} else {
scale = Math.min(srcWidth / targetWidth, srcHeight / targetHeight); // min
}
break;
}
if (scale < ) {
scale = ;
}
//根據最大允許長寬值在設定一下scale
scale = considerMaxTextureSize(srcWidth, srcHeight, scale, powerOf2Scale);
return scale;
}
通過計算得出inSampleSize ,就能大幅度減少圖檔加載消耗的記憶體。
注意有ViewScaleType 和 ImageScaleType ,ViewScaleType 是在DisplayImageOptions裡面設定。
用來決定decodingOptions.inSampleSize的計算方法,而ImageScaleType 是在ImageViewAware中定義,用來決定scaleType縮放類型,隻有等比例縮放和等比例裁剪.
圖檔的大緻加載流程就這樣結束了,當然還有很多小細節沒有提到,但我已疲軟,隻能如此了。
總結
-
記憶體緩存
雖然是自己寫的,和LruCache很像,也是使用LinkedHashMap來實作Lru緩存,系統預設使用,如想使用其他緩存方式,可在cache/memory/包檢視
-
磁盤緩存
也就是使用DiskLruCache,也是使用LinkedHashMap來實作Lru緩存,系統預設使用,如想使用其他緩存方式,可在cache/memory/包檢視
- DisplayImageOption方法介紹
showImageOnLoading(int/drawable) 加載的時候顯示的圖檔
showImageForEmptyUri(int/drawable) url為空時顯示的圖檔
showImageOnFail(int/drawable) 加載失敗顯示的圖檔
resetViewBeforeLoading(boolean) 預設為false,當沒有設定showImageOnLoading的時候生效,加載圖檔中的時候imageview圖檔是否設為空
cacheInMemory(boolean) 預設為false 是否緩存到記憶體
cacheOnDisk(boolean cacheOnDisk) 預設為false 是否緩存到磁盤
imageScaleType(ImageScaleType) 設定scaletype,與圖檔的inSampleSize由關系, 預設為 ImageScaleType.IN_SAMPLE_POWER_OF_2,将inSampleSiz設定為2的平方數,一般預設值
bitmapConfig(Bitmap.Config) 圖檔config預設Bitmap.Config#ARGB_8888
decodingOptions(Options ) 設定圖檔Option,一般使用imageScaleType配置來動态設定。
delayBeforeLoading(int ) 加載延時
extraForDownloader(Object ) 在ImageDownloader.getStream調用這個參數,但沒使用到它,不知何意
considerExifParams(boolean ) 是否設定Exif格式,關于該格式可以百度之
preProcessor(BitmapProcessor) 自定義個圖檔處理Process,在LoadAndDisplayImageTask 加載完圖檔之後(下載下傳+設定大小)會調用該方法,修改完之後放到記憶體中
postProcessor(BitmapProcessor ) 同上,不同的是記憶體中已經存在,隻是拿出來臨時修改
displayer(BitmapDisplayer ) 設定imageview顯示形狀 ,在core/display/包可看類型,預設正常形狀加載
syncLoading(boolean ) 加載的時候是否同步,預設異步加載
handler(Handler ) 自定義一個handle來顯示圖檔,預設不需要
- ImageLoaderConfiguration介紹
memoryCacheExtraOptions(int , int ) 設定記憶體緩存的最大長寬度,預設不處理
diskCacheExtraOptions(int , int ,BitmapProcessor ) 設定磁盤緩存的最大長寬度,預設不處理
taskExecutor(Executor ) 定義線程池存儲圖檔加載線程,預設不處理,取預設值
taskExecutorForCachedImages(Executor ) 定義線程池存儲圖檔加載線程,預設不處理,取預設值
threadPoolSize(int ) 線程池的線程數量,預設為DEFAULT_THREAD_POOL_SIZE = 3
threadPriority(int ) 線程權限,預設為 DEFAULT_THREAD_PRIORITY = Thread.NORM_PRIORITY - 2
denyCacheImageMultipleSizesInMemory()同一url圖檔有不同尺寸時,隻緩存最後一個,預設為全部緩存
tasksProcessingOrder(QueueProcessingType )任務處理順序,有先進先出和後進先出
memoryCacheSize(int ) 記憶體緩存大小。預設為目前app可用記憶體的1/8
Builder memoryCacheSizePercentage(int )記憶體緩存大小占比,1-100
memoryCache(MemoryCache )記憶體緩存模式,預設為LruCache
diskCacheFileCount(int ) 磁盤緩存檔案數量上限
diskCacheFileNameGenerator(FileNameGenerator )檔案名生成器,預設為hashcode 生成
imageDownloader(ImageDownloader ) 圖檔下載下傳器,預設使用預設值BaseImageDownLoader
imageDecoder(ImageDecoder )圖檔解析器,預設使用預設值BaseImageDecoder