图片的三级缓存

介绍

本文介绍了三级缓存的原理及操作,并且用策略模式的架子框起来。


1.图片的三级缓存是什么

图片的三级缓存顾名思义就是指三级图片的缓存技术,哦,不对,我在说什么。图片的三级缓存顾名思义就是指在图片加载过程中使用缓存技术,在加载网上的图片时,第一次加载时下载图片然后显示,然后会把图片进行缓存操作,之后如果再次使用这个图片时就不用再次下载了,这样子的话可以节约系统资源,也可以节省用户流量。

那么三级缓存是哪三级呢?

  • 网络缓存:这一步其实就是利用网络去下载图片的操作,严格说不能算“缓存操作”,但是为了名称的统一,大家就叫它网络缓存。
  • 本地缓存:就是说将图片保存在本地SD卡上,使用时直接从SD卡取出就好了。
  • 内存缓存:最麻烦的一步,就是将图片保存在内存中,要知道内存缓存是最快的,但是会占用用户内存,所以需要适当的释放内存,后文有具体描述。

2.流程图来一波

图片从百度拿来的

流程图

我觉得这张图不错,就直接拿来用了,图很好理解,优先从内存中加载,其次是SD卡,内存是最快的,最后才是从网络下载。记得获取到图片以后要相应的存储在缓存中。


3.上代码

代码使用了策略模式封装,如果不了解策略模式的可以去看看本人另一篇关于策略模式的博客。

缓存的抽象父类:

//缓存的抽象类
public abstract class Cache {
    abstract Bitmap get(String url) throws IOException;
    abstract void put(String url,Bitmap bitmap);
}

所有的缓存类应当有put和get方法,以便存和取。

文件缓存:


public class FileCache extends Cache {

    //文件目录
    private static final String LOCAL_CACHE_PATH = Environment
            .getExternalStorageDirectory().getAbsolutePath() + "/Cache";
    private static final String TAG = "ImageDown";

    @Override
    Bitmap get(String url) {
        try {
            File cacheFile = new File(LOCAL_CACHE_PATH, MD5Encoder.encode(url));

            if (cacheFile.exists()) {
                Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(
                        cacheFile));
                Log.e(TAG, "get: 图片来源文件");
                return bitmap;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    void put(String url, Bitmap bitmap) {
        File dir = new File(LOCAL_CACHE_PATH);
        if (!dir.exists() || !dir.isDirectory()) {
            boolean b = dir.mkdirs();// 创建文件夹
            if (!b){
                Log.e(TAG, "put: 创建文件夹失败");
            }

        }

        try {
            String fileName = MD5Encoder.encode(url);

            File cacheFile = new File(dir, fileName);

            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(
                    cacheFile));// 参1:图片格式;参2:压缩比例0-100; 参3:输出流
        } catch (Exception e) {
            e.printStackTrace();
        }    }
}

这就是一些文件的读写操作,需要注意几点
如果创建文件夹失败,可能是6.0权限的问题。
这里存的名字不是直接以url为名字,而是以url计算的MD5值来作为文件名的,因为有些url不能直接作为文件名

补充一下MD5的工具类:

public class MD5Encoder {
    
    public static String encode(String string) throws Exception {
        byte[] hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));
        StringBuilder hex = new StringBuilder(hash.length * 2);
        for (byte b : hash) {
            if ((b & 0xFF) < 0x10) {
                hex.append("0");
            }
            hex.append(Integer.toHexString(b & 0xFF));
        }
        return hex.toString();
    }
}

内存缓存:

先上代码:

public class MemoryCache extends Cache {
    private static final String TAG = "ImageDown";

    public MemoryCache() {
        this.memortCache = new LruCache<String,Bitmap>((int) (Runtime.getRuntime().maxMemory()/1024/40)){

            // 返回每个对象的大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight()/1024;
            }
        };
    }

    private LruCache<String,Bitmap> memortCache ;
    @Override
    Bitmap get(String url) {
        Bitmap bitmap = memortCache.get(url);
        if (bitmap != null){
            Log.e(TAG, "get: 图片来源内存" );
            return bitmap;
        }else {
            return null;
        }
    }

    @Override
    void put(String url, Bitmap bitmap) {
        memortCache.put(url,bitmap);
    }
}

解释几个东西:

java的四种引用:、

  • 默认强引用, A a = new A(),当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。所以用完以后记得a = null;
  • 软引用SoftReference, 垃圾回收器会考虑回收, 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
    SoftReference<String> str=new SoftReference<String>("xxx");
  • 弱引用 WeakReference, 垃圾回收器更会考虑回收,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
    WeakReference<String> str= new WeakReference<String>("xxx");
  • 虚引用PhantomReference, 垃圾回收器最优先回收,就是形同虚设,在任何时候都可能被垃圾回收器回收。

由于内存缓存把图片放入内存中,倘若所有所有图片都强引用,图片越来越多,内存就爆炸了,所以以前很多都是把图片bitmap用成弱引用,然后放进hashmap中,这样可以适当的回收,后来出现了新东西LruCache,就不这么做了。

关于LruCache:

这个是谷歌提供的工具类,采用了一种叫最近最少使用算法,这个LruCache封装了一个LinkedHashMap,这里面放的引言它会根据情况,如果占用内存超出了预设值,就优先释放最近最少使用的引用。
初始化LruCache:
在构造方法里设置LruCache占用的内存应该多大,这里取的是虚拟机分配给本app的内存的1/4

        this.memortCache = new LruCache<String,Bitmap>((int) (Runtime.getRuntime().maxMemory()/1024/4)){

            // 返回每个对象的大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight()/1024;
            }
        };

ImageUtils:

public class ImageUtils {

    private ImageView imageView;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == GOT_BITMAP) {
                imageView.setImageBitmap((Bitmap) msg.obj);
                Log.e(TAG, "handleMessage: 图片来源网络");
            }
        }
    };
    private static final int GOT_BITMAP = 0;

    private static final String TAG = "ImageDown";

    Cache mCache = new MemoryCache();

    //创建一个线程池,线程数量为cpu的核心数
    private ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    //注入缓存类型
    public void setImageCache(Cache imageCache) {
        mCache = imageCache;
    }

    public void ShowPic(String url, ImageView imageView) throws IOException {
        this.imageView = imageView;
        Bitmap bitmap = mCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        //没有缓存
        LoadPic(url, imageView);
    }

    private void LoadPic(final String url, final ImageView imageView) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = DownLoadImage(url);

                Message message = handler.obtainMessage();
                message.what = GOT_BITMAP;
                message.obj = bitmap;
                handler.sendMessage(message);

                mCache.put(url, bitmap);
            }
        });
    }

    private Bitmap DownLoadImage(String url) {
        Bitmap bitmap = null;
        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder().get().url(url).build();
        Response response = null;
        try {
            response = okHttpClient.newCall(request).execute();
            if (response.isSuccessful()) {
                byte[] bytes = response.body().bytes();
                bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                Log.e(TAG, "DownandSet: decodeByteArray");
            } else {
                Log.e(TAG, "DownLoadImage: 从网络下载图片失败--" + response.message());
            }
            if (bitmap == null) {
                Log.e(TAG, "DownandSet: bitmap==null");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

因为网络缓存是备用方案,所以放进这个类里面实现了。
在showPic方法中有缓存就使用缓存,没缓存就loadPic下载图片。

使用者:

先看一下xml布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="st.zlei.com.imageload.MainActivity"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
    <Button
        android:id="@+id/down_button"
        android:layout_height="wrap_content"
        android:layout_width="match_parent" />
    <Button
        android:text="清空图片"
        android:id="@+id/dclear_button"
        android:layout_height="wrap_content"
        android:layout_width="match_parent" />


</LinearLayout>

布局长这样:


布局

具体调用:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        down_button = (Button) findViewById(R.id.down_button);
        clear_button = (Button) findViewById(R.id.dclear_button);
        image = (ImageView) findViewById(R.id.image);
        imageUtils = new ImageUtils();
        //这里传入使用的缓存类型
        imageUtils.setImageCache(new MemoryCache());

        down_button.setText("显示图片");
        down_button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    imageUtils.ShowPic(imageURL, image);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        clear_button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                image.setImageBitmap(null);
            }
        });
    }

看一下运行结果

首先使用文件缓存:

采用文件缓存策略

首先点击显示图片,然后清空图片,再次点击显示图片:

打印结果

可以看到第一次加载是从网络下载的图片,第二次加载就直接从文件中获取图片了。

在手机内存卡上多了一个cache文件夹,里面存放的就是缓存的图片文件:

缓存文件夹
缓存的图片文件

使用内存缓存

采用内存缓存
打印结果

综合使用三级缓存

上面都只是单独使用的文件缓存或者内存缓存,现在我们综合一下,优先使用内存缓存,如果没有就使用文件缓存,都没有的话才去网络下载图片。

OverAllCache 缓存类,在调用的时候注入这个类即可

public class OverAllCache extends Cache {
    private static final String TAG = "ImageDown";

    MemoryCache memoryCache = new MemoryCache();
    FileCache fileCache = new FileCache();

    @Override
    Bitmap get(String url) throws IOException {
        Bitmap bitmap;
        bitmap = memoryCache.get(url);
        if (bitmap == null){
            bitmap = fileCache.get(url);
            if (bitmap != null){
                memoryCache.put(url,bitmap);
            }
        }
        return bitmap;
    }

    @Override
    void put(String url, Bitmap bitmap) {
        memoryCache.put(url,bitmap);
        fileCache.put(url,bitmap);
    }
}

看看运行结果

调用
打印结果

由于之前加载过图片,SD卡上已经有缓存了,此时第一次加载就从文件中获取,之后每次调用都是从内存中获取了。


扩展缓存类型

这部分就是策略模式的架子带来的好处了,当以后想要自己定义怎么缓存,就可以这样调用了:

扩展缓存

这里直接传父类Cache,则必须重写get,put方法,于是用户就可以在这里自定义get,put的内容了。


总结

到这里图片的三级缓存就讲完了,一口气写了一天,一顿操作猛如虎,但是我还是得说,当我们真的需要图片加载框架的时候还是Picasso或者Glide什么的更靠谱,本文的内容只是让我们理解三级缓存的知识,跟Picasso什么的相比还是图样图森破。

另外本文所使用代码可在本人github上找到 -->传送门

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

推荐阅读更多精彩内容