圖片加載的三級(jí)緩存

三級(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í)緩存流程圖
  • 三級(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、BitmapDrawable和序列化的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

  1. 從內(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é) :

  1. 剛開始我是通過一個(gè)HashMap來緩存數(shù)據(jù)的,當(dāng)緩存大量的圖片時(shí),就會(huì)導(dǎo)致應(yīng)用程序出現(xiàn)內(nèi)存溢出OOM的情況。屬于強(qiáng)引用狀態(tài).
  2. 為了解決這樣的問題,我將緩存的bitmap對(duì)象,由原來的強(qiáng)引用狀態(tài)變?yōu)檐浺脿顟B(tài)。當(dāng)內(nèi)存不足時(shí),回收軟用的bitmap對(duì)象。
  3. 我們通過軟引用方式,來解決內(nèi)存溢出的情況,是可以的!但是代碼中我有提到,隨著android系統(tǒng)的升級(jí)和更新,垃圾回收器會(huì)更傾向于回收持有軟引用或弱引用的對(duì)象. 因此,持有軟引用對(duì)象變得不太可靠了。
  4. 最后確定的是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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容