Android ImageLoader 框架之圖片快取
在Android ImageLoader框架系列博文中,我們從基本架構到具體實現已經更新了大部分的內容。今天,我們來講最後一個關鍵點,即圖片的快取。為了使用者體驗,通常情況下我們都會將已經下載的圖片快取起來,一般來說記憶體和本地都會有圖片快取。那既然是框架,必然需要有很好的定製性,這讓我們又自然而然的想到了抽象。下面我們就一起來看看快取的實現吧。
快取介面
在Android ImageLoader框架之圖片載入與載入策略我們聊到了Loader,然後闡述了AbsLoader的基本邏輯,其中就有圖片快取。因此AbsLoader中必然含有快取物件的引用。我們看看相關程式碼:
/** * @author mrsimple */ public abstract class AbsLoader implements Loader { /** * 圖片快取 */ private static BitmapCache mCache = SimpleImageLoader.getInstance().getConfig().bitmapCache; // 程式碼省略 }
AbsLoader中定義了一個static的BitmapCache物件,這個就是圖片快取物件。那為什麼是static呢?因為不管Loader有多少個,快取物件都應該是共享的,也就是快取只有一份。說了那麼多,那我們先來了解一下BitmapCache吧。
public interface BitmapCache { public Bitmap get(BitmapRequest key); public void put(BitmapRequest key, Bitmap value); public void remove(BitmapRequest key); }
BitmapCache很簡單,只宣告瞭獲取、新增、移除三個方法來操作圖片快取。這裡有依賴了一個BitmapRequest類,這個類代表了一個圖片載入請求,該類中有該請求對應的ImageView、圖片uri、顯示Config等屬性。在快取這塊我們主要要使用圖片的uri來檢索快取中是否含有該圖片,快取以圖片的uri為key,Bitmap為value來關聯儲存。另外需要BitmapRequest的ImageView寬度和高度,以此來按尺寸載入圖片。
定義BitmapCache介面還是為了可擴充套件性,面向介面的程式設計的理念又再一次的浮現在你面前。如果是你,你會作何設計呢?自己寫程式碼來練習一下吧,看看自己作何考慮,如果實現,這樣你才會從中有更深的領悟。
記憶體快取
既然是框架,那就需要接受使用者各種各樣的需求。但通常來說框架會有一些預設的實現,對於圖片快取來說記憶體快取就其中的一個預設實現,它會將已經載入的圖片快取到記憶體中,大大地提升圖片重複載入的速度。記憶體快取我們的策略是使用LRU演算法,直接使用了support.v4中的LruCache類,相關程式碼如下。
/** * 圖片的記憶體快取,key為圖片的uri,值為圖片本身 * * @author mrsimple */ public class MemoryCache implements BitmapCache { private LruCache<String, Bitmap> mMemeryCache; public MemoryCache() { // 計算可使用的最大記憶體 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 取4分之一的可用記憶體作為快取 final int cacheSize = maxMemory / 4; mMemeryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; } @Override public Bitmap get(BitmapRequest key) { return mMemeryCache.get(key.imageUri); } @Override public void put(BitmapRequest key, Bitmap value) { mMemeryCache.put(key.imageUri, value); } @Override public void remove(BitmapRequest key) { mMemeryCache.remove(key.imageUri); } }
就是簡單的實現了BitmapCache介面,然後內部使用LruCache類實現記憶體快取。比較簡單,就不做說明了。
sd卡快取
對於圖片快取,記憶體快取是不夠的,更多的需要是將圖片快取到sd卡中,這樣使用者在下次進入app時可以直接從本地載入圖片,避免重複地從網路上讀取圖片資料,即耗流量,使用者體驗又不好。sd卡快取我們使用了Jake Wharton的DiskLruCache類,我們的sd卡快取類為DiskCache,程式碼如下 :
public class DiskCache implements BitmapCache { /** * 1MB */ private static final int MB = 1024 * 1024; /** * cache dir */ private static final String IMAGE_DISK_CACHE = "bitmap"; /** * Disk LRU Cache */ private DiskLruCache mDiskLruCache; /** * Disk Cache Instance */ private static DiskCache mDiskCache; /** * @param context */ private DiskCache(Context context) { initDiskCache(context); } public static DiskCache getDiskCache(Context context) { if (mDiskCache == null) { synchronized (DiskCache.class) { if (mDiskCache == null) { mDiskCache = new DiskCache(context); } } } return mDiskCache; } /** * 初始化sdcard快取 */ private void initDiskCache(Context context) { try { File cacheDir = getDiskCacheDir(context, IMAGE_DISK_CACHE); if (!cacheDir.exists()) { cacheDir.mkdirs(); } mDiskLruCache = DiskLruCache .open(cacheDir, getAppVersion(context), 1, 50 * MB); } catch (IOException e) { e.printStackTrace(); } } /** * 獲取sd快取的目錄,如果掛載了sd卡則使用sd卡快取,否則使用應用的快取目錄。 * @param context Context * @param uniqueName 快取目錄名,比如bitmap * @return */ public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { Log.d("", "### context : " + context + ", dir = " + context.getExternalCacheDir()); cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } @Override public synchronized Bitmap get(final BitmapRequest bean) { // 圖片解析器 BitmapDecoder decoder = new BitmapDecoder() { @Override public Bitmap decodeBitmapWithOption(Options options) { final InputStream inputStream = getInputStream(bean.imageUriMd5); Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); IOUtil.closeQuietly(inputStream); return bitmap; } }; return decoder.decodeBitmap(bean.getImageViewWidth(), bean.getImageViewHeight()); } private InputStream getInputStream(String md5) { Snapshot snapshot; try { snapshot = mDiskLruCache.get(md5); if (snapshot != null) { return snapshot.getInputStream(0); } } catch (IOException e) { e.printStackTrace(); } return null; } public void put(BitmapRequest key, Bitmap value) { // 程式碼省略 } public void remove(BitmapRequest key) { // 程式碼省略 } }
程式碼比較簡單,也就是實現BitmapCache,然後包裝一下DiskLruCache類的方法實現圖片檔案的增加、刪除、獲取方法。這裡給大家介紹一個類,是我為了簡化圖片按ImageView尺寸載入的輔助類,即BitmapDecoder。
BitmapDecoder
BitmapDecoder是一個按ImageView尺寸載入圖片的輔助類,一般我載入圖片的過程是這樣的:
1. 建立BitmapFactory.Options options,設定options.inJustDecodeBounds = true,使得只解析圖片尺寸等資訊;
2. 根據ImageView的尺寸來檢查是否需要縮小要載入的圖片以及計算縮放比例;
3. 設定options.inJustDecodeBounds = false,然後按照options設定的縮小比例來載入圖片.
BitmapDecoder類使用decodeBitmap方法封裝了這個過程 ( 模板方法噢 ),使用者只需要實現一個子類,並且覆寫BitmapDecoder的decodeBitmapWithOption實現圖片載入即可完成這個過程(參考DiskCache中的get方法)。程式碼如下 :
/** * 封裝先載入圖片bound,計算出inSmallSize之後再載入圖片的邏輯操作 * * @author mrsimple */ public abstract class BitmapDecoder { /** * @param options * @return */ public abstract Bitmap decodeBitmapWithOption(Options options); /** * @param width 圖片的目標寬度 * @param height 圖片的目標高度 * @return */ public Bitmap decodeBitmap(int width, int height) { // 如果請求原圖,則直接載入原圖 if (width <= 0 || height <= 0) { return decodeBitmapWithOption(null); } // 1、獲取只載入Bitmap寬高等資料的Option, 即設定options.inJustDecodeBounds = true; BitmapFactory.Options options = getJustDecodeBoundsOptions(); // 2、通過options載入bitmap,此時返回的bitmap為空,資料將儲存在options中 decodeBitmapWithOption(options); // 3、計算縮放比例, 並且將options.inJustDecodeBounds設定為false; calculateInSmall(options, width, height); // 4、通過options設定的縮放比例載入圖片 return decodeBitmapWithOption(options); } /** * 獲取BitmapFactory.Options,設定為只解析圖片邊界資訊 */ private Options getJustDecodeBoundsOptions() { // BitmapFactory.Options options = new BitmapFactory.Options(); // 設定為true,表示解析Bitmap物件,該物件不佔記憶體 options.inJustDecodeBounds = true; return options; } protected void calculateInSmall(Options options, int width, int height) { // 設定縮放比例 options.inSampleSize = computeInSmallSize(options, width, height); // 圖片質量 options.inPreferredConfig = Config.RGB_565; // 設定為false,解析Bitmap物件加入到記憶體中 options.inJustDecodeBounds = false; options.inPurgeable = true; options.inInputShareable = true; } private int computeInSmallSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // Calculate ratios of height and width to requested height and // width final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; final float totalPixels = width * height; // Anything more than 2x the requested pixels we'll sample down // further final float totalReqPixelsCap = reqWidth * reqHeight * 2; while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { inSampleSize++; } } return inSampleSize; } }
在decodeBitmap中,我們首先建立BitmapFactory.Options物件,並且設定options.inJustDecodeBounds = true,然後第一次呼叫decodeBitmapWithOption(options),使得只解析圖片尺寸等資訊;然後呼叫calculateInSmall方法,該方法會呼叫computeInSmallSize來根據ImageView的尺寸來檢查是否需要縮小要載入的圖片以及計算縮放比例,在calculateInSmall方法的最後將 options.inJustDecodeBounds = false,使得下次再次decodeBitmapWithOption(options)時會載入圖片;那最後一步必然就是呼叫decodeBitmapWithOption(options)啦,這樣圖片就會按照按照options設定的縮小比例來載入圖片了。
我們使用這個輔助類封裝了這個麻煩、重複的過程,在一定程度上簡化了程式碼,也使得程式碼的可複用性更高,也是模板方法模式的一個較好的示例。
二級快取
有了記憶體和sd卡快取,其實這還不夠。我們的需求很可能就是這個快取會同時有記憶體和sd卡快取,這樣上述兩種快取的優點我們就會具備,這裡我們把它稱為二級快取。看看程式碼吧,也很簡單。
/** * 綜合快取,記憶體和sd卡雙快取 * * @author mrsimple */ public class DoubleCache implements BitmapCache { DiskCache mDiskCache; MemoryCache mMemoryCache = new MemoryCache(); public DoubleCache(Context context) { mDiskCache = DiskCache.getDiskCache(context); } @Override public Bitmap get(BitmapRequest key) { Bitmap value = mMemoryCache.get(key); if (value == null) { value = mDiskCache.get(key); saveBitmapIntoMemory(key, value); } return value; } private void saveBitmapIntoMemory(BitmapRequest key, Bitmap bitmap) { // 如果Value從disk中讀取,那麼存入記憶體快取 if (bitmap != null) { mMemoryCache.put(key, bitmap); } } @Override public void put(BitmapRequest key, Bitmap value) { mDiskCache.put(key, value); mMemoryCache.put(key, value); } @Override public void remove(BitmapRequest key) { mDiskCache.remove(key); mMemoryCache.remove(key); } }
其實就是封裝了記憶體快取和sd卡快取的相關操作嘛~ 那我就不要再費口舌了
自定義快取
快取是有很多實現策略的,既然我們要可擴充套件性,那就要允許使用者注入自己的快取實現。只要你實現BitmapCache,就可以將它通過ImageLoaderConfig注入到ImageLoader內部。
private void initImageLoader() { ImageLoaderConfig config = new ImageLoaderConfig() .setLoadingPlaceholder(R.drawable.loading) .setNotFoundPlaceholder(R.drawable.not_found) .setCache(new MyCache()) // 初始化 SimpleImageLoader.getInstance().init(config); }
MyCache.java
// 自定義快取實現類 public class MyCache implements BitmapCache { // 程式碼 @Override public Bitmap get(BitmapRequest key) { // 你的程式碼 } @Override public void put(BitmapRequest key, Bitmap value) { // 你的程式碼 } @Override public void remove(BitmapRequest key) { // 你的程式碼 } }
Github地址
總結
ImageLoader系列到這裡就算結束了,我們從基本架構、具體實現、設計上面詳細的闡述了一個簡單、可擴充套件性較好的ImageLoader實現過程,希望大家看完這個系列之後能夠自己去實現一遍,這樣你會發現一些具體的問題,領悟能夠更加的深刻。如果你在看這系列部落格的過程中,真的能夠從中體會到物件導向的基本原則、設計思考等東西,而不是說”我擦,我又找到了一個可以copy來用的ImageLoader”,那我就覺得我做的這些分享到達目的了。
相關文章
- Android圖片快取框架GlideAndroid快取框架IDE
- Android ImageLoader框架之圖片載入與載入策略Android框架
- 手把手教你封裝自己的圖片快取工具-ImageLoader封裝快取
- Android 之 遠端圖片獲取和本地快取Android快取
- Android 圖片快取處理Android快取
- 我的圖片四級快取框架快取框架
- Android ImageLoader框架之基本架構Android框架架構
- Android應用開發之(利用好圖片快取)Android快取
- 快取圖片快取
- 圖片快取快取
- Android中的快取策略,實戰打造ImageLoaderAndroid快取
- Android使用LruCache、DiskLruCache實現圖片快取+圖片瀑布流Android快取
- Android技術積累:圖片快取管理Android快取
- 圖片三級快取及OOM--android快取OOMAndroid
- Android中圖片的三層快取詳解Android快取
- android ListView非同步載入圖片(雙快取)AndroidView非同步快取
- SDWebImage實現圖片展示、快取、清除快取Web快取
- 開源框架——圖片載入和快取方案總結框架快取
- React Native圖片快取元件React Native快取元件
- 圖片快取(源於SDK文件)快取
- Glide 4.0.0 下之載入本地快取的圖片IDE快取
- Android開源框架ImageLoader的完美例子Android框架
- Android 圖片載入框架Android框架
- Swift iOS : WebView快取圖片的方法SwiftiOSWebView快取
- 輕量級Android快取框架ASimpleCacheAndroid快取框架
- Android圖片之svgAndroidSVG
- Android圖片快取之Bitmap詳解Android快取
- Android 圖片載入快取問題:為什麼你的Glide快取沒有起作用?Android快取IDE
- Android平滑圖片載入和快取庫 Glide 使用詳解Android快取IDE
- Android 平滑圖片載入和快取庫 Glide 使用詳解Android快取IDE
- android glide圖片載入框架AndroidIDE框架
- 【構建Android快取模組】(三)Controller & 非同步圖片載入Android快取Controller非同步
- Android圖片快取之Lru演算法Android快取演算法
- Android圖片快取之Glide進階Android快取IDE
- Android圖片快取之初識GlideAndroid快取IDE
- android非同步圖片載入三之handler+執行緒池+訊息佇列模式+快取Android非同步執行緒佇列模式快取
- [Android]反射讀取drawable中圖片Android反射
- 快取、快取演算法和快取框架簡介快取演算法框架