Android:跟着实战项目学缓存策略之DiskLruCache详谈

##写在前面java

以前花费大心思更新了一篇《Android:跟着实现项目学缓存策略之LruCache详谈》,原本是准备用项目实战的方式分享一下缓存策略的使用。可是因为篇幅过长,DiskLruCache也比较复杂,因此决定把DiskLruCache抽取出来单独讲。本文仍然是在上一篇文章中新闻小项目基础上来讲明DiskLurCache的用法,以及与LruCache的不一样。文章的目录以下:git

  • 写在前面
  • 遗留问题
  • DiskLruCache详解
    • 基本介绍
    • 实战运用
  • 缓存策略对比与总结
  • 结语
  • 项目源码

##遗留问题github

上一篇文章中已经将图片成功的缓存到内存中,当全部图片缓存完成后,再次滑动就已经不须要从新加载图片了。可是注意看下面这张图的现象:算法

存回收,缓存随之回收

能够看到,成功缓存后确实在应用内再次滑动就不须要加载了,可是若是此时咱们kill掉APP,从新打开的话,仍然是须要加载的。这是为何呢?数组

答案很显然,由于LruCache是将文件类型缓存到内存中,随着APP中Activity的销毁,内存也会随之回收。也就将内存中的缓存回收掉,再次打开APP的时候,内存中找不到缓存,固然须要从新加载了。缓存

因此如何才能缓存到存储设备中呢?下面就来详细说说。安全

##DiskLruCache详解bash

###基本介绍网络

DiskLruCache与LruCache不一样,它不是Android中已经封装好的类,因此 想要使用的话须要从网上下载。关于下载这个类,我也是费了很多功夫,你们若是想尝试的话,能够直接Copy我这个项目中的 com.libcore.io 包下的全部文件便可,这个就很少说了。下面这是它的一个基本定义,也是开发艺术探索中任老师说的:app

DiskLruCache用于实现存储设备缓存,即磁盘缓存,它经过将缓存对象写入文件系统从而实现缓存的效果。

注意,重点是将缓存对象写入文件系统,你们可能不太理解,不过不用担忧,后面会说到。先来它的建立、添加、获取方法。

####一、建立

与LruCache不一样的是,它不能经过构造方法的方式来建立,它的建立方法是经过DiskLruCache类的一个静态方法 open 来建立。具体以下:

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

其中有四个参数,很好理解:

  • File directory:这是缓存文件在磁盘中的存储路径,这是必需要指定的,通常来讲是选择SD卡上的缓存目录,APP卸载后自动删除缓存。
  • int appVersion:这个是版本号,用处不大,正常设置为1便可。
  • int valueCount:这个是单个节点所对应的数据个数,其实就是一个key对应多少个value,正常设置为1便可,这样key和value一一对应,方便查找。
  • long maxSize:这个就是缓存的总大小,很好理解。

这样看来,建立一个DiskLruCache就至少要指定文件的目录与缓存大小。因此建立方式以下:

//DiskLruCache
private DiskLruCache mDiskCache;
//指定磁盘缓存大小
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;//50MB
//获得缓存文件
File diskCacheDir = getDiskCacheDir(mContext, "diskcache");
//若是文件不存在 直接建立
if (!diskCacheDir.exists()) {
	diskCacheDir.mkdirs();
}
mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1,DISK_CACHE_SIZE);
复制代码
/**
 * 建立缓存文件
 *
 * @param context  上下文对象
 * @param filePath 文件路径
 * @return 返回一个文件
 */
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);
}
复制代码

注意,下面的方法是一个工具方法,用来返回一个文件,难度不大。这样就建立了一个DiskLruCache。

####二、设置key

通常来讲,须要用到缓存的地方都是须要联网下载的,因此这个key最好的就是须要下载的文件的Url。可是Url中可能有一些特殊字符,因此最好的方式就是将其转换成MD5值。

MD5是计算机安全领域普遍使用的一种散列函数,用以提供消息的完整性保护。

说简单点,就是一种加密算法,将一串信息转成定长的一串字符。这里只是防止Url中的特殊字符影响正常使用。下面给出如何转成MD5,这是《Android开发艺术探索》中的源码,能够当成工具方法,直接用便可。

/**
 * 将URL转换成key
 *
 * @param url 图片的URL
 * @return
 */
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的字节数组转换成哈希字符串
 *
 * @param bytes URL的字节数组
 * @return
 */
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();
}
复制代码

####三、添加

与LruCache不一样的是,LruCache内部实现是Map,添加直接用put便可;而DiskLruCache是将文件存储到文件中,因此须要经过文件输出流的形式将文件写入到文件系统中。可是仅仅写入是不够的,必须经过Editor对象来提交。它是缓存对象的编辑对象。它是根据文件的Url对应的key的 edit() 方法获取。

值得注意的是,若是返回的Editor对象正在被编辑,那么返回的结果不为null。反之若是返回null,表示编辑对象可用。因此咱们在使用前必须判断一下返回的Editor对象是否为空。若是不为空的话,那就经过Editor对象的 commi 方法来提交写入操做,固然你也能够经过 abort 方法来撤销写入操做。

说了这么多,概括来讲DiskLruCache的添加操做分为三步:

  • 经过文件的Url将文件写入文件系统
  • 经过Url对应的key来获得一个不为空的Editor对象
  • 经过这个Editor对象来对写入操做进行提交或者撤销操做

好了,如今来看具体的实现代码,代码逻辑应该很清晰:

/**
 * 将URL中的图片保存到输出流中
 *
 * @param urlString    图片的URL地址
 * @param outputStream 输出流
 * @return 输出流
 */
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(), IO_BUFFER_SIZE);
        out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
        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;
}
复制代码
/**
 * 将Bitmap写入缓存
 *
 * @param url
 * @return
 * @throws IOException
 */
private Bitmap addBitmapToDiskCache(String url) throws IOException {
    //若是当前线程是在主线程 则异常
    if (Looper.myLooper() == Looper.getMainLooper()) {
        throw new RuntimeException("can not visit network from UI Thread.");
    }
    if (mDiskCache == null) {
        return null;
    }

    //设置key,并根据URL保存输出流的返回值决定是否提交至缓存
    String key = hashKeyFormUrl(url);
    DiskLruCache.Editor editor = mDiskCache.edit(key);
    if (editor != null) {
        OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
        if (downloadUrlToStream(url, outputStream)) {
            editor.commit();
        } else {
            editor.abort();
        }
        mDiskCache.flush();
    }
    return getBitmapFromDiskCache(url);
}
复制代码

####四、获取

相比较于添加操做,获取操做很简单。固然仍是经过key来获取。有了key,能够经过DiskLruCache的get方法获取到一个 Snapshot 对象,再经过这个对象的 getInputStream 方法获得文件的输入流,获得了输出流固然能够获取流中的文件了。

因此归纳起来,获取缓存中文件的步骤也有三个:

  • 经过key来获得一个Snapshot对象
  • 经过Snapshot获得一个文件输入流
  • 经过文件输入流获得文件对象

具体的代码实现以下:

/**
 * 从缓存中取出Bitmap
 *
 * @param url 图片的URL
 * @return 返回Bitmap对象
 * @throws IOException
 */
private Bitmap getBitmapFromDiskCache(String url) throws IOException {
    //若是当前线程是主线程 则异常
    if (Looper.myLooper() == Looper.getMainLooper()) {
        Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
    }
    //若是缓存中为空  直接返回为空
    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) {
        //获得文件输入流
        FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
        FileDescriptor fileDescriptor = fileInputStream.getFD();
        bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
    }
    return bitmap;
}
复制代码

####五、补充

若是你们仔细看了上面的代码会发现不论是缓存的添加仍是获取方法中,都有下面这段代码:

//若是当前线程是主线程 则异常
    if (Looper.myLooper() == Looper.getMainLooper()) {
        Log.w("DiskLruCache", "load bitmap from UI Thread, it's not recommended!");
    }	
复制代码

这是由于这两个方法都不能在主线程中调用,因此须要检查一下,若是不是主线程的话,直接抛出异常。这也算是一个细节吧。

###实战运用

好了,经过上面的分块讲解,你们应该对DiskLruCache有了基本的认识了。如今咱们就对上一个项目添加这样的缓存策略。一样的,为了方便你们对比查看,我仍然把这些方法封装到DiskCacheUtil类。

给出代码以前,咱们也大体梳理一下思路:

  • 首先要初始化DiskLruCache,这个毋庸置疑
  • 其次就须要提供DiskLruCache的添加、获取方法。
  • 而这个添加获取方法须要用到key值,因此要将Url转成MD5值。
  • 剩下的就是经过AsyncTask来展现图片了,并在展现过程当中添加到缓存中。
  • 固然不要忘了,前一篇所说的ListView滑动中止加载,静止才能加载的优化。

下面直接给出代码,代码比较长,可是冷静下来,按照前面说的逻辑来看是否是很清晰呢?

/**
 * 利用DiskLruCache来缓存图片
 */
public class DiskCacheUtil {
    private Context mContext;

    private ListView mListView;
    private Set<NewsAsyncTask> mTaskSet;

    //定义DiskLruCache
    private DiskLruCache mDiskCache;
    //指定磁盘缓存大小
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;//50MB
    //IO缓存流大小
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    //缓存个数
    private static final int DISK_CACHE_INDEX = 0;
    //缓存文件是否建立
    private boolean mIsDiskLruCacheCreated = false;

    public DiskCacheUtil(Context context, ListView listView) {
        this.mListView = listView;
        mTaskSet = new HashSet<>();
        mContext = context.getApplicationContext();
        //获得缓存文件
        File diskCacheDir = getDiskCacheDir(mContext, "diskcache");
        //若是文件不存在 直接建立
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1,
                        DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 经过异步任务的方式加载数据
     *
     * @param iv  图片的控件
     * @param url 图片的URL
     */
    public void showImageByAsyncTask(ImageView iv, final String url) throws IOException {
        //从缓存中取出图片
        Bitmap bitmap = getBitmapFromDiskCache(url);
        //若是缓存中没有,则须要从网络中下载
        if (bitmap == null) {
            iv.setImageResource(R.mipmap.ic_launcher);
        } else {
            //若是缓存中有 直接设置
            iv.setImageBitmap(bitmap);
        }
    }

    /**
     * 将一个URL转换成bitmap对象
     *
     * @param urlStr 图片的URL
     * @return
     */
    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(), IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(is);
            connection.disconnect();
            return bitmap;
        } catch (java.io.IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 将URL中的图片保存到输出流中
     *
     * @param urlString    图片的URL地址
     * @param outputStream 输出流
     * @return 输出流
     */
    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(), IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
            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;
    }

    /**
     * 加载从start到end的全部的Image
     *
     * @param start
     * @param end
     */
    public void loadImages(int start, int end) throws IOException {
        for (int i = start; i < end; i++) {
            String url = NewsAdapter.urls[i];
            //从缓存中取出图片
            Bitmap bitmap = getBitmapFromDiskCache(url);
            //若是缓存中没有,则须要从网络中下载
            if (bitmap == null) {
                NewsAsyncTask task = new NewsAsyncTask(url);
                task.execute(url);
                mTaskSet.add(task);
            } else {
                //若是缓存中有 直接设置
                ImageView imageView = (ImageView) mListView.findViewWithTag(url);
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    /**
     * 中止全部当前正在运行的任务
     */
    public void cancelAllTask() {
        if (mTaskSet != null) {
            for (NewsAsyncTask task : mTaskSet) {
                task.cancel(false);
            }
        }
    }

    /*--------------------------------DiskLruCaChe的实现-----------------------------------------*/

    /**
     * 建立缓存文件
     *
     * @param context  上下文对象
     * @param filePath 文件路径
     * @return 返回一个文件
     */
    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);
    }

    /**
     * 获得当前可用的空间大小
     *
     * @param path 文件的路径
     * @return
     */
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    }

    /**
     * 将URL转换成key
     *
     * @param url 图片的URL
     * @return
     */
    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的字节数组转换成哈希字符串
     *
     * @param bytes URL的字节数组
     * @return
     */
    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写入缓存
     *
     * @param url
     * @return
     * @throws IOException
     */
    private Bitmap addBitmapToDiskCache(String url) throws IOException {
        //若是当前线程是在主线程 则异常
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }
        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(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url, outputStream)) {
                //提交写入操做
                editor.commit();
            } else {
                //撤销写入操做
                editor.abort();
            }
            mDiskCache.flush();
        }
        return getBitmapFromDiskCache(url);
    }
复制代码
/**
     * 从缓存中取出Bitmap
     *
     * @param url 图片的URL
     * @return 返回Bitmap对象
     * @throws IOException
     */
    private Bitmap getBitmapFromDiskCache(String url) throws IOException {
        //若是当前线程是主线程 则异常
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w("DiskLruCache", "load bitmap from UI Thread, it's not recommended!");
        }
        //若是缓存中为空  直接返回为空
        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) {
            //获得文件输入流
            FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
            //获得文件描述符
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        }
        return bitmap;
    }

     /*--------------------------------DiskLruCaChe的实现-----------------------------------------*/
复制代码
/*--------------------------------异步任务AsyncTask的实现--------------------------------------*/
	    /**
	     * 异步任务类
	     */
	    private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {
	        private String url;
        public NewsAsyncTask(String url) {
            this.url = url;
        }

        @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);
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            mTaskSet.remove(this);
        }
    }

    /*--------------------------------异步任务AsyncTask的实现--------------------------------------*/
}
复制代码

最后不要忘了在自定义Adapter中调用DiskCache这个工具类,并把图片加载方法换成DiskLruCache方式:

//第三种方式 经过异步任务方式设置 且利用DiskLruCache存储到磁盘缓存中
    try {
        mDiskCacheUtil.showImageByAsyncTask(viewHolder.iconImage, iconUrl);
    } catch (IOException e) {
        e.printStackTrace();
    }
复制代码

好了,如今来看效果图吧:

DiskLruCaChe

从图中能够看出尽管退出了APP,可是从新打开的时候,仍然不须要加载图片,大功告成!

##缓存策略对比与总结

好了,DiskLruCache也讲完了。回顾以前的LruCache,一样是Android中的缓存策略。那它们之间有什么不一样呢?

  1. LruCache是Android中的已经封装好的类,能够直接用。可是DiskLruCache须要导入对应的包后,才能使用。
  2. LruCache实现的是内存缓存,当APP被kill的时候,缓存也随之消失。而DiskLruCache实现的是磁盘缓存,当APP被kill的时候,缓存仍然不会消失。
  3. LruCache的内部实现是LinkedHashMap,也就是集合。因此添加获取方式经过put与get就好了。而DiskLruCache是经过文件流的形式来缓存,因此添加获取是经过输入输出流来实现。

大致也就也上三种主要的区别。

最后我想说的是,本项目是为了你们看起来方便,有对比性,因此把普通线程加载、LruCache加载、DiskLruCache加载分别封装了不一样的类。

可是在平常开发中,须要Bitmap的压缩类与这几种加载方式在一块儿封装成一个大的类。就是你们常提到的 ImageLoader 。它专门用来处理Bitmap的加载。

这样作的好处就是将三种加载方式结合,也就是你们常据说的 三级缓存机制 ,网上也有不少优秀的ImageLoader,固然你们也能够尝试尝试,本身写出一个ImageLoader。

##结语

经过两篇文章中的一个小小的实战项目,终于把缓存策略说完了。写文章的过程当中本身也是回顾了整个项目,受益不浅。有些时候把一个东西用本身的话分享出来而且让别人能听懂,比本身学一个东西要难不少。因此以为常常写博客,仍是对知识的消化有点帮助的。

最后因为我水平有限,项目和文章中不免会有错误,欢迎你们指正与交流。

##项目源码

IamXiaRui - MoocNewsDemo


我的博客:www.iamxiarui.com

原文连接:http://www.iamxiarui.com/?p=719

相关文章
相关标签/搜索