安卓缓存之DiskLruCache及设计(异步+缓存)图片加载器DiskCacheImageLoader

DiskLruCache

DiskLruCache是一套硬盘缓存的解决方案,算法同LruCache基于LRU算法。

  1. DiskLruCache不是由Google官方编写的,这个类没有被包含在Android API当中。这个类需要从网上下载

    可以到作者的Github这里获取

一、DiskLruCache的基本使用

1. 初始化DiskLruCache

调用它的open()静态方法创建一个DiskLruCache的实例

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

参数说明:

  • File directory:指定的是数据的缓存地址,一般为应用的内部/外部缓存文件夹,这样当应用卸载时,里面的数据也会被清除。

  • int appVersion:指定当前应用程序的版本号。

  • int valueCount :指定同一个key可以对应多少个缓存文件,一般都是传1,这样key和value一一对应,方便查找。

  • long maxsize:指定最多可以缓存数据的总字节数。

  • 获取DiskLruCache的缓存目录directory

    当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取手机内部缓存路径,否则就调用getCacheDir()方法来获取手机外部缓存路径。前者获取到的就是 /sdcard/Android/data//cache 这个路径,后者获取到的是 /data/data//cache 这个路径。

    public File getDiskCacheDir(Context context, String uniqueName) {
    
      String cachePath;
      if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
          || !Environment.isExternalStorageRemovable()) {
        cachePath = context.getExternalCacheDir().getPath();
      } else {
        cachePath = context.getCacheDir().getPath();
      }
    
      cacheDir = new File(cachePath + File.separator + uniqueName);
    
      // 判断缓存文件夹是否存在,不存在则创建             
      if (!cacheDir.exists()) {
            cacheDir.mkdirs();
      }
    
      return cacheDir;
    }       
    
  • 获取应用版本号appVersion

    每当版本号改变,缓存路径下存储的所有数据都会被清除掉。

    public int getAppVersion(Context context) {
        try {
          PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
          return info.versionCode;
        } catch (NameNotFoundException e) {
          e.printStackTrace();
        }
        return 1;
      }
    
  • 创建DiskLruCache对象方式如下

    private DiskLruCache mDiskCache;
    
    //指定磁盘缓存大小
    private long DISK_CACHE_SIZE = 1024 * 1024 * 10;//10MB
    //得到缓存牡蛎
    File cacheDir = getDiskCacheDir(mContext, "Bitmap");
    
    mDiskCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1,DISK_CACHE_SIZE);
    

2. 将文件写入DiskLruCache缓存

写入操作是借助DiskLruCache.Editor这个类完成,需要调用DiskLruCache的edit()方法来获取实例。

public Editor edit(String key) throws IOException

edit()方法接收一个参数key,该key将会成为缓存文件的文件名,并且需要和文件的URL是一 一对应。

直接使用URL来作为key不太合适,因为文件的URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。
最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,并且只会包含0-F这样的字符,完全符合文件的命名规则。

  • 将文件的URL字符串进行MD5编码:

    public String hashKeyForDisk(String key) {
      String cacheKey;
      try {
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(key.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
      } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(key.hashCode());
      }
      return cacheKey;
    }
    
    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();
    }
    
  • 获取DiskLruCache.Editor的实例

    String imageUrl = "http://.....jpg";
    // 转化为对应的key
    String key = hashKeyForDisk(imageUrl);
    
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    
  • 通过editor获取输出流并写入数据

    if (editor != null) {
        OutputStream outputStream = editor.newOutputStream(0);
        if (downloadUrlToStream(url, outputStream)) {
            editor.commit(); // 提交编辑
        } else {
            editor.abort(); // 放弃编辑
        }
        mDiskCache.flush(); // 刷新缓存
    }
    
    
    private 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(), 8 * 1024);
        out = new BufferedOutputStream(outputStream, 8 * 1024);
        int b;
        while ((b = in.read()) != -1) {
          out.write(b);
        }
        return true;
      } catch (final IOException e) {
        e.printStackTrace();
      } finally {
        if (urlConnection != null) {
          urlConnection.disconnect();
        }
        try {
          if (out != null) {
            out.close();
          }
          if (in != null) {
            in.close();
          }
        } catch (final IOException e) {
          e.printStackTrace();
        }
      }
      return false;
    }
    

    注:newOutputStream()方法接收一个index参数,由于前面在设置valueCount的时候指定的是1,这里index传0就可以了。

    在写入操作执行完之后,我们还需要调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。

3. 获取缓存文件:

  • 调用DiskLruCache的get方法来得到Snapshot对象

    get()方法要求传入一个key来获取到相应的缓存数据,而这个key就是将文件URL进行MD5编码后的值

    public synchronized Snapshot get(String key) throws IOException
    
    
    
    String imageUrl = "http://.....jpg";
    // 转化为key
    String key = hashKeyForDisk(imageUrl);
    
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    
  • 通过Snapshot得到文件输入流

    InputStream is = snapShot.getInputStream(0);
    

    getInputStream()方法也需要传一个index参数,这里传入0。这是因为我们在open方法中参数设置为一个key对应一个文件

  • 通过文件输入流得到文件对象(以获取bitmap为例)

    Bitmap bitmap = BitmapFactory.decodeStream(is);
    

4. 移除某个key的缓存

借助DiskLruCache的remove()方法移除缓存

public synchronized boolean remove(String key) throws IOException

这个方法不经常被调用。因为DiskLruCache会根据我们在调用open()方法时设定的缓存最大值来自动删除多余的缓存。

5. 获取缓存的总字节数和删除全部缓存

借助DiskLruCache的size()方法获取缓存的总字节数

public synchronized long size() 

这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位。如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出。

public void delete() throws IOException

这个方法用于将所有的缓存数据全部删除,可以实现手动清理缓存功能。

6. 刷新缓存状态

 public synchronized void flush() throws IOException {
        checkNotClosed();
        trimToSize();
        journalWriter.flush();
    }

这个方法用于将内存中的操作记录同步到日志文件(journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。可在Activity的onPause()方法中去调用一次flush()方法。

二、使用DiskLruCache封装图片加载器

根据DiskLruCache,我们可以将它封装在DiskCacheImageLoader来对图片进行缓存。设计模式为使用单例模式来设计:

在Logcat中打印异步任务个数:

package com.cxmscb.cxm.cacheproject;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.os.StatFs;
import android.provider.ContactsContract;
import android.util.Log;
import android.widget.ImageView;
import android.widget.ListView;



import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;

import libcore.io.DiskLruCache;

/**
 * 利用DiskLruCache来缓存图片
 */
public class DiskCacheImageLoader {


    private Context mContext;

    private Set<DiskCacheAsyncTask> mTaskSet;

    //DiskLruCache
    private DiskLruCache mDiskCache;


    private static DiskCacheImageLoader mDiskCacheImageLoader;

    public static DiskCacheImageLoader getInstance(Context context){
        if(mDiskCacheImageLoader==null){
            synchronized (DiskCacheImageLoader.class){
                if(mDiskCacheImageLoader==null){
                    mDiskCacheImageLoader = new DiskCacheImageLoader(context);
                }
            }
        }
        return  mDiskCacheImageLoader;
    }


    private DiskCacheImageLoader(Context context) {

        mTaskSet = new HashSet<>();
        mContext = context.getApplicationContext();
        //得到缓存文件
        File diskCacheDir = getDiskCacheDir(mContext, "Bitmap");
        //如果文件不存在 直接创建
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }

        try {
            mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1,
                            1024*1024*20);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    /**
     * 将一个URL转换成bitmap对象
     *
     */
    public Bitmap getBitmapFromURL(String urlStr) {
        Bitmap bitmap;
        InputStream is = null;

        try {
            URL url = new URL(urlStr);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(connection.getInputStream(), 1024*8);
            bitmap = BitmapFactory.decodeStream(is);
            connection.disconnect();
            return bitmap;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 将URL中的图片保存到输出流中
     *
     */
    private 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(), 1024*8);
            out = new BufferedOutputStream(outputStream, 1024*8);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (final IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }



    /**
     * 普通地加载url
     *
     */
    public void loadImage(ImageView imageView,String url){
        //从缓存中取出图片
        Bitmap bitmap = null;
        try {
            bitmap = getBitmapFromDiskCache(url);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //如果缓存中没有,则需要从网络中下载
        if (bitmap == null) {
            DiskCacheAsyncTask task = new DiskCacheAsyncTask(imageView);
            task.execute(url);
            mTaskSet.add(task);
        } else {
            //如果缓存中有 直接设置
            imageView.setImageBitmap(bitmap);
        }
    }

    /**
     * 为listview加载从start到end的所有的Image
     *
     */
    public void loadTagedImagesInListView(int start, int end,String[] urls,ListView mListView) {
        for (int i = start; i < end; i++) {
            String url = urls[i];
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            loadImage(imageView,url);
        }
        Log.i("num of task"," "+mTaskSet.size());
    }



    /**
     * 创建缓存文件
     *
     */
    public File getDiskCacheDir(Context context, String filePath) {
        boolean externalStorageAvailable = Environment
                .getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }

        return new File(cachePath + File.separator + filePath);
    }



    /**
     * 将URL转换成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());
        }
        return cacheKey;
    }

    /**
     * 将Url的字节数组转换成哈希字符串
     *
     */
    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();
    }

    /**
     * 将Bitmap写入缓存
     *
     */
    private Bitmap addBitmapToDiskCache(String url) throws IOException {


        if (mDiskCache == null) {
            return null;
        }

        //设置key,并根据URL保存输出流的返回值决定是否提交至缓存
        String key = hashKeyFormUrl(url);
        //得到Editor对象
        DiskLruCache.Editor editor = mDiskCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(0);
            if (downloadUrlToStream(url, outputStream)) {
                //提交写入操作
                editor.commit();
            } else {
                //撤销写入操作
                editor.abort();
            }
            mDiskCache.flush();
        }
        return getBitmapFromDiskCache(url);
    }


    /**
     * 从缓存中取出Bitmap
     *
     */
    private Bitmap getBitmapFromDiskCache(String url) throws IOException {

        //如果缓存中为空  直接返回为空
        if (mDiskCache == null) {
            return null;
        }

        //通过key值在缓存中找到对应的Bitmap
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        //通过key得到Snapshot对象
        DiskLruCache.Snapshot snapShot = mDiskCache.get(key);
        if (snapShot != null) {
            //得到文件输入流
            InputStream ins = snapShot.getInputStream(0);

            bitmap = BitmapFactory.decodeStream(ins);
        }
        return bitmap;
    }






    /**
     * 异步任务类
     */
    private class DiskCacheAsyncTask extends AsyncTask<String, Void, Bitmap> {
        private ImageView imageView;

        public DiskCacheAsyncTask(ImageView imageView){
            this.imageView = imageView;
        }


        @Override
        protected Bitmap doInBackground(String... params) {

            Bitmap bitmap = getBitmapFromURL(params[0]);
            //保存到缓存中
            if (bitmap != null) {
                try {
                    //写入缓存
                    addBitmapToDiskCache(params[0]);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            mTaskSet.remove(this);
            Log.i("num of task"," "+mTaskSet.size());
        }
    }

    /**
     * 停止所有当前正在运行的任务
     */
    public void cancelAllTask() {
        if (mTaskSet != null) {
            for (DiskCacheAsyncTask task : mTaskSet) {
                task.cancel(false);
            }
            mTaskSet.clear();
            Log.i("num of task"," "+mTaskSet.size());
        }
    }


}

三、 使用LruCacheImageLoader来加载网络图片:

一、项目效果图:

开始ListView从网络上加载图片,关闭网络退出应用后,再打开应用,从本地DiskLruCache中加载图片

这里写图片描述

二、对ListView加载图片的优化:

listView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView absListView, int scrollState) {
                if(scrollState==SCROLL_STATE_IDLE){
                    mDiskImageLoader.loadTagedImagesInListView(mStart,mEnd,urls,listView);
                }else {
                    mDiskImageLoader.cancelAllTask();
                }
            }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mStart = firstVisibleItem;
        mEnd = firstVisbleItem + visibleItemCount;
        if(mFirstIn && visibleItemCount > 0){
            mDiskImageLoader.loadTagedImagesInListView(mStart,mEnd,urls,listView); 
            mFirstIn = false;
        }
    }
});

完整项目地址:Github地址

参考

Android DiskLruCache完全解析,硬盘缓存的最佳方案

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值