教你寫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 ImageLoader框架之圖片快取 (完結篇)
在教你寫Android ImageLoader框架系列博文中,我們從基本架構到具體實現已經更新了大部分的內容。今天,我們來講最後一個關鍵點,即圖片的快取。為了使用者體驗,通常情況下我們都會將已經下載的圖片快取起來,一般來說記憶體和本地都會有圖片快取。那既然是框架
教你寫Android網路框架之基本架構
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!  
教你寫Android網路框架之Request Response類與請求佇列
我正在參加部落格之星,點選這裡投我一票吧,謝謝~ 前言在教你寫Android網路框架之基本架構一文中我們已經介紹了SimpleNet網路框架的基本結構,今天我們就開始從程式碼的角度來開始切入該網路框架的實現,在剖析的同時我們會分析設計思路,以及為什麼要這樣做,這樣做的好處是
手把手教你用R語言建立信用評分模型(完結篇)— —打分卡轉換
打分卡轉換 我們在上一部分,我們已經基本完成了建模相關的工作,並用混淆矩陣驗證了模型的預測能力。接下來的步驟,就是將Logistic模型轉換為標準打分卡的形式。 在建立標準評分卡之前,我們需要選取幾個評分卡引數:基礎分值、 PDO(比率翻倍的分值)和好
教你寫響應式框架(二)
還要做什麼? 在教你寫響應式框架(一)中我們介紹了觀察者模式,現在我們將基於上一篇中的程式碼進行改造。當然,我們是有目的的改造: 在響應式框架中,觀察者是可能隨時產生,種類多,生命週期卻短暫. 我們希望操作是非同步的,並且只有在觀察者被註冊到
教你寫響應式框架(一)
在真正開始編寫自己的響應式框架之前,我們先來從觀察者模式說起。已經對觀察者模式很熟悉的可以直接掠過。 基本概念 觀察者模式屬於物件行為模式之一,也可叫做釋出——訂閱模式。它定義了一種以對多的依賴關係,讓多個觀察者(訂閱者)同時觀察(監聽)一個被觀察者(主
手把手教你寫基於C++ Winsock的圖片下載的網路爬蟲
先來說一下主要的技術點: 1. 輸入起始網址,使用ssacnf函式解析出主機號和路徑(僅處理http協議網址) 2. 使用socket套接字連線伺服器,,獲取網頁html程式碼(使用http協議的GET請求),然後使用正則表示式解析出圖片url和其他的url。 3. 下載圖
手把手教你寫一個手勢密碼解鎖View(GesturePasswordView)
相信大家在很多的app肯定看到過手勢密碼解鎖View,但是大家有沒有想過怎麼實現這樣一個View,哈,接下來,小編手把手教大家教寫一個GesturePasswordView。 先看一張效果圖 要實現這樣一個效果,首先需要在螢幕上繪製一個3x3九宮圖,如下圖 具體思路:
手把手教你寫個簡易計算器--棧的應用(C++)
程式使用範圍 1) 運算數為實數 2) 運算子為+、-、*、/、(、)、# 3) 運算結果為實數 設計流程 主要分為三步 1,表示式預處理 2,建立運算子優先表 3,運算求值 1) 表示式預處理 從檔案中讀取一行,去除所有空格,並在表示式首尾各新增一個符號‘#’,表示
教你做一個單機版人事管理系統(Winform版)treeview與listview使用詳情
不讓 ogr lena 位置 exc bject tel horizon raw ------------------------------------------------------------------部門部分--------------------------
Django框架之模板語法(重要!)
便在 light 自定義標簽 註釋 submit 調用 不為 items AI 一、什麽是模板? 只要是在html裏面有模板語法就不是html文件了,這樣的文件就叫做模板。 二、模板語法分類 1、模板語法之變量:語法為 {{ }}: 在 Django 模板中遍歷復雜數據結
獨家 | 手把手教你用Python建立簡單的神經網路(附程式碼)
作者:Michael J.Garbade 翻譯:陳之炎 校對:丁楠雅 本文共2000字,建議閱讀9分鐘。本文將為你演示如何建立一個神經網路,帶你深入瞭解神經網路的工作方式。 瞭解神經網路工作方式的最佳途徑莫過於親自建立一個神經網路,本文將演示如何做到這一點。
手把手教你編寫一個具有基本功能的shell(已開源)
/*read command line until EOF*/while(read(stdin,buffer,numchars)){ /*parse command line*/ if(/* command line contains & */) amper = 1;
應用程式框架實戰十四:DDD分層架構之領域實體(基礎篇)
using System.ComponentModel.DataAnnotations; using System.Text; namespace Util.Domains { /// <summary> /// 領域實體 /// </summary
應用程式框架實戰十六:DDD分層架構之值物件(介紹篇)
前面介紹了DDD分層架構的實體,並完成了實體層超型別的開發,同時提供了驗證方面的支援。本篇將介紹另一個重要的構造塊——值物件,它是聚合中的主要成分。 如果說你已經在使用DDD分層架構,但你卻從來沒有使用過值物件,這毫不奇怪,因為多年來養成的資料建模思維已經牢牢把你禁錮,以致於你在使用面向物件方式進行
應用程式框架實戰十五:DDD分層架構之領域實體(驗證篇)
在應用程式框架實戰十四:DDD分層架構之領域實體(基礎篇)一文中,我介紹了領域實體的基礎,包括標識、相等性比較、輸出實體狀態等。本文將介紹領域實體的一個核心內容——驗證,它是應用程式健壯性的基石。為了完成領域實體的驗證,我們在前面已經準備好了驗證公共操作類和異常公共操作類。 .Net提供的DataA
教你怎麼在本地做二級域名解析(apache版)
之前我從來沒有這麼去弄過,在網上也找了一些時間,有些不能用,這個是apache+mysql+php+windows版本,絕對可用,而且很簡單。 首先把意思說清楚,你們想說的是不是這個意思——在本地127.0.0.1就相當於一個本地IP,系統預設localhost解析到這個I
Webpack實戰(八):教你搞懂webpack如果實現程式碼分片(code splitting)
2020年春節已過,本來打算回鄭州,卻因為新型冠狀病毒感染肺炎的疫情公司推遲了上班的時間,我也推遲了去鄭州的時間,在家多陪娃幾天。以前都是在書房學習寫部落格,今天比較特殊,抱著電腦,在樓頂晒著太陽,陪著家人,寫著部落格。 前面的幾篇文章主要告訴大家如何安裝、配置webpack、webpack實現樣式分離
[C#] (原創)一步一步教你自定義控制元件——02,ScrollBar(滾動條)
一、前言 技術沒有先進與落後,只有合適與不合適。 本篇的自定義控制元件是:滾動條(ScollBar)。 我們可以在網上看到很多自定義的滾動條控制元件,它們大都是使用UserControl去做,即至少使用一個Panel或其它控制元件作滑塊,使用UserControl本身或另一個控制元件作為背景條,而有的複雜的還
手把手教你在win10下搭建pytorch GPU環境(Anaconda+Pycharm)
Anaconda指的是一個開源的[Python](https://baike.baidu.com/item/Python)發行版本,其主要優點如下: - Anaconda預設安裝了常見的科學計算包,用它搭建起Python環境後不用再費時費力安裝這些包; - Anaconda可以建立互相隔離的虛擬環境,可以