三級(jí)緩存概覽引入
我們之所以要做緩存,主要是為了提高效率,節(jié)省流量。但是為什么要做三級(jí)呢?為什么不只存在內(nèi)存或者只存在文件中呢?這是因?yàn)閮?nèi)存的讀取速度快,但是容易被回收,容量小,文件的讀取速度次之,不過容量大,不到不得已不會(huì)被回收。
一般來說,我們首次加載圖片,內(nèi)存和文件是沒有緩存的,這樣我們需要從網(wǎng)絡(luò)加載,加載完成后,我們會(huì)存到內(nèi)存和文件中去;當(dāng)再次加載圖片的時(shí)候,我們會(huì)先查找內(nèi)存有沒有,如果有就直接顯示內(nèi)存中的圖片,如果沒有,我們會(huì)接著查找文件中是否有,如果文件中有,我們會(huì)顯示文件中的圖片,并且把它存到內(nèi)存中去,這樣下次我們?cè)趦?nèi)存中就能找到它了。
三級(jí)緩存概念圖 :

- 三級(jí)緩存出現(xiàn)原因:
假如每次啟動(dòng)的時(shí)候都從網(wǎng)絡(luò)拉取圖片的話,勢必會(huì)消耗很多流量。在當(dāng)前的狀況下,對(duì)于非wifi用戶來說,流量還是很貴的,一個(gè)很耗流量的應(yīng)用,其用戶數(shù)量級(jí)肯定要受到影響
-
三級(jí)緩存是什么:
a,內(nèi)存緩存 優(yōu)先加載 速度最快 一級(jí)緩存
b,磁盤緩存優(yōu)先加載速度稍快 二級(jí)緩存
c,網(wǎng)絡(luò)緩存最后加載速度由網(wǎng)絡(luò)速度決定(浪費(fèi)流量)三級(jí)緩存
緩存原理
一級(jí)緩存:內(nèi)存緩存
當(dāng)加載網(wǎng)絡(luò)圖片時(shí),首先會(huì)從內(nèi)存緩存中,查找圖片:
有:直接顯示圖片
沒有:從二級(jí)緩存中查找圖片
二級(jí)緩存:磁盤緩存
當(dāng)一級(jí)緩存中沒有目標(biāo)圖片時(shí),從二級(jí)緩存中查找圖片 :
有:先保存內(nèi)存緩存中,然后顯示圖片.
沒有:從三級(jí)緩存中獲取圖片.
三級(jí)緩存:網(wǎng)絡(luò)緩存
當(dāng)二級(jí)緩存中也沒有目標(biāo)圖片時(shí),從網(wǎng)絡(luò)緩存中查找圖片 :
有,先變?yōu)榇疟P緩存,再變?yōu)閮?nèi)存緩存, 最后顯示圖片.
沒有,先網(wǎng)絡(luò)獲取圖片,然后磁盤緩存,內(nèi)存緩存,顯示圖片
三級(jí)緩存的實(shí)現(xiàn):
(1) 網(wǎng)絡(luò)緩存
要實(shí)現(xiàn)網(wǎng)絡(luò)緩存,這里面一定涉及到異步加載網(wǎng)絡(luò)圖片。
異步加載網(wǎng)絡(luò)圖片常見的有兩種形式:
第一種形式就是Thread + handler機(jī)制,來完成,
第二種形式就是android的SDK提供的異步工具類,AsyncTask來實(shí)現(xiàn)。
這里我們選擇AsyncTask來異步加載圖片,因?yàn)樗鼉?nèi)部維護(hù)了線程池,可以實(shí)現(xiàn)并發(fā)的加載圖片.
【NetCacheUtils】類具體代碼如下:
package com.xyxy.cache;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* 2018/6/7.
* 三級(jí)緩存之: 網(wǎng)絡(luò)緩存
*/
public class NetCacheUtils {
private static final String TAG = "NetCacheUtils";
private Context context;
//從網(wǎng)絡(luò)獲取圖片
public void getBitmapFromNet(Context context, ImageView iv, String url){
this.context = context;
//讓ImageView和url關(guān)聯(lián)起來: 解決圖片錯(cuò)位的bug
iv.setTag(url);
// 創(chuàng)建內(nèi)部類 BitmapTask 去實(shí)現(xiàn)異步加載網(wǎng)絡(luò)圖片
new BitmapTask().execute(iv,url);
}
//異步任務(wù)
class BitmapTask extends AsyncTask<Object,Void,Bitmap> {
// 定義放圖片的容器和網(wǎng)絡(luò)鏈接
private ImageView iv;
private String url;
@Override
protected Bitmap doInBackground(Object... params) {
//獲取參數(shù)
iv = (ImageView) params[0];
url = (String) params[1];
//下載圖片: 抽取下載圖片的方法downloadBitmap()
Bitmap bitmap = downloadBitmap(url);
// MyLogger是一個(gè)自定義的日志工具類(附件會(huì)提供)
// MyLogger.i(TAG,"從網(wǎng)絡(luò)上加載了圖片");
//將加載的網(wǎng)絡(luò)圖片緩存到磁盤和內(nèi)存中:
// 再緩存到內(nèi)存中(MemoryCacheUtils后面會(huì)實(shí)現(xiàn))
MemoryCacheUtils.saveCache(bitmap,url);
// 先緩存到磁盤(LocalCacheUtils后面會(huì)實(shí)現(xiàn))
LocalCacheUtils.saveCache(context,bitmap,url);
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
//獲取ImageView對(duì)應(yīng)的url
String url = (String) iv.getTag();
// 只有是同一張圖片時(shí)才顯示,防止圖片錯(cuò)位
if(bitmap != null && this.url.equals(url)){
iv.setImageBitmap(bitmap);
}
}
}
//下載圖片的方法(利用IO流)
private Bitmap downloadBitmap(String url) {
Bitmap bitmap = null;
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(3000);
conn.setReadTimeout(6000);
conn.connect();
int responseCode = conn.getResponseCode();
if(responseCode == 200){
InputStream inputStream = conn.getInputStream();
//把流轉(zhuǎn)換成Bitmap對(duì)象
bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(conn != null){
conn.disconnect();
}
}
return bitmap;
}
}
小結(jié):
定義異步任務(wù),并實(shí)現(xiàn)兩個(gè)方法:
doInBackground:執(zhí)行在子線程中的方法,作用:網(wǎng)絡(luò)加載圖片
onPostExecute:執(zhí)行在UI線程中的方法,作用:更新UI,顯示圖片
定義網(wǎng)絡(luò)加載圖片:該邏輯執(zhí)行在doInBackground方法中。
綁定UI,顯示圖片:該邏輯執(zhí)行在onPostExecute方法中。
備注:
AsyncTask 定義的三種泛型類型 參數(shù):Params,Progress 和 Result的解釋
Params: 啟動(dòng)任務(wù)執(zhí)行的輸入?yún)?shù), 比如HTTP 請(qǐng)求的 URL。
Progress: 后臺(tái)任務(wù)執(zhí)行的百分比。
Result: 后臺(tái)執(zhí)行任務(wù)最終返回的結(jié)果,比如String。
(2) 磁盤緩存
接下來實(shí)現(xiàn)磁盤緩存:
磁盤緩存包含兩部內(nèi)容:
1)寫磁盤:
如何將一個(gè)bitmap寫入到磁盤中?
可以通過bitmap.compress(Bitmap.CompressFormat.JPEG,100,stream);
2)讀磁盤
可以通過BitmapFactory.decodeFile(file.getAbsolutePath());讀數(shù)據(jù)
【LocalCacheUtils.java】工具類,具體代碼如下:
package com.xyxy.cache;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
/**
* Created by on 2018/6/7
* 磁盤緩存(數(shù)據(jù)存放在本應(yīng)用程序的cache目錄下的Local_cache目錄)
*/
public class LocalCacheUtils {
//寫緩存
public static void saveCache(Context context, Bitmap bitmap, String url){
//緩存目錄
File dir = new File(context.getCacheDir(),"local_cache");
if(!dir.exists()){
dir.mkdirs();
}
//把圖片緩存在緩存目錄
// 此處的Md5Utils是一個(gè)生成md5文件的工具類(附件會(huì)提供)
File file = new File(dir, Md5Utils.encode(url));
FileOutputStream stream = null;
try {
stream = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 核心代碼
bitmap.compress(Bitmap.CompressFormat.JPEG,100,stream);
}
//讀緩存
public static Bitmap readCache(Context context,String url){
File dir = new File(context.getCacheDir(),"local_cache");
if(!dir.exists()){
return null;
}
File file = new File(dir, Md5Utils.encode(url));
if(!file.exists()){
return null;
}
// 核心代碼
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
//重要操作: 把數(shù)據(jù)緩存在內(nèi)存中
// MemoryCacheUtils馬上講到
MemoryCacheUtils.saveCache(bitmap,url);
return bitmap;
}
}
小結(jié) :
借助md5工具類將圖片url轉(zhuǎn)化為圖片名稱,通過IO流將數(shù)據(jù)寫到磁盤中.
通過url獲取圖片名稱,從磁盤中讀取數(shù)據(jù)使用 DiskDataCacher是一個(gè)輕量級(jí)的Android磁盤緩存工具,基于LRU算法實(shí)現(xiàn),同時(shí)可以設(shè)置緩存有效期,使用起來十分方便。
用于緩存網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù),并且可以設(shè)置緩存數(shù)據(jù)的有效期,比如,緩存時(shí)間假設(shè)為1個(gè)小時(shí),超時(shí)1小時(shí)后再次獲取緩存會(huì)自動(dòng)失效,讓客戶端重新請(qǐng)求新的數(shù)據(jù),這樣
可以減少客戶端流量,同時(shí)減少服務(wù)器并發(fā)量。
用于代替SharePreference當(dāng)做配置文件,緩存一些較大的配置數(shù)據(jù),效率更高,可以減少內(nèi)存消耗。SharePreference不能用來緩存較大數(shù)據(jù)的理由:請(qǐng)不要濫用
SharedPreference
- 支持?jǐn)U展,擴(kuò)展后可以緩存
JsonObject、Bitmap、Drawable和序列化的java對(duì)象等等
(3) 內(nèi)存緩存
接下來我們實(shí)現(xiàn)內(nèi)存緩存, 內(nèi)存緩存同樣也包含兩部分內(nèi)容:
1)寫入內(nèi)存: 可以通過HashMap,把對(duì)應(yīng)的數(shù)據(jù)進(jìn)行保存
Key:對(duì)應(yīng)的url
Value:對(duì)應(yīng)的bitmap
- 從內(nèi)存讀 從HashMap中取, 通過key取出對(duì)應(yīng)bitmap
【MemoryCacheUtils】工具類 , 具體代碼如下:
package com.xyxy.cache;
import android.graphics.Bitmap;
import android.util.LruCache;
/**
* Created by on 2018/6/7.
* 內(nèi)存緩存:通過HashMap來進(jìn)行數(shù)據(jù)
* <p>
* 的存儲(chǔ)
*/
public class MemoryCacheUtils {
static {
//caches = new HashMap<>();
//Android虛擬機(jī)的內(nèi)存只有16M,易產(chǎn)生OOM異常(內(nèi)存溢出)
//java語言提供了另外一種機(jī)制:軟引用、弱引用、虛引用
//軟引用:當(dāng)虛擬機(jī)內(nèi)存不足的時(shí)候,回收軟引用的對(duì)象
//弱引用:當(dāng)對(duì)象沒有應(yīng)用的時(shí)候,馬上回收
//虛引用 :任何情況下都可能回收
//java默認(rèn)的數(shù)據(jù)類型是強(qiáng)引用類型
//caches = new HashMap<>();
//因?yàn)閺?Android 2.3 (API Level 9)開始,
//垃圾回收器會(huì)更傾向于回收持有軟引用或弱引用的對(duì)象,
//這讓軟引用和弱引用變得不再可靠。
//google官方推薦我們使用這樣一種緩存機(jī)制:LruCache.
//LruCache lru:least recently used 最近最少使用的算法
//A
//B
//C(最近使用的最少 優(yōu)先被回收)
//B
//A
long maxMemory = Runtime.getRuntime().maxMemory();
int cacheSize = (int) (maxMemory / 8);
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
// 1. 創(chuàng)建LruCache對(duì)象
private static LruCache<String, Bitmap> lruCache;
private static final String TAG = "TextDownload";
//寫緩存
public static void saveCache(Bitmap bitmap, String url) {
// 4. 使用put()方法緩存對(duì)象
if (readCache(url) == null) {
lruCache.put(url, bitmap);
}
}
// 從緩存中刪除指定的Bitmap
//讀緩存
public static Bitmap readCache(String url) {
// 5. 使用get()方法取出對(duì)象
return lruCache.get(url);
}
}
小結(jié) :
- 剛開始我是通過一個(gè)HashMap來緩存數(shù)據(jù)的,當(dāng)緩存大量的圖片時(shí),就會(huì)導(dǎo)致應(yīng)用程序出現(xiàn)內(nèi)存溢出OOM的情況。屬于強(qiáng)引用狀態(tài).
- 為了解決這樣的問題,我將緩存的bitmap對(duì)象,由原來的強(qiáng)引用狀態(tài)變?yōu)檐浺脿顟B(tài)。當(dāng)內(nèi)存不足時(shí),回收軟用的bitmap對(duì)象。
- 我們通過軟引用方式,來解決內(nèi)存溢出的情況,是可以的!但是代碼中我有提到,隨著android系統(tǒng)的升級(jí)和更新,垃圾回收器會(huì)更傾向于回收持有軟引用或弱引用的對(duì)象. 因此,持有軟引用對(duì)象變得不太可靠了。
- 最后確定的是google官方推薦我們使用這樣一種緩存機(jī)制:LruCache
什么是LruCache??
- 使用最近最少算法機(jī)制。來緩存每個(gè)對(duì)象的。
- 把近期最少使用的數(shù)據(jù)從緩存中移除,保留使用最頻繁的數(shù)據(jù)
- LruCache內(nèi)存維護(hù)一個(gè)LinkedHashMap,來維護(hù)每個(gè)緩存對(duì)象
- 從LruCache取出對(duì)象,它會(huì)把當(dāng)前使用的對(duì)象進(jìn)行移動(dòng)到LinkedHashMap尾端
- 如果添加一個(gè)緩存對(duì)象,LruCache會(huì)把當(dāng)前對(duì)象也放在LinkedHashMap尾端
- 在LinkedHashMap的頂端就是最近最少使用的緩存對(duì)象。也就是被移除的對(duì)象了。
LruCache使用:
- 構(gòu)建LruCache對(duì)象,同時(shí)指定內(nèi)存緩存大小
- 重寫內(nèi)部的sizeOf方法,計(jì)算每個(gè)圖片的大小
- 使用LruCache的put方法緩存對(duì)象
- 使用LruCache的get方法取出對(duì)象
- 緩存在Android的Data data目錄下
講到LruCache不得不提一下LinkedHashMap,因?yàn)長ruCache中Lru算法的實(shí)現(xiàn)就是通過LinkedHashMap來實(shí)現(xiàn)的。LinkedHashMap繼承于HashMap,它使用了一個(gè)雙向鏈表來存儲(chǔ)Map中的Entry順序關(guān)系,這種順序有兩種,一種是LRU順序,一種是插入順序,這可以由其構(gòu)造函數(shù)public LinkedHashMap(int initialCapacity,float loadFactor, boolean accessOrder)指定。所以,對(duì)于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,還做些調(diào)整Entry順序鏈表的工作。LruCache中將LinkedHashMap的順序設(shè)置為LRU順序來實(shí)現(xiàn)LRU緩存,每次調(diào)用get(也就是從內(nèi)存緩存中取圖片),則將該對(duì)象移到鏈表的尾端。調(diào)用put插入新的對(duì)象也是存儲(chǔ)在鏈表尾端,這樣當(dāng)內(nèi)存緩存達(dá)到設(shè)定的最大值時(shí),將鏈表頭部的對(duì)象(近期最少用到的)移除。
最終用于使用的 三級(jí)緩存框架 搭建
結(jié)合上面創(chuàng)建的三個(gè)工具類, 實(shí)現(xiàn)類似Picasso功能的框架:
【BitmapUtils】類, 具體代碼如下:
package com.xyxy.cache;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.widget.ImageView;
/**
* Created by Administrator on 2018/6/7.
* 圖片三級(jí)緩存框架
*/
public class BitmapUtils {
private static final String TAG = "BitmapUtils";
static {
netCacheUtils = new NetCacheUtils();
localCacheUtils = new LocalCacheUtils();
memoryCacheUtils = new MemoryCacheUtils();
}
private static NetCacheUtils netCacheUtils;
private static LocalCacheUtils localCacheUtils;
private static MemoryCacheUtils memoryCacheUtils;
//顯示圖片
public static void display(Context context, ImageView iv, String url) {
Bitmap bitmap = null;
//內(nèi)存緩存
bitmap = memoryCacheUtils.readCache(url);
if (bitmap != null) {
iv.setImageBitmap(bitmap);
Log.e(TAG, "從內(nèi)存獲取了圖片");
return;
}
//磁盤緩存
bitmap = localCacheUtils.readCache(context, url);
if (bitmap != null) {
iv.setImageBitmap(bitmap);
Log.e(TAG, "從磁盤獲取了圖片");
return;
}
//網(wǎng)絡(luò)緩存
netCacheUtils.getBitmapFromNet(context, iv, url);
}
}
小結(jié) :
此處只提供了一個(gè)顯示網(wǎng)絡(luò)圖片的方法 display(), 只需要傳入一下三個(gè)參數(shù)即可:
Context context : 上下文
ImageView iv: 存放圖片的容器
String url: 圖片的網(wǎng)絡(luò)鏈接
【Md5Utils】生成md5文件工具類:
package com.xyxy.picture.aaaaaaa;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Utils {
public static String encode(String password){
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] result = digest.digest(password.getBytes());
StringBuffer sb = new StringBuffer();
for(byte b : result){
int number = (int)(b & 0xff) ;
String str = Integer.toHexString(number);
if(str.length()==1){
sb.append("0");
}
sb.append(str);
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}
}
版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載http://www.itdecent.cn/writer#/notebooks/26335980/notes/29184868/preview