圖片緩存原理作為android進階的必備知識,是一名中高級開發(fā)人員必須掌握的,也經(jīng)常在面試中被問到,故做一下記錄。
眾所周知,為避免內(nèi)存溢出,圖片有三級緩存的說法,即內(nèi)存,硬盤,網(wǎng)絡(luò)。
內(nèi)存緩存技術(shù)
核心類是LruCache,它的算法原理是把最近使用的對象用強引用存儲在LinkedHashMap中,并且把最近最少使用的對象在緩存值達到預(yù)設(shè)值之前從內(nèi)存中移除。
代碼解釋:
// 獲取到可用內(nèi)存的最大值,使用內(nèi)存超出這個值會引起OutOfMemory異常。
// LruCache通過構(gòu)造函數(shù)傳入緩存值,以KB為單位。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用最大可用內(nèi)存值的1/8作為緩存的大小。
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 重寫此方法來衡量每張圖片的大小,默認(rèn)返回圖片數(shù)量。
return bitmap.getByteCount() / 1024;
}
};
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
當(dāng)向ImageView中加載圖片的時候,首先會在緩存中檢查是否存在,如果根據(jù)key值找到該圖片會立即更新,否則開啟一條線程加載這張圖片。
如下:
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
}
BitmapWorkerTask 還要把新加載的圖片的鍵值對放到緩存中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
// 在后臺加載圖片。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
}
硬盤緩存
核心類DiskLruCache。
DiskLruCache并沒有限制數(shù)據(jù)的緩存位置,可以自由地進行設(shè)定,但是通常情況下多數(shù)應(yīng)用程序都會將緩存的位置選擇為 /sdcard/Android/data/<application package>/cache 這個路徑。選擇在這個位置有兩點好處:第一,這是存儲在SD卡上的,因此即使緩存再多的數(shù)據(jù)也不會對手機的內(nèi)置存儲空間有任何影響,只要SD卡空間足夠就行。第二,這個路徑被Android系統(tǒng)認(rèn)定為應(yīng)用程序的緩存路徑,當(dāng)程序被卸載的時候,這里的數(shù)據(jù)也會一起被清除掉,這樣就不會出現(xiàn)刪除程序之后手機上還有很多殘留數(shù)據(jù)的問題。
DiskLruCache是不能new出實例的,如果我們要創(chuàng)建一個DiskLruCache的實例,則需要調(diào)用它的open()方法。
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
open()方法接收四個參數(shù),第一個參數(shù)指定的是數(shù)據(jù)的緩存地址,第二個參數(shù)指定當(dāng)前應(yīng)用程序的版本號,第三個參數(shù)指定同一個key可以對應(yīng)多少個緩存文件,基本都是傳1,第四個參數(shù)指定最多可以緩存多少字節(jié)的數(shù)據(jù)。
獲取緩存地址方法:
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
接著又將獲取到的路徑和一個uniqueName進行拼接,作為最終的緩存路徑返回。那么這個uniqueName又是什么呢?其實這就是為了對不同類型的數(shù)據(jù)進行區(qū)分而設(shè)定的一個唯一值,比如說在網(wǎng)易新聞緩存路徑下看到的bitmap、object等文件夾。
獲取版本號代碼:
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
需要注意的是,每當(dāng)版本號改變,緩存路徑下存儲的所有數(shù)據(jù)都會被清除掉,因為DiskLruCache認(rèn)為當(dāng)應(yīng)用程序有版本更新的時候,所有的數(shù)據(jù)都應(yīng)該從網(wǎng)上重新獲取。
后面兩個參數(shù)就沒什么需要解釋的了,第三個參數(shù)傳1,第四個參數(shù)通常傳入10M的大小就夠了,這個可以根據(jù)自身的情況進行調(diào)節(jié)。
綜上所述,一個標(biāo)準(zhǔn)的open方法可以這樣寫:
DiskLruCache mDiskLruCache = null;
try {
File cacheDir = getDiskCacheDir(context, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
有了DiskLruCache的實例之后,我們就可以對緩存的數(shù)據(jù)進行操作了,操作類型主要包括寫入、訪問、移除等。
- 寫入
寫入的操作是借助DiskLruCache.Editor這個類完成的。類似地,這個類也是不能new的,需要調(diào)用DiskLruCache的edit()方法來獲取實例,接口如下所示:
public Editor edit(String key) throws IOException
可以看到,edit()方法接收一個參數(shù)key,這個key將會成為緩存文件的文件名,并且必須要和圖片的URL是一一對應(yīng)的。那么怎樣才能讓key和圖片的URL能夠一一對應(yīng)呢?直接使用URL來作為key?不太合適,因為圖片URL中可能包含一些特殊字符,這些字符有可能在命名文件時是不合法的。其實最簡單的做法就是將圖片的URL進行MD5編碼,編碼后的字符串肯定是唯一的,并且只會包含0-F這樣的字符,完全符合文件的命名規(guī)則。
public String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
因此,現(xiàn)在就可以這樣寫來得到一個DiskLruCache.Editor的實例:
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
有了DiskLruCache.Editor的實例之后,我們可以調(diào)用它的newOutputStream()方法來創(chuàng)建一個輸出流。
完整的寫入操作:
new Thread(new Runnable() {
@Override
public void run() {
try {
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKeyForDisk(imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
-
讀取
完整的讀取緩存代碼如下:try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); mImage.setImageBitmap(bitmap); } } catch (IOException e) { e.printStackTrace(); }
-
移除
try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); mDiskLruCache.remove(key); } catch (IOException e) { e.printStackTrace(); }
用法雖然簡單,但是你要知道,這個方法我們并不應(yīng)該經(jīng)常去調(diào)用它。因為你完全不需要擔(dān)心緩存的數(shù)據(jù)過多從而占用SD卡太多空間的問題,DiskLruCache會根據(jù)我們在調(diào)用open()方法時設(shè)定的緩存最大值來自動刪除多余的緩存。只有你確定某個key對應(yīng)的緩存內(nèi)容已經(jīng)過期,需要從網(wǎng)絡(luò)獲取最新數(shù)據(jù)的時候才應(yīng)該調(diào)用remove()方法來移除緩存。
兩者的融合使用參考:http://blog.csdn.net/boyupeng/article/details/47127605