本文出自 “阿敏其人” 簡(jiǎn)書博客,轉(zhuǎn)載或引用請(qǐng)注明出處。
在上一篇:安卓OOM和Bitmap圖片二級(jí)緩存機(jī)制(一)中,已經(jīng)討論了安卓中OOM發(fā)生的原因,情況和如何有效加載高清圖片的。現(xiàn)在在此回顧一下:
- 安卓OOM發(fā)生的原因:圖片分辨率過大,導(dǎo)致加載圖片所需的內(nèi)存超過系統(tǒng)給進(jìn)程(app)分配的運(yùn)行內(nèi)存,內(nèi)存爆掉,產(chǎn)生OOM
- 核心解決辦法: 利用BitmapFactory。Options的inSimpleSize,計(jì)算出合適的圖片采樣率,減小圖片分辨率。
再續(xù)前緣,接下來的這篇博客里面我們說圖片的緩存機(jī)制。
緩存機(jī)制,也叫二級(jí)緩存,實(shí)際上也就是一個(gè)圖片存儲(chǔ)策略,軟件中二級(jí)緩存是一個(gè)很常見圖片存取策略。
實(shí)現(xiàn)二級(jí)緩存只要使用的Lru機(jī)制(Least Recently Used),即為最近最少使用。
- Lru策略主要涉及兩個(gè)類:
- LruCache 用作內(nèi)存緩存
- DiskLruCache 用作存儲(chǔ)設(shè)備緩存
一、安卓圖片二級(jí)緩存機(jī)制
1、什么是二級(jí)緩存
移動(dòng)聯(lián)通電信是有錢的,用戶的流量是花錢買的,wifi不是7*24小時(shí)連著的,所以耗流量下載的圖片是需要緩存起來的,不能沒去都去重新聯(lián)網(wǎng)獲取圖片,一來速度慢二來耗流量。
所以,圖片要緩存,緩存很重要。
1.1、二級(jí)緩存,內(nèi)存緩存 --> 存儲(chǔ)設(shè)備緩存 --> 網(wǎng)絡(luò)
二級(jí)緩存,就是 內(nèi)存緩存 --> 存儲(chǔ)設(shè)備緩存 --> 網(wǎng)絡(luò)
每次二級(jí)走,每次都這么拿圖片,直到再拿到圖片的那一級(jí)就停下。
- 比如有圖片A:
* 1.第一次我們加載這樣圖片的時(shí)候,發(fā)現(xiàn)內(nèi)存緩存拿不到,就去存儲(chǔ)設(shè)備緩存拿,如果發(fā)現(xiàn)存儲(chǔ)設(shè)備緩存拿不到,那么就去網(wǎng)絡(luò)拿圖片,到了網(wǎng)絡(luò)就肯定能拿到圖片,除非url錯(cuò)誤或者網(wǎng)絡(luò)問題。
**當(dāng)我們成功拿到圖片之后,就會(huì)把這個(gè)圖片往內(nèi)存緩存和存儲(chǔ)設(shè)備緩存都存一份**
* 2.第二次拿圖片的時(shí)候,路還是這么走,先去 *內(nèi)存緩存* 里面找,發(fā)現(xiàn),哎呀,找到了,那么就停下來了,拿著圖片高興地和ImageView過上了幸福的生活,加載顯示出給用戶看了。
第3第4次也是這么走,反正就是 *內(nèi)存緩存* 、 *本地緩存*、 *網(wǎng)絡(luò)* 這么三個(gè)循序一條道走到底,知道拿到為止。
那么什么時(shí)候會(huì)拿到 本地緩存 呢,拿不到內(nèi)存緩存的時(shí)候就拿不到內(nèi)存緩存唄,那么明明從網(wǎng)絡(luò)拿到圖片的時(shí)候我們就往 內(nèi)存緩存 和 本地緩存 都存了一份備份的圖片,那么為什么會(huì)拿不到 內(nèi)存緩存 呢?因?yàn)閮?nèi)存緩存是緩存,是緩存都可能被清掉,空間不足等情況或者使用較少(Lru)卻占著茅坑不拉屎的時(shí)候就會(huì)被清掉;同樣的,本地緩存也有可能被清掉,反正只要是緩存都有可能被清掉。
1.2、緩存看得見嗎,在哪里
- 對(duì)于內(nèi)存緩存(路徑不是自己指定,是谷歌指定的)
* 1.在真機(jī)上是看不見的(除非你拿真機(jī)去刷機(jī)然后折騰各種命令然后一番浴血奮戰(zhàn),就可以看到了)
* 2.在模擬器的時(shí)看得到。目錄是 data/data/包名/cache - 對(duì)于存儲(chǔ)設(shè)備緩存
這個(gè)不管是在真機(jī)上還是在模擬器上都是可以看見的,路徑都是我們自己指定的
補(bǔ)充一下關(guān)于安卓手機(jī)內(nèi)部緩存的一點(diǎn)知識(shí)
-
如何獲得手機(jī)內(nèi)部存儲(chǔ)的路徑
- 1.getFileDir(); 獲取自己的文件夾 /data/data/包名/files
- 2.getCacheDir(); /data/data/包名/cache
<br>
files 目錄存放重要的應(yīng)用程序數(shù)據(jù).手機(jī)不會(huì)自動(dòng)清理files目錄的文件,除非用戶到 應(yīng)用管理 手動(dòng)清楚程序數(shù)據(jù),這個(gè)目錄下的數(shù)據(jù)才會(huì)被清除
<br>
cache 目錄是存放臨時(shí)的不重要的數(shù)據(jù).這個(gè)目錄有特定,當(dāng)手機(jī)內(nèi)存空間不足的時(shí)候會(huì)自動(dòng)清理cache目錄的文件
不管是cache還是files,都是屬于手機(jī)的內(nèi)部存儲(chǔ)空間
1.3、為什么用我們的二級(jí)存儲(chǔ)要采用Lru機(jī)制
首先我們按照最基本的思想開始思考,在沒有使用Lru(Least Recent Used)之前,我們可能會(huì)想,直接簡(jiǎn)單粗暴按照順序把圖片放進(jìn)緩存的文件夾,假設(shè)設(shè)定存放存放本地緩存的文件夾為30m(內(nèi)存緩存沒辦法自定好像),那么先進(jìn)來的先出去,想隊(duì)列看一樣不好嗎?其實(shí)確實(shí)不好,因?yàn)橛械膱D片雖然進(jìn)來的慢,但是卻需要經(jīng)常出現(xiàn),如果直接按照循序來決定誰先被清出緩存,那么就直接暴力了,這樣肯定不好,這時(shí)候,Lru機(jī)制出現(xiàn)了,他說,在最近的一段時(shí)間里面,誰被使用得最少,誰先出局,這個(gè)規(guī)則挺好的,然后大家都認(rèn)同了。
1.4、Lru的 取 存 刪 三個(gè)操作
- 說到底,我就是想從緩存里面拿圖片來顯示,那么現(xiàn)明顯,需要有一個(gè) 取 的過程。
- 那么,既然需要 取 圖片,那么在取之前一定要一個(gè)動(dòng)作,那么就是 存的過程
- 存放圖片緩存的地方大小是有限的,既然是有限的,那么沒用的或者說少用的多余的圖片,就需要被清理出去,那么就會(huì)有一個(gè) 刪 的過程
凡是對(duì)數(shù)據(jù)測(cè)操作,無非跟數(shù)據(jù)庫的基本操作原理一樣,終究逃不過 增、刪、改、查 著四個(gè)方面,但是我們實(shí)事求是,根據(jù)需要確定我們一個(gè)操作數(shù)據(jù)的流程到底需要具備增刪改查的那幾個(gè)方面,就可以最終確定我們需要的進(jìn)行的操作了,總結(jié)下來,就是 取、存、刪 三個(gè)過程
只要弄清楚了LruCache和DiskLruCache的 取 存 刪 各自三個(gè)過程是如何實(shí)現(xiàn)的,二級(jí)緩存機(jī)制我們幾乎都可以宣告完成了。
做一個(gè)小結(jié):二級(jí)緩存的主要使用LruCache算法,而Lru主要就是使用LruCache和DiskLruCache這兩個(gè)類。
要使用著兩個(gè)類,就是要明白著兩個(gè)類具體的 存 取 刪 分別是怎么進(jìn)行的。
2、LruCache類
LruCahce是Android3.0提供給我們的一個(gè)緩存類,我們可以通過support-v4包最低兼容到Android2.2,所以我們?cè)谑褂玫臅r(shí)候最好調(diào)用的V4包得LruCache,為了兼容嘛。
內(nèi)存緩存技術(shù)小歷史
在沒有Lru這套內(nèi)存緩存算法之前,(也就是安卓3.0之前),人們的做內(nèi)存緩存主要利用的是 弱引用 和 軟引用 ,從 Android 2.3 (API Level 9)開始,垃圾回收器會(huì)更傾向于回收持有軟引用或弱引用的對(duì)象,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的數(shù)據(jù)會(huì)存儲(chǔ)在本地的內(nèi)存當(dāng)中,因而無法用一種可預(yù)見的方式將其釋放,這就有潛在的風(fēng)險(xiǎn)造成應(yīng)用程序的內(nèi)存溢出并崩潰,所以在Android2.3之后再采用軟引用弱引用就不再合適了,這是也是L如出現(xiàn)的原因之一,因?yàn)長(zhǎng)ruCache是以強(qiáng)引用的方式引用外界的緩存對(duì)象的,被強(qiáng)引用的對(duì)象gc(垃圾回收機(jī)制)是沒有辦法對(duì)其進(jìn)行回收的。
強(qiáng)引用: 直接的對(duì)象引用,不會(huì)被gc回收
軟引用:(SoftReference) 當(dāng)一個(gè)對(duì)象只有弱引用存在時(shí),當(dāng)系統(tǒng)內(nèi)存不足就gc會(huì)隨時(shí)回收這個(gè)對(duì)象
弱引用:(WeakReference)當(dāng)一個(gè)對(duì)象只有弱引用存在時(shí),隨時(shí)可能被系統(tǒng)回收,弱爆了
LruCache是一個(gè)泛型類,他內(nèi)部采用了一個(gè)LinkedHashMap以強(qiáng)引用的方式存儲(chǔ)外界的緩存對(duì)象,其提供了get和put方法來進(jìn)行緩存的獲取和存儲(chǔ),當(dāng)緩存滿時(shí),LruCache會(huì)移除最近最少使用的對(duì)象,然后再添加新的對(duì)象。
LruCache是線程安全的
public class LruCache<K,V>{
private final LinkedHashMap<K,V> map;
...
}
1、 LruCache的創(chuàng)建
下面這個(gè)代碼簡(jiǎn)單說明了LruCache的初始化過程:
int maxMemory = (int)(Runtime.getRuntime().maxMemory)/1024);
int cacheSize = maxMemory/8;
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key,Bitmap bitmap){
return bitmap.getRowBytes()*bitmap.getHeight()/1024;
}
}
從過上面的代碼我們知道,其實(shí)我們需要提供的只是緩存的容量總大小和重寫sizeOf()方法即可。
- siseOf方法的作用是計(jì)算 緩存對(duì)象的大小。(這里的單位規(guī)格必須要和總?cè)萘恳恢保y(tǒng)一為KB比較好)
需要注意的一點(diǎn)是,LruCache移除舊的緩存的時(shí)候會(huì)調(diào)用entryRemoved方法,如果需要的話可以在entryRemoved方法里面做一些相關(guān)的操作。
補(bǔ)充一下的Runtime的相關(guān)知識(shí):
java.lang.Object.Runtime官方API
- Runtime.getRuntime().maxMemory 當(dāng)先虛擬機(jī)給每個(gè)進(jìn)程分配最大限額的內(nèi)存
- Runtime.getRuntime().totalMemory 當(dāng)前進(jìn)程已經(jīng)占用過的內(nèi)存
- Runtime.getRuntime().freeMemory 當(dāng)前申請(qǐng)過又沒用上空閑下來的可用內(nèi)存
這三者返回的單位就是byte,除以1024得出kb,接著再除以1024,得出m
下面是新建一個(gè)程序調(diào)用者三個(gè)方法的輸出結(jié)果(單位轉(zhuǎn)成M)
10-25 03:50:39.584 2137-2137/com.example.amqr.myapplication D/Me: maxMemory:32
10-25 03:50:39.584 2137-2137/com.example.amqr.myapplication D/Me: freeMemory:2
10-25 03:50:39.584 2137-2137/com.example.amqr.myapplication D/Me: totalMemory:5
談?wù)凴untime類中的freeMemory,totalMemory,maxMemory幾個(gè)方法
經(jīng)過上面的這一個(gè)分析,我們應(yīng)該比較清楚上面那段關(guān)于LruCache的代碼的含義了。
就是我們把我們當(dāng)前安卓app所能申請(qǐng)到的最大內(nèi)存(maxMemory)的8分之1來用作內(nèi)存緩存。這個(gè)就是我們指定的內(nèi)存緩存的容器的大小,大小是自己指定的。
2、 LruCache的 存 取 刪
存儲(chǔ)
mMemoryCache.get(key)
獲取
mMemoryCache.put(key,bitmap)
刪除
mMemory.remove(key)
3、DiskLruCache類
DiskLruCache是用來做存儲(chǔ)設(shè)備緩存的(區(qū)別于內(nèi)存緩存),也就是磁盤緩存。
DiskLruCache源碼
3.1.a、DiskLruCache的創(chuàng)建
DiskLruCache不能通過構(gòu)造方法創(chuàng)建,他提供了Open方法用來創(chuàng)建自身,如下所示:
/**
第一個(gè)參數(shù):緩存文件的存放路徑(重要)
第二個(gè)參數(shù):一般寫 1 就好
第二個(gè)參數(shù): 一般寫 1 就好
第四個(gè)參數(shù):存放緩存文件的容器的容量大小(重要)
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
一個(gè)完整的open方法
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory a writable directory
* @param appVersion
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
* @throws java.io.IOException if reading or writing the cache directory fails
*/
**public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)**
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// prefer to pick up where we left off
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
3.1.b、open的四個(gè)方法進(jìn)行分析:
第一個(gè)參數(shù):File directory
表示磁盤緩存的存儲(chǔ)路徑,緩存路徑可以選擇的SD卡作為緩存路徑,具體是指
/sdcard/Android/data/包名/cache 目錄
為什么選擇這個(gè)目錄,因?yàn)橹付檫@個(gè)目錄時(shí),當(dāng)應(yīng)用被卸載的時(shí)候,這個(gè)文件夾的內(nèi)容會(huì)被一起刪除掉。當(dāng)然我們要是不想這么干的話可以指定的我們自己想要的路徑。
第二個(gè)參數(shù) int appVerasion
表示的應(yīng)用的版本號(hào),這個(gè)一般來說寫 1 就好。
這個(gè)參數(shù)的作用是當(dāng)檢測(cè)到版本號(hào)發(fā)生改變的實(shí)惠,就把緩存文件清空,但是這個(gè)特性在實(shí)際開發(fā)中并不實(shí)用,因?yàn)榘l(fā)現(xiàn)很多時(shí)候就算我們的版本號(hào)寫著發(fā)生改變了緩存文件仍然是有限的,所以一般這個(gè)我們寫1就好
第三個(gè)參數(shù) int valueCount
這個(gè)還是一般都寫 1 就好。
這個(gè)表示單個(gè)節(jié)點(diǎn)對(duì)應(yīng)的數(shù)據(jù)的個(gè)數(shù)。
第四個(gè)節(jié)點(diǎn) 存放緩存文件的總空間的大小
比如我們?cè)O(shè)定為50M,那么當(dāng)緩存文件超出50M之后DiskLruCache就會(huì)自動(dòng)根據(jù)算法刪除一些緩存文件,騰出位置給新的緩存文件存放。
知道了這四個(gè)參數(shù)的作用之后,一個(gè)典型的DiskLruCache應(yīng)該這么創(chuàng)建:
private static final long DISK_CACHE_SIZE= 1024 *1024 * 50; // 50M
try {
File cacheDir = getDiskCacheDir(context, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
3.2、DiskLruCache的存儲(chǔ)(緩存添加)
DiskLruCache 的緩存添加是通過Editor完成的。
Editor表示一個(gè)緩存對(duì)象的編輯對(duì)象,我們這里以圖片緩存為例子,首先需要獲取圖片的url所對(duì)應(yīng)的key,然后根據(jù)key就可以通過edit()來獲取Editor對(duì)象。如果這個(gè)緩存正在被編輯,那么edit()就會(huì)返回null,也就是說, DiskLruCache不允許同時(shí)編輯一個(gè)緩存對(duì)象
圖片的url不能只為作為key(因?yàn)閡rl往往含有亂碼),一般都是以圖片的url的MD5的值作為key
3.2.a、將圖片url的MD5值作為緩存的key
將url轉(zhuǎn)換為MD5的代碼:
// 傳入圖片url,返回將圖片的url的MD5的值(這個(gè)返回值的將作為存儲(chǔ)緩存的 key)
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
// 返回將作為緩存的key值
return cacheKey;
}
// 將byte[]數(shù)組轉(zhuǎn)為字符串的的方法
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();
}
3.2.b、DiskLruCache的可用的Editor對(duì)象,利用的Editor獲得輸入流將文件寫入緩存目標(biāo)地址
將url轉(zhuǎn)成key之后,就可以捕獲的Editor對(duì)象了(前提是當(dāng)前不存愛其他Editor對(duì)象)。
我們獲得這個(gè)Editor對(duì)象的目的就是想要拿到一個(gè)輸出流。
注意: 由于前面的我們?cè)趏pen方法中參數(shù)的第三個(gè)參數(shù)里面設(shè)置一個(gè)節(jié)點(diǎn)只能對(duì)應(yīng)一個(gè)數(shù)據(jù),所以啊,在輸入流的參數(shù) DISK_CACHE_INDEX 中我們把常量設(shè)置為 0 ,如下所示:
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
得到輸入流了,我們的目標(biāo)就是把圖片寫入緩存目錄,這是時(shí)候我們來看一下:下載圖片并得寫入文件系統(tǒng)的方法:
public 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(),
IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
經(jīng)過上面的步驟,我們還差一步就可以真正的將圖片寫入到緩存的文件系統(tǒng)了,還差哪一步 —— editor.commit()
通過commit來提交寫入操作,如果圖片下載的過程發(fā)生了異常,那么還可以通過Editor的abort()來回退整個(gè)操作,具體代碼如下:
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
DiskLruCache進(jìn)行 緩存添加 小結(jié)
- 第一步,將圖片的url的MD5值作為緩存的key
- 第二步,得到key之后獲得Editor對(duì)象獲得輸出流
- 第三步,commit最終確定把圖片寫入到緩存,如果下載試下那么就利用abort進(jìn)行回退
3.3、DiskLruCache的緩存查找 (獲取緩存)
緩存查找的過程(和DiskLruCache的寫入相似):
- 第一步,將圖片的url的MD5值作為緩存查找的的key
- 第二步,利用DiskLruCache的get方法得到一個(gè)Snapshot對(duì)象今兒得到文件輸入流
- 得到輸入流了,自然可以得到Bitmap對(duì)象了
注意:
為了避免OOM,我們一般都不直接加載原圖,都會(huì)利用BitmapFactory.Options來加載一張合適分辨率的圖片,但是這個(gè)方式針對(duì)特殊的FileInputStream卻行不通,因?yàn)镕ileInputStream是一種有序的文件流,而兩次的decodeStream調(diào)用影響了文件流的位置屬性,導(dǎo)致了第二次decodeStream時(shí)得到的是null,為了解決這個(gè)問題,可以通過文件流來得到他所對(duì)應(yīng)的 文件描述符 ,然后在通過 BitmapFactory.decodeFileDescriptor方法來加載一張縮放過的圖片。
過程如下所示:
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
3.4、緩存的刪除
主要涉及兩個(gè)方法,remove和delete。
remove方法,指定刪除哪一個(gè)緩存
現(xiàn)在已經(jīng)是老油條了
第一步,獲取url的MD5,作為key
第二步,調(diào)用 remove 方法,進(jìn)行刪除
大概代碼如下:
try {
String imageUrl = "http:XXX.jpg";
String key = hashKeyForDisk(imageUrl);
mDiskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
delete()方法,刪除所有緩存
這個(gè)方法用于將所有的緩存數(shù)據(jù)全部刪除,比如說網(wǎng)易新聞中的那個(gè)手動(dòng)清理緩存功能,其實(shí)只需要調(diào)用一下DiskLruCache的delete()方法就可以實(shí)現(xiàn)了。
4、緩存機(jī)制的應(yīng)用
4.1、 簡(jiǎn)單的圖片加載框架(ImageLoader)的打造
一個(gè)簡(jiǎn)潔的ImageLoader小圖片加載框架,主要參考《Android開發(fā)藝術(shù)探索》一書。
4.1.a、ImageLoader應(yīng)該具備的功能
- 一個(gè)相對(duì)優(yōu)秀的開發(fā)圖片框架應(yīng)該具備如下功能:
- 1、同步加載
- 2、異步加載
- 3、圖片的壓縮
- 4、二級(jí)緩存(內(nèi)存緩存、磁盤緩存、網(wǎng)絡(luò)拉取)
同步加載:指圖片能夠以同步的方式想調(diào)用者提供圖片,至于提供的方式,可能是內(nèi)存緩存,可能是磁盤緩存,也可能是網(wǎng)絡(luò)拉取。
異步加載:異步是安卓相當(dāng)重要的一個(gè)操作,我們不能讓主線程干太多事,耗時(shí)的操作柏旭交給異步的去做,比如加載圖片這種事情是耗時(shí)的,所以我們的加載圖片的耗時(shí)操作最好交給異步來執(zhí)行。
壓縮:壓縮沒什么好說的,就是利用Options計(jì)算inSimpleSize
至于二級(jí)緩存,肯定就是一個(gè)圖片框架的核心了
圖片框架的注意的地方:復(fù)用,不管是ListView還是GridView,復(fù)用無疑是一個(gè)很好地設(shè)計(jì),但是在圖片加載框架的里面就是一個(gè)需要特別思考對(duì)待的問題,比如情況是這樣子的,ListView的itemA正在加載一張圖片,這個(gè)時(shí)候突然用戶快速滑動(dòng)屏幕,那么很可能itemB就復(fù)用上了A的圖片,其實(shí)圖片A不是itemB想要的,itemB明顯是要顯示圖片B的。
4.1.b、抽出壓縮類 ImageResizer,
專門用來計(jì)算合適的inSimpleSize,合理地壓縮圖片,減少內(nèi)存壓力
這個(gè)類主要由兩個(gè)方法:
- decodeSampledBitmapFromResource
- decodeSampledBitmapFromFileDescriptor
import java.io.FileDescriptor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
// 圖片壓縮類
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
// 傳入資源文件,計(jì)算合適的大小,返回一個(gè)Bitmap對(duì)象
public Bitmap decodeSampledBitmapFromResource(Resources res,
int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
為了加載文件系統(tǒng)的文件避免二次decodeStream返回null而專門調(diào)整的方法decode方法
解釋:
FileInputStream是一種有序的文件流,而兩次的decodeStream調(diào)用影響了文件流的位置屬性,
導(dǎo)致了第二次decodeStream時(shí)得到的是null,為了解決這個(gè)問題,可以通過文件流來得到他所對(duì)應(yīng)的文件描述符,
然后在通過 BitmapFactory.decodeFileDescriptor方法來加載一張縮放過的圖片
*/
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
}
4.1.c、內(nèi)存緩存和磁盤緩存的實(shí)現(xiàn)
在構(gòu)造函數(shù)里面對(duì)LruCache和DiskLruCache進(jìn)行創(chuàng)建
圖片的框架的主要實(shí)現(xiàn)功能的類 ImageLoader類 的構(gòu)造方法進(jìn)就行對(duì) LruCache 和 DiskLruCache 的 初始化
構(gòu)造方法如下:
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
內(nèi)存緩存空間:這里我們指定為系統(tǒng)分配給app最大的內(nèi)存的八分之一,這里要怎么分配都行,不太大太夸張就行。
磁盤緩存空間:這里我們?cè)O(shè)定為50M,這是當(dāng)系統(tǒng)磁盤內(nèi)存足夠多情況。
但是當(dāng)系統(tǒng)磁盤內(nèi)存不足的時(shí)候,這個(gè)DiskLruCache就會(huì)失效,因?yàn)橄到y(tǒng)剩余 的內(nèi)存空間比我們想要申請(qǐng)的還小。當(dāng)然,我們也可以指定為20M或者30M,都行,看實(shí)際情況。
內(nèi)存緩存的 存 和 取
內(nèi)存的緩存的 存 和 取 相對(duì)比較簡(jiǎn)單
// 內(nèi)存緩存 的 添加
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
// 內(nèi)存緩存的 獲取
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
磁盤緩存的 存 和 取
這個(gè)相對(duì)特殊一些,說明一下:
**存(添加):loadBitmapFormHttp**
磁盤緩存的添加需要通過Editor來完成,Editor利用commit和abort方法來提交和撤銷文件系統(tǒng)的寫入操作,我們定義了loadBitmapFormHttp方法
**取(獲取): loadBitmapFormDiskCache**
磁盤的讀取需要通過 Snapshot 來完成,通過 Snapshot 磁盤緩存對(duì)象對(duì)應(yīng)的 FileInputStream,但是 FileInputStream 無法邊界地挖寶從各部分壓縮,所以我們利用專門在壓縮類里面專門為其打造的 FileDescriptor 來加載壓縮圖片,最后成功獲取Bitmap。
代碼呈上:
// 添加圖片到 磁盤緩存 。(從網(wǎng)絡(luò)加載后,添加緩存到 磁盤緩存)
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
// 獲取圖片,從磁盤緩存處
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth,
int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
關(guān)于圖片緩存就寫到這里了
如果對(duì)圖片緩存的實(shí)現(xiàn)例子有興趣可以查看接著查看這篇文章:
造簡(jiǎn)單的圖片加載框架——ImageLoader的實(shí)現(xiàn)
本篇完。
相關(guān)參考:
《Android開發(fā)藝術(shù)探索》一書