【【【【ImageCache】】】】
Android中最常用到緩存的地方就是圖片,通過(guò)過(guò)緩存即可以提高應(yīng)用程序的效率,又可以節(jié)省用戶的流量。
圖片的緩存簡(jiǎn)單來(lái)說(shuō)可以分為SD卡緩存和內(nèi)存緩存,也可以倆者配合使用。
Android中圖片緩存遵循的策略就是:當(dāng)?shù)谝淮螐木W(wǎng)絡(luò)中加載圖片的時(shí)候,將其緩存到存儲(chǔ)設(shè)備上(比如sd卡,這也就是我們說(shuō)的SD卡緩存),并且在內(nèi)存中同樣也緩存一份(內(nèi)存緩存),這樣當(dāng)下次使用或者網(wǎng)絡(luò)請(qǐng)求圖片的時(shí)候,就先去內(nèi)存中獲取(因?yàn)閺膬?nèi)存中讀取要比從SD卡中讀取的快,所以我們先從內(nèi)存中讀取,這樣的話在節(jié)省流量的同時(shí)也可以提高程序的性能),如果內(nèi)存中沒(méi)有的話再去SD卡中獲取,如果SD卡中也沒(méi)有的話才去網(wǎng)絡(luò)上請(qǐng)求。
當(dāng)然,由于設(shè)備的存儲(chǔ)容量限制,我們也需要把緩存到SD卡的圖片資源在一個(gè)合適的時(shí)間內(nèi)刪除。當(dāng)然,這個(gè)合適的時(shí)間怎么定義就仁者見(jiàn)仁智者見(jiàn)智了,目前常用的是算法是LRU算法,所以一個(gè)完整的緩存策略包括添加,獲取,刪除三類操作
LRU緩存算法
LRU(Least Recently Used),近期最少使用算法,它的核心思想就是當(dāng)緩存滿時(shí),優(yōu)先淘汰即刪除近期最少使用的緩存對(duì)象。
采用LRU算法的緩存有倆種:
- LruCache,用于實(shí)現(xiàn)內(nèi)存緩存
- DiskLruCache,用于實(shí)現(xiàn)存儲(chǔ)設(shè)備緩存(它不屬于官方SDK的一部分,但得到官方的推薦)
- 其他存儲(chǔ)方式,比如文件存儲(chǔ)。SqlLite存儲(chǔ)等
LruCache
LruCache是Android 3.1 提供的,使用support-4兼容包中LruCache可以向下兼容到2.2。
LruCache內(nèi)部采用一個(gè)LinkedHashMap以強(qiáng)引用的方式存儲(chǔ)外界提供的緩存對(duì)象,提供了put和get方法來(lái)添加和獲取緩存對(duì)象,另外LruCache也是線程安全的。。
1、LruCache的使用
Runtime.getRuntime().maxMemory()
在Java中返回的是java虛擬機(jī)(這個(gè)進(jìn)程)能構(gòu)從操縱系統(tǒng)那里挖到的最大的內(nèi)存,而在Android中返回應(yīng)用程序最大可用內(nèi)存,以字節(jié)為單位。也可以用
((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass()
來(lái)獲取,不過(guò)返回值的單位是M。
Runtime.getRuntime().totalMemory()
在Java中返回的是java虛擬機(jī)現(xiàn)在已經(jīng)從操縱系統(tǒng)那里挖過(guò)來(lái)的內(nèi)存大小,也就是java虛擬機(jī)這個(gè)進(jìn)程當(dāng)時(shí)所占用的所有內(nèi)存,在Android中返回的是應(yīng)用程序已獲得內(nèi)存,所以totalMemory()是慢慢增大的。
Runtime.getRuntime().freeMemory()
簡(jiǎn)單來(lái)說(shuō)就是已經(jīng)獲取到但是還沒(méi)有使用的內(nèi)存。
創(chuàng)建LruCache代碼:
//創(chuàng)建LruCache對(duì)象
int maxSize = (int) Runtime.getRuntime().maxMemory();// 返回byte
int cacheSize = maxSize / 1024 / 8;
// 創(chuàng)建LruCache時(shí)需要提供緩存的最大容量
mCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return super.sizeOf(key, value);
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
// 當(dāng)調(diào)用put或remove時(shí)觸發(fā)此方法,可以在這里完成一些資源回收的操作
super.entryRemoved(evicted, key, oldValue, newValue);
}
};
mCache.put(key, value);// 添加緩存對(duì)象
mCache.get(key);// 獲取緩存對(duì)象
mCache.remove(key);// 刪除緩存對(duì)象
mCache.resize(1024);// 重新設(shè)置緩存的總?cè)萘看笮?
DiskLruCache
上面講過(guò),DiskLruCache不屬于Android SDK 的一部分,但是它得到了Google官方的認(rèn)可與推薦,它的源碼在這里或者這里;其實(shí)GitHub上才是正兒八經(jīng)的出產(chǎn)地哈。在使用DiskLruCache時(shí),首先要從網(wǎng)上下載DiskLruCache的源碼,然后放到自己的項(xiàng)目里編譯后才能正常使用。
1、DiskLruCache 的創(chuàng)建
DiskLruCache不能通過(guò)構(gòu)造方法來(lái)創(chuàng)建,它通過(guò)open方法來(lái)穿件自身
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
- directory表示磁盤(pán)緩存的存儲(chǔ)路徑
緩存目錄沒(méi)有具體限制,可以根據(jù)需求自己的定義。一般來(lái)說(shuō),可以選擇SD卡上的/sdcard/Android/data/<application package>/cache
目錄,這個(gè)目錄是Android系統(tǒng)指定的應(yīng)用程序緩存目錄,當(dāng)應(yīng)用卸載時(shí),緩存也會(huì)被系統(tǒng)清除;當(dāng)然還可以選擇sd卡上的其他目錄,也可以選擇data下的當(dāng)前應(yīng)用目錄。當(dāng)然,一個(gè)嚴(yán)禁的程序還要考慮SD卡是否存在等。 - appVersion表示應(yīng)用的版本號(hào)
當(dāng)appVersion改變時(shí),之前的緩存都會(huì)被清除,所以如非必要,我們?yōu)槠渲付ㄒ粋€(gè)1,不再改變即可 - valueCount表示單個(gè)節(jié)點(diǎn)對(duì)應(yīng)的數(shù)據(jù)個(gè)數(shù),也就是同一個(gè)key可以對(duì)應(yīng)多少個(gè)緩存文件,一般來(lái)說(shuō)我們都選取1.
- maxSize緩存的總大小。
private void createDiskLruCache() {
try {
File cacheDir = getDiskCacheDir(DiskLruCacheActivity.this, "bitmapsCache");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
diskLruCache = DiskLruCache.open(cacheDir, 1, 1, MAX_DISK_CACHE);
} catch (IOException e) {
Log.i("disk cache", "createDiskLruCache e: " + e.toString());
}
}
2、DiskLruCache 添加緩存
DiskLruCache的緩存添加是通過(guò)Editor完成的,代碼如下:
// 把圖片添加到硬盤(pán)緩存
private void exeAdd2DiskCache(DiskLruCache.Editor editor) {
if (editor != null) {
try {
// 創(chuàng)建DiskLruCache時(shí)設(shè)置一個(gè)節(jié)點(diǎn)只有一個(gè)數(shù)據(jù),所以這里的index直接設(shè)為0即可
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();// 提交寫(xiě)入操作
Log.i("disk cache", "onCreate editor.commit() ");
} else {
editor.abort();// 回退整個(gè)操作
Log.i("disk cache", "onCreate editor.abort() ");
}
diskLruCache.flush();
} catch (IOException e) {
Log.i("disk cache", "onCreate e: " + e.toString());
}
}
}
// 建立HTTP請(qǐng)求,并獲取Bitmap對(duì)象。
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
當(dāng)然,以上只是展示DiskLruCache的用法,在實(shí)際項(xiàng)目中還要和項(xiàng)目需求結(jié)合,考慮實(shí)際情況,比如Caused by: android.os.NetworkOnMainThreadException
異常等。
3、DiskLruCache 從緩存中查找
首先需要將URL轉(zhuǎn)換成Key,然后通過(guò)DiskLruCache的get方法得到一個(gè)Snapshot對(duì)象,在通過(guò)Snapshot對(duì)象得到緩存文件的輸入流,再把輸入流轉(zhuǎn)換成Bitamp對(duì)象。
// 從緩存中獲取Bitmap對(duì)象
private Bitmap getCacheBitmap() {
String key = hashKeyForDisk(imageUrl);// 把Url轉(zhuǎn)換成KEY
try {
DiskLruCache.Snapshot snapShot = diskLruCache.get(key);// 通過(guò)key獲取Snapshot對(duì)象
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);// 通過(guò)Snapshot對(duì)象獲取緩存文件的輸入流
Bitmap bitmap = BitmapFactory.decodeStream(is);// 把輸入流轉(zhuǎn)換成Bitmap對(duì)象
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
關(guān)于 BitmapFactory.decodeStream(is) 更多資料請(qǐng)點(diǎn)這里查看
4、DiskLruCache 緩存的刪除
private void deleteCacheBitmap(String url){
try {
String key = hashKeyForDisk(imageUrl);
diskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
}
這個(gè)remove方法,如果不是必要的話,我們盡量不要去調(diào)用,因?yàn)槲覀円呀?jīng)設(shè)定了最大緩存容量,當(dāng)超過(guò)這個(gè)容量時(shí),系統(tǒng)會(huì)根據(jù)Lru算法自動(dòng)刪除該刪除的緩存文件。
5、DiskLruCache 其他幾個(gè)重要且常用的方法
- diskLruCache.delete() 刪除所有的緩存數(shù)據(jù)
- diskLruCache. size() 返回當(dāng)前緩存路徑下所有緩存數(shù)據(jù)的大小,以byte為單位
- editor.abort() 圖片下載發(fā)生異常時(shí),可以通過(guò)這個(gè)方法回退整個(gè)操作
- editor.commit() 提交寫(xiě)入操作,這個(gè)放在在寫(xiě)入緩存數(shù)據(jù)時(shí)是一定要調(diào)用的
- diskLruCache.flush() 這個(gè)方法用于將內(nèi)存中的操作記錄同步到日志文件(也就是journal文件,系統(tǒng)LRU算法依賴于這個(gè)文件,因?yàn)檫@個(gè)文件中保存著對(duì)數(shù)據(jù)的操作記錄)當(dāng)中;但其實(shí)并不是每次寫(xiě)入緩存都要調(diào)用一次flush()方法的,頻繁地調(diào)用并不會(huì)帶來(lái)任何好處,只會(huì)額外增加同步j(luò)ournal文件的時(shí)間。比較標(biāo)準(zhǔn)的做法就是在Activity的onPause()方法中去調(diào)用一次flush()方法就可以了。
- diskLruCache.close() 這個(gè)方法用于將DiskLruCache關(guān)閉掉,是和open()方法對(duì)應(yīng)的一個(gè)方法。關(guān)閉掉了之后就不能再調(diào)用DiskLruCache中任何操作緩存數(shù)據(jù)的方法,通常只應(yīng)該在Activity的onDestroy()方法中去調(diào)用close()方法。
6、journal 文件
當(dāng)執(zhí)行完寫(xiě)入操作后,我們看看對(duì)應(yīng)的目錄下(/sdcard/Android/data/<application package>/cache)有什么文件?
打開(kāi)cache目錄后,發(fā)現(xiàn)里邊只有一個(gè)bitmapsCache的文件夾;這個(gè)文件是哪里來(lái)的呢?這個(gè)其實(shí)就是上文在創(chuàng)建DiskLruCache實(shí)例時(shí)傳入的url中拼接的(看 getDiskCacheDir 方法),為什么要指定這么一個(gè)目錄呢?其實(shí)就是類似于分類的概念,比如你可以把緩存的Bitmap放到一個(gè)文件夾下,把file或者其他格式的數(shù)據(jù)放到另外一個(gè)文件夾下。
打開(kāi)bitmapsCache文件夾,它的子目錄又有哪些呢?首先有一個(gè)文件,這個(gè)文件的文件名很長(zhǎng)而且沒(méi)有任何規(guī)則,完全看不懂是什么意思;另外下邊還有一個(gè)journal文件。其實(shí)文件名很長(zhǎng)的文件就是一張緩存的圖片,每個(gè)文件都對(duì)應(yīng)著一張圖片,如果我們緩存了很多圖片的話,就會(huì)有一堆這樣的文件;而journal文件是DiskLruCache的一個(gè)日志文件,就像我上面說(shuō)的:這個(gè)文件中保存著對(duì)數(shù)據(jù)的操作記錄。如下圖:
打開(kāi)journal這個(gè)文件,發(fā)現(xiàn)它長(zhǎng)成這樣 長(zhǎng)得也很整齊:
首先有一行字符串“l(fā)ibcore.io.DiskLruCache”表示我們使用的是DiskLruCache技術(shù);然后又有三行,且每行都只有一個(gè)“1”,其中第一行的“1”表示DiskLruCache的版本號(hào),這個(gè)值是恒為1的,第2行的“1”表示應(yīng)用程序的版本號(hào),我們?cè)趏pen()方法里傳入的版本號(hào)是什么這里就會(huì)顯示什么,第三個(gè)“1”表示的是valueCount,表示單個(gè)節(jié)點(diǎn)對(duì)應(yīng)的數(shù)據(jù)個(gè)數(shù),這個(gè)值也是在open()方法中傳入的,通常情況下都為1。接下來(lái)就是一個(gè)空行,標(biāo)志著開(kāi)始記錄數(shù)據(jù)的操作記錄。
接下來(lái),會(huì)有DIRTY開(kāi)頭的一行數(shù)據(jù),DIRTY后邊跟著的是文件的key,DIRTY表示開(kāi)始向緩存中寫(xiě)入數(shù)據(jù),但寫(xiě)入結(jié)果是什么還未知。然后調(diào)用commit()方法表示寫(xiě)入緩存成功,這時(shí)會(huì)向journal中寫(xiě)入一條CLEAN記錄,表示數(shù)據(jù)寫(xiě)入成功;如果數(shù)據(jù)寫(xiě)入失敗,會(huì)調(diào)用abort()方法回退整個(gè)操作,這時(shí)會(huì)向journal中寫(xiě)入一條REMOVE記錄。當(dāng)調(diào)用get()方法去讀取一條緩存數(shù)據(jù)時(shí),就會(huì)向journal文件中寫(xiě)入一條READ記錄;另外,某些行后面還有一個(gè)數(shù)字(20090、6602),這個(gè)數(shù)字就是緩存圖片的大小,以byte為單位。