图片加载的三级缓存

三级缓存概览引入

我们之所以要做缓存,主要是为了提高效率,节省流量。但是为什么要做三级呢?为什么不只存在内存或者只存在文件中呢?这是因为内存的读取速度快,但是容易被回收,容量小,文件的读取速度次之,不过容量大,不到不得已不会被回收。
一般来说,我们首次加载图片,内存和文件是没有缓存的,这样我们需要从网络加载,加载完成后,我们会存到内存和文件中去;当再次加载图片的时候,我们会先查找内存有没有,如果有就直接显示内存中的图片,如果没有,我们会接着查找文件中是否有,如果文件中有,我们会显示文件中的图片,并且把它存到内存中去,这样下次我们在内存中就能找到它了。

三级缓存概念图 :

三级缓存流程图
  • 三级缓存出现原因:

假如每次启动的时候都从网络拉取图片的话,势必会消耗很多流量。在当前的状况下,对于非wifi用户来说,流量还是很贵的,一个很耗流量的应用,其用户数量级肯定要受到影响

  • 三级缓存是什么:

    a,内存缓存 优先加载 速度最快 一级缓存

    b,磁盘缓存优先加载速度稍快 二级缓存

    c,网络缓存最后加载速度由网络速度决定(浪费流量)三级缓存

缓存原理

一级缓存:内存缓存

当加载网络图片时,首先会从内存缓存中,查找图片:
有:直接显示图片
没有:从二级缓存中查找图片

二级缓存:磁盘缓存

当一级缓存中没有目标图片时,从二级缓存中查找图片 :
有:先保存内存缓存中,然后显示图片.
没有:从三级缓存中获取图片.

三级缓存:网络缓存

当二级缓存中也没有目标图片时,从网络缓存中查找图片 :
有,先变为磁盘缓存,再变为内存缓存, 最后显示图片.
没有,先网络获取图片,然后磁盘缓存,内存缓存,显示图片

三级缓存的实现:

(1) 网络缓存

要实现网络缓存,这里面一定涉及到异步加载网络图片。
异步加载网络图片常见的有两种形式:
第一种形式就是Thread + handler机制,来完成,
第二种形式就是android的SDK提供的异步工具类,AsyncTask来实现。
这里我们选择AsyncTask来异步加载图片,因为它内部维护了线程池,可以实现并发的加载图片.

【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.
 *  三级缓存之: 网络缓存
 */
public class NetCacheUtils {
private static final String TAG = "NetCacheUtils";
private Context context;
//从网络获取图片
public void getBitmapFromNet(Context context, ImageView iv, String url){
    this.context = context;
    //让ImageView和url关联起来: 解决图片错位的bug
    iv.setTag(url);

    // 创建内部类 BitmapTask 去实现异步加载网络图片
    new BitmapTask().execute(iv,url);
}

//异步任务
class BitmapTask extends AsyncTask<Object,Void,Bitmap> {
// 定义放图片的容器和网络链接
    private ImageView iv;
    private String url;

    @Override
    protected Bitmap doInBackground(Object... params) {
        //获取参数
        iv = (ImageView) params[0];
        url = (String) params[1];

        //下载图片: 抽取下载图片的方法downloadBitmap()
        Bitmap bitmap = downloadBitmap(url);
        // MyLogger是一个自定义的日志工具类(附件会提供)
//            MyLogger.i(TAG,"从网络上加载了图片");

        //将加载的网络图片缓存到磁盘和内存中:
        // 再缓存到内存中(MemoryCacheUtils后面会实现)
        MemoryCacheUtils.saveCache(bitmap,url);
        // 先缓存到磁盘(LocalCacheUtils后面会实现)
        LocalCacheUtils.saveCache(context,bitmap,url);

        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        //获取ImageView对应的url
        String url = (String) iv.getTag();
        // 只有是同一张图片时才显示,防止图片错位
        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();
            //把流转换成Bitmap对象
            bitmap = BitmapFactory.decodeStream(inputStream);
            return bitmap;
        }
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if(conn != null){
            conn.disconnect();
        }
    }
    return bitmap;
  }

 }

小结:

定义异步任务,并实现两个方法:
doInBackground:执行在子线程中的方法,作用:网络加载图片
onPostExecute:执行在UI线程中的方法,作用:更新UI,显示图片
定义网络加载图片:该逻辑执行在doInBackground方法中。
绑定UI,显示图片:该逻辑执行在onPostExecute方法中。

备注:

AsyncTask 定义的三种泛型类型 参数:Params,Progress 和 Result的解释
Params: 启动任务执行的输入参数, 比如HTTP 请求的 URL。
Progress: 后台任务执行的百分比。
Result: 后台执行任务最终返回的结果,比如String。

(2) 磁盘缓存

接下来实现磁盘缓存:
磁盘缓存包含两部内容:
1)写磁盘:
如何将一个bitmap写入到磁盘中?
可以通过bitmap.compress(Bitmap.CompressFormat.JPEG,100,stream);
2)读磁盘
可以通过BitmapFactory.decodeFile(file.getAbsolutePath());读数据

【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
 * 磁盘缓存(数据存放在本应用程序的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是一个生成md5文件的工具类(附件会提供)
    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());
    //重要操作: 把数据缓存在内存中
    // MemoryCacheUtils马上讲到
    MemoryCacheUtils.saveCache(bitmap,url);
    return bitmap;
}
}

小结 :

借助md5工具类将图片url转化为图片名称,通过IO流将数据写到磁盘中.
通过url获取图片名称,从磁盘中读取数据使用 DiskDataCacher是一个轻量级的Android磁盘缓存工具,基于LRU算法实现,同时可以设置缓存有效期,使用起来十分方便。
用于缓存网络请求返回的数据,并且可以设置缓存数据的有效期,比如,缓存时间假设为1个小时,超时1小时后再次获取缓存会自动失效,让客户端重新请求新的数据,这样
可以减少客户端流量,同时减少服务器并发量。
用于代替SharePreference当做配置文件,缓存一些较大的配置数据,效率更高,可以减少内存消耗。SharePreference不能用来缓存较大数据的理由:请不要滥用
SharedPreference

  • 支持扩展,扩展后可以缓存JsonObjectBitmapDrawable和序列化的java对象等等

(3) 内存缓存

接下来我们实现内存缓存, 内存缓存同样也包含两部分内容:
1)写入内存: 可以通过HashMap,把对应的数据进行保存
Key:对应的url
Value:对应的bitmap

  1. 从内存读 从HashMap中取, 通过key取出对应bitmap
【MemoryCacheUtils】工具类 , 具体代码如下:
package com.xyxy.cache;
import android.graphics.Bitmap;
import android.util.LruCache;

/**
 * Created by on 2018/6/7.
 * 内存缓存:通过HashMap来进行数据
 * <p>
   * 的存储
   */
public class MemoryCacheUtils {
    static {
    //caches = new HashMap<>();
    //Android虚拟机的内存只有16M,易产生OOM异常(内存溢出)
    //java语言提供了另外一种机制:软引用、弱引用、虚引用
    //软引用:当虚拟机内存不足的时候,回收软引用的对象
    //弱引用:当对象没有应用的时候,马上回收
    //虚引用 :任何情况下都可能回收
    //java默认的数据类型是强引用类型
    //caches = new HashMap<>();

    //因为从 Android 2.3 (API Level 9)开始,
    //垃圾回收器会更倾向于回收持有软引用或弱引用的对象,
    //这让软引用和弱引用变得不再可靠。
    //google官方推荐我们使用这样一种缓存机制:LruCache.
    //LruCache lru:least recently used 最近最少使用的算法
    //A
    //B
    //C(最近使用的最少   优先被回收)
    //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. 创建LruCache对象
private static LruCache<String, Bitmap> lruCache;

private static final String TAG = "TextDownload";


//写缓存
public static void saveCache(Bitmap bitmap, String url) {
    // 4. 使用put()方法缓存对象
    if (readCache(url) == null) {
        lruCache.put(url, bitmap);
    }
}

// 从缓存中删除指定的Bitmap
//读缓存
public static Bitmap readCache(String url) {
    // 5. 使用get()方法取出对象
    return lruCache.get(url);
}
}

小结 :

  1. 刚开始我是通过一个HashMap来缓存数据的,当缓存大量的图片时,就会导致应用程序出现内存溢出OOM的情况。属于强引用状态.
  2. 为了解决这样的问题,我将缓存的bitmap对象,由原来的强引用状态变为软引用状态。当内存不足时,回收软用的bitmap对象。
  3. 我们通过软引用方式,来解决内存溢出的情况,是可以的!但是代码中我有提到,随着android系统的升级和更新,垃圾回收器会更倾向于回收持有软引用或弱引用的对象. 因此,持有软引用对象变得不太可靠了。
  4. 最后确定的是google官方推荐我们使用这样一种缓存机制:LruCache

什么是LruCache??

  • 使用最近最少算法机制。来缓存每个对象的。
  • 把近期最少使用的数据从缓存中移除,保留使用最频繁的数据
  • LruCache内存维护一个LinkedHashMap,来维护每个缓存对象
  • 从LruCache取出对象,它会把当前使用的对象进行移动到LinkedHashMap尾端
  • 如果添加一个缓存对象,LruCache会把当前对象也放在LinkedHashMap尾端
  • 在LinkedHashMap的顶端就是最近最少使用的缓存对象。也就是被移除的对象了。

LruCache使用:

  • 构建LruCache对象,同时指定内存缓存大小
  • 重写内部的sizeOf方法,计算每个图片的大小
  • 使用LruCache的put方法缓存对象
  • 使用LruCache的get方法取出对象
  • 缓存在Android的Data data目录下
    讲到LruCache不得不提一下LinkedHashMap,因为LruCache中Lru算法的实现就是通过LinkedHashMap来实现的。LinkedHashMap继承于HashMap,它使用了一个双向链表来存储Map中的Entry顺序关系,这种顺序有两种,一种是LRU顺序,一种是插入顺序,这可以由其构造函数public LinkedHashMap(int initialCapacity,float loadFactor, boolean accessOrder)指定。所以,对于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,还做些调整Entry顺序链表的工作。LruCache中将LinkedHashMap的顺序设置为LRU顺序来实现LRU缓存,每次调用get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用put插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。

最终用于使用的 三级缓存框架 搭建

结合上面创建的三个工具类, 实现类似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.
 * 图片三级缓存框架
 */

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;
    //内存缓存
    bitmap = memoryCacheUtils.readCache(url);
    if (bitmap != null) {
        iv.setImageBitmap(bitmap);
        Log.e(TAG, "从内存获取了图片");
        return;
    }
    //磁盘缓存
    bitmap = localCacheUtils.readCache(context, url);
    if (bitmap != null) {
        iv.setImageBitmap(bitmap);
        Log.e(TAG, "从磁盘获取了图片");
        return;
    }
    //网络缓存
    netCacheUtils.getBitmapFromNet(context, iv, url);
}
}

小结 :

此处只提供了一个显示网络图片的方法 display(), 只需要传入一下三个参数即可:
Context context : 上下文
ImageView iv: 存放图片的容器
String url: 图片的网络链接

【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 "";
    }
}
}

版权声明:本文为博主原创文章,未经博主允许不得转载https://www.jianshu.com/writer#/notebooks/26335980/notes/29184868/preview

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容