當用戶要瀏覽微信的某張圖片或視頻時,第一次肯定需要從網絡上下載下來才能看,但如果第二次去瀏覽,還要從網絡上下載就不合適了,用戶體驗差,更重要的是浪費了用戶的流量。當圖片首次下載下來的時候,我們需要對圖片做一下緩存,這樣再次讀它的時候直接從緩存中取就可以了。圖片緩存對於目前的主流圖片加載框架(比如UniversalImageLoader)是最最基礎的功能了。
緩存通常分爲兩種:內存緩存和硬盤緩存。當應用打算從網絡上請求一張圖片時,先嚐試從內存中獲取,如果沒有再嘗試從硬盤中獲取,還是沒有再從網絡上下載。因爲內存速度>硬盤速度>下載速度,而且還能節省流量。上述的緩存策略不只適用於圖片,還適用於其他文件類型。
緩存算法
內存緩存和硬盤緩存的存儲空間都是有限的,而且使用緩存時都需要制定一個最大的使用容量。如果超過這個容量,但程序還需要添加緩存,就需要刪除一些舊的緩存。目前最常用的一種緩存算法是LRU(Least Recently Used),最近最少使用算法,當緩存滿時會優先淘汰那些近期最少使用的緩存對象。採用LRU算法的緩存有兩種:LruCache和DiskLruCache,其中LruCache用於實現內存緩存,DiskLruCache用於實現硬盤緩存。
LruCache
從Android 3.1開始提供這個類,之前的android版本想使用的話可以用support-v4下面的。
LruCache是一個泛型類,內部通過LinkedHashMap以強引用的方式存儲緩存對象,它本身也提供了get和put方法供外界調用。另外它是線程安全的:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
public LruCache(int maxSize) {
new一個LruCache對象時,構造函數的參數需指定緩存的總容量大小。下面通過一個小demo來看一下它的使用:
public class MainActivity extends ActionBarActivity {
private LruCache<String, Bitmap> mLruCache;
private ImageView mImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImageView = (ImageView) findViewById(R.id.iv);
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 4;
mLruCache = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
Bitmap bitmap;
String path = new File(Environment.getExternalStorageDirectory(), "a.jpg").getAbsolutePath();
if((bitmap = BitmapFactory.decodeFile(path)) != null){
//先緩存後再取出
mLruCache.put(path, bitmap);
mImageView.setImageBitmap(mLruCache.get(path));
}else{
Toast.makeText(MainActivity.this, "error path", Toast.LENGTH_SHORT).show();
}
}
}
這個demo中,設定了緩存容量的總大小爲當前進程可用內存的1/4,單位是KB。另外重寫了sizeOf方法,它的作用是計算緩存對象的大小,注意它的單位應該跟總容量的單位保持一致,這裏都是KB。
一些特殊情況下,還需要重寫entryRemoved方法,LruCache移除舊緩存時會調用該方法,因此可以在其中完成一些資源回收工作。
DiskLruCache
這個類並不在android源碼中,使用時需要手動把這個文件加入到項目中,它的源碼可以從google source中獲取:
android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java
如果網址打不開可以點擊這裏下載源碼:點我下載
複製到工程中後注意改一下包名。
這個類的構造方法是私有的,只能通過下面方法去構造對象:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
首先來講解一下參數的含義:
directory表示要緩存的路徑,我們選擇路徑的時候最好存在/sdcard/Android/data//cache裏,因爲系統可以識別出這是應用的緩存路徑,當程序被卸載時這裏的數據會被一起清掉;另外cache下面可以再加一級路徑,比如/bitmap/,用來區分不同的緩存對象類型。獲取路徑可以參考下面的代碼:
private File getDiskCacheDir(String folderName) {
String cachePath;
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) && !Environment.isExternalStorageRemovable()){
cachePath = getExternalCacheDir().getPath();
}else{
cachePath = getCacheDir().getPath();
}
return new File(cachePath, folderName);
}
appVersion表示當前應用的版本,如果版本號改變了,那麼緩存會被清空,數據需要從網上重新獲取。獲取版本號可以參考下面的代碼:
private int getAppVersion() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
return packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
valueCount表示一個key可以對應幾個緩存文件,通常設爲1。
maxSize表示緩存容量的最大值。
得到DiskLruCache對象後,就可以緩存文件了,比如我們要緩存網上的一個bitmap,具體步驟是:
1.通過DiskLruCache對象獲取DiskLruCache.Editor對象,要執行緩存操作必須要用到這個editor對象:
DiskLruCache.Editor editor = mDiskLruCache.edit(cacheKey);
2.通過editor獲取輸出流,用來存儲緩存:
OutputStream os = editor.newOutputStream(0);
這裏參數傳0是因爲創建DiskLruCache時valueCount我們傳了1。
3.從網上下載bitmap,將訪問url得到的輸入流寫入到第2步得到的輸出流裏:
private boolean downloadBitmap(String urlString, OutputStream os) {
try {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
BufferedInputStream bis = new BufferedInputStream(conn.getInputStream(), 4 * 1024);
BufferedOutputStream bos = new BufferedOutputStream(os, 4 * 1024);
int len;
while((len = bis.read()) != -1){
bos.write(len);
}
bis.close();
bos.close();
conn.disconnect();
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
4.將輸出流存入緩存:
if(downloadBitmap(downloadUrl, os)){
editor.commit();
}else{
editor.abort();
}
mDiskLruCache.flush();
commit代表提交,即寫入生效;abort代表放棄此次操作。調用flush()表示將操作記錄都同步到journal文件裏,這個journal文件是DiskLruCache的操作記錄日誌,它的位置也在上面我們指定的緩存目錄下,它是DiskLruCache能夠正常工作的前提,我們不需要頻繁調用,一般只需在onPause()時調用即可。
接下來是讀取緩存文件,具體步驟是:
1.通過DiskLruCache對象獲取DiskLruCache.Snapshot對象,要讀取緩存必須要用到這個snapshot對象:
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(cacheKey);
2.通過snapshot獲取輸入流:
InputStream is = snapshot.getInputStream(0);
這裏參數傳0也是因爲創建DiskLruCache時valueCount我們傳了1。
3.得到輸入流以後就可以做業務相關的操作了,比如解析出bitmap:
Bitmap bitmap = BitmapFactory.decodeStream(is);
完整demo如下:
public class MainActivity extends ActionBarActivity {
private final String downloadUrl = "http://img3.douban.com/view/note/large/public/p28933592.jpg";
private DiskLruCache mDiskLruCache;
private ImageView mImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImageView = (ImageView) findViewById(R.id.iv);
//打開硬盤緩存,最大容量設爲10M
final File cacheDir = getDiskCacheDir("bitmap");
if(!cacheDir.exists()){
cacheDir.mkdirs();
}
final int appVersion = getAppVersion();
new Thread(new Runnable() {
@Override
public void run() {
try {
mDiskLruCache = DiskLruCache.open(cacheDir, appVersion, 1, 10 * 1024 * 1024);
String cacheKey = getCacheKey(downloadUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(cacheKey);
if(editor != null){
OutputStream os = editor.newOutputStream(0);
//從網絡上下載一張圖片
if(downloadBitmap(downloadUrl, os)){
//將下載的圖片存入到緩存中
editor.commit();
}else{
editor.abort();
}
}
mDiskLruCache.flush();
//從緩存中讀取該圖片並顯示在ImageView中
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(cacheKey);
if(snapshot != null){
InputStream is = snapshot.getInputStream(0);
final Bitmap bitmap = BitmapFactory.decodeStream(is);
runOnUiThread(new Runnable() {
@Override
public void run() {
mImageView.setImageBitmap(bitmap);
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private String getCacheKey(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();
}
private File getDiskCacheDir(String folderName) {
String cachePath;
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) && !Environment.isExternalStorageRemovable()){
cachePath = getExternalCacheDir().getPath();
}else{
cachePath = getCacheDir().getPath();
}
return new File(cachePath, folderName);
}
private int getAppVersion() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
return packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
private boolean downloadBitmap(String urlString, OutputStream os) {
try {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
BufferedInputStream bis = new BufferedInputStream(conn.getInputStream(), 4 * 1024);
BufferedOutputStream bos = new BufferedOutputStream(os, 4 * 1024);
int len;
while((len = bis.read()) != -1){
bos.write(len);
}
bis.close();
bos.close();
conn.disconnect();
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
這個demo中緩存的key沒有直接用url,而是用了url的MD5編碼,因爲url中可能包含特殊字符,不能作爲緩存文件的命名,而MD5編碼既唯一又肯定符合命名要求。
另外,除了上面的存取、獲取,還有移除操作,需要調用
mDiskLruCache.remove(key);
這個通常不需要我們操作,因爲緩存容量超過maxSize後,DiskLruCache會根據LRU算法自動刪除某些緩存,所以除非你很明確某個緩存是沒有必要的了,否則不必手動去調。
close()方法用於將DiskLruCache關閉掉,是和open()方法對應的一個方法。關閉掉以後就不能再調用DiskLruCache中任何操作緩存數據的方法,通常只應該在Activity的onDestroy()方法中去調用close()方法。
delete()方法用於將所有的緩存數據全部刪除,比如某些app設置裏通常都有的手動清理緩存功能,其實只需要調用一下DiskLruCache的delete()方法就可以實現了。
journal文件簡單介紹
由於現在只緩存了一張圖片,所以journal中並沒有幾行日誌,第一行是個固定的字符串“libcore.io.DiskLruCache”,標誌着使用了DiskLruCache。第二行是DiskLruCache的版本號,這個值是恆爲1的。第三行是應用程序的版本號,我們在open()方法裏傳入的版本號是什麼這裏就會顯示什麼。第四行是valueCount,這個值也是在open()方法中傳入的,通常情況下都爲1。第五行是一個空行。前五行也被稱爲journal文件的頭,這部分內容還是比較好理解的,但是接下來的部分就要稍微動點腦筋了。
第六行是以一個DIRTY前綴開始的,後面緊跟着緩存圖片的key。每當我們調用一次DiskLruCache的edit()方法時,都會向journal文件中寫入一條DIRTY記錄,表示我們正準備寫入一條緩存數據,但不知結果如何。然後調用commit()方法表示寫入緩存成功,這時會向journal中寫入一條CLEAN記錄,調用abort()方法表示寫入緩存失敗,這時會向journal中寫入一條REMOVE記錄。也就是說,每一行DIRTY的key,後面都應該有一行對應的CLEAN或者REMOVE的記錄,否則這條數據就是“髒”的,會被自動刪除掉。
除了DIRTY、CLEAN、REMOVE之外,還有一種前綴是READ的記錄,這個就非常簡單了,每當我們調用get()方法去讀取一條緩存數據時,就會向journal文件中寫入一條READ記錄。因此,非常大的程序journal文件中就可能會有大量的READ記錄,那麼你可能會擔心了,如果我不停頻繁操作的話,就會不斷地向journal文件中寫入數據,那這樣journal文件豈不是會越來越大?這倒不必擔心,DiskLruCache中使用了一個redundantOpCount變量來記錄用戶操作的次數,每執行一次寫入、讀取或移除緩存的操作,這個變量值都會加1,當變量值達到2000的時候就會觸發重構journal的事件,這時會自動把journal中一些多餘的、不必要的記錄全部清除掉,保證journal文件的大小始終保持在一個合理的範圍內。