安卓中的缓存包括两种情况即内存缓存与磁盘缓存,其中内存缓存主要是使用LruCache这个类,其中内存缓存我在【安卓中的缓存策略系列】安卓缓存策略之内存缓存LruCache中已经进行过详细讲解,如看官还没看过此博客,建议看官先去看一下。

我们知道LruCache可以让我们快速的从内存中获取用户最近使用过的Bitmap,但是我们无法保证最近访问过的Bitmap都能够保存在缓存中,像类似GridView等需要大量数据填充的控件很容易就会用完整个内存缓存。另外,我们的应用可能会被类似打电话等行为而暂停导致退到后台,因为后台应用可能会被杀死,那么内存缓存就会被销毁,缓存的Bitmap也就不存在了。一旦用户恢复应用的状态,那么应用就需要重新处理那些图片,另外某些情况下即使用户退出整个APP后重新打开该APP其缓存的图片应该还能被显示出来,显然此种情况下使用内存缓存是做不到的。

而磁盘缓存可以用来保存那些已经处理过的Bitmap,它还可以减少那些不在内存缓存中的Bitmap的加载次数。磁盘缓存主要涉及到DiskLruCache这个类。下面从源码的角度详细讲解DiskLruCache这个类,然后在此基础上讲解如何使用DiskLruCache,让读者知其然更知其所以然。

一DiskLruCache类:

首先我们来看一下其构造函数

 private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}

可以看到其构造函数被private修饰,也就意味着对外是不可见的,即我们不能通过其构造函数来创建一个DiskLruCache对象,如果要创建一个DiskLruCache实例需要使用open函数,其代码如下:

 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
} // prefer to pick up where we left off
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
} // create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}

可以看到open函数对应的参数与DiskLruCache的参数完全一致,其中第一个参数directory表示磁盘缓存在文件系统中的存储路径,一般选择SD卡上的缓存路径,默认位置为/sdcard/Android/data/<application package_name>/cache目录下,其中<application package_name>表示应用的包名,当应用被卸载后该目录会被删除。第二个参数顾名思义为app版本号,通常将其置为1,当版本号被改变时会清空之前所有的缓存文件,第三个参数用来指定单个缓存节点可以对应的缓存文件个数,通常为1,第四个参数maxSize顾名思义表示该磁盘缓存的最大容量。

其中第一个参数maxSize也可以指定选择data下的当前应用的目录(此时的缓存路径为/data/data/<application package>/cache),所以通常我们先判断是否存在SD卡,如果存在则使用SD卡缓存,否则选择data下的当前应用的目录缓存。具体代码如下:

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();
}
return new File(cachePath + File.separator + uniqueName);
}

在open函数中可以看到首先会调用DiskLruCache的构造函数,在该构造函数中创建了journalFile,journalFileTmp这两个文件,然后判断journalFile是否存在,如果存在则

调用cache.readJournal();读取journal日志文件,然后调用 cache.processJournal();处理日志文件,该函数的作用就是计算初始化的大小和收集缓存文件中的垃圾文件( Computes the initial size and collects garbage as a part of opening the cache),删除Dirty记录(Dirty entries are assumed to be inconsistent and will be deleted),即垃圾文件.这个概念与数据库中的读取脏数据是差不多的,讲到这里就不得不为读者讲解一下DiskLruCache的日志文件的格式。格式如下(注:此图来源于网络,向贡献该图的人表示感谢)

其中的前五行基本上是固定的,表示DiskLruCache日志文件的头部数据,第一行是个固定的字符串“libcore.io.DiskLruCache”,意味着我们使用的是DiskLruCache,

第二行是DiskLruCache的版本号,这个值是恒为1的。第三行是应用程序的版本号,这个值与我们在open()方法里传入的版本号是相同的。第四行是valueCount,这个值也是在open()方法中传入的,通常情况下都为1。第五行是一个空行。空行过后才是日志文件的内容:

接下来是一个以DIRTY开头的行,其后的一串数字表示的是存入的数据的key,如果读者了解数据库的话,知道一般DIRTY表示的是脏数据,这是因为当我们每次向磁盘缓存中写入一条数据时都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。当调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净”,它不再是脏数据,当调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。另外以READ开头的行表示我们从缓存中读取了一条数据,这时会向日志文件中添加一个READ记录。

这样我们就可以理解上面讲述的 cache.processJournal()函数处理日志文件的过程,即该函数会清除只出现DIRTY但未出现CLEAN或REMOVE的记录,即出现CLEAN且没被REMOVE的记录才会保存下来,然后通过cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), IO_BUFFER_SIZE);将CLEAN的记录保存到日志文件中,最后返回该cache。

第二种情况是如果cache.journalFile不存在,相当于初次创建cahce文件,则会创建一个空的cache,代码如下:

        directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();

在创建空的Cahce时会调用 cache.rebuildJournal();的方法,该方法的作用是删除日志文件中的多余信息,如果日志文件已存在,则会替换当前的日志文件。在该过程中会向日志文件中写入头部数据,这也是为何我们在上面看到的DiskLruCache的日志文件的格式中会包含前面5行数据的原因,代码如下:

 private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
} Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
writer.write(MAGIC);
writer.write("\n");
writer.write(VERSION_1);
writer.write("\n");
writer.write(Integer.toString(appVersion));
writer.write("\n");
writer.write(Integer.toString(valueCount));
writer.write("\n");
writer.write("\n"); for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
writer.write(DIRTY + ' ' + entry.key + '\n');
} else {
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
}
} writer.close();
journalFileTmp.renameTo(journalFile);
journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
}

接下来我们看一下DiskLruCache中的重要方法:

首先来看一下关于添加缓存的edit方法。

 public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
} private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
&& (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
return null; // snapshot is stale
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // another edit is in progress
} Editor editor = new Editor(entry);
entry.currentEditor = editor; // flush the journal before creating files to prevent file leaks
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}

可以看到edit方法是同步的。edit方法中首先会调用validateKey(key);来检测传入的key是否合法,不能包含空格,换行,当我们在缓存一张图片时通常我们拿到的是图片的Url,而Url中可能会包含上述不合法字符,所以通常我们会将图片的Url转换为key,然后将其作为参数传给edit(),然后调用LinkedHashMap的get方法通过key获取缓存entry,如果该entry为空(表示我们第一次存入该缓存),则会通过key创建Entry将其赋给entry然后将其put到lruEntries,即entry
= new Entry(key);lruEntries.put(key, entry);

如果获取的entry不为空,则代表不是初次存入该key的缓存,则判断entry.currentEditor是否为空,如果不为空则表示当前缓存entry正在被edit,此时将直接返回null,即DiskLruCache不允许同时edit一个缓存对象。注意entry.currentEditor不为空的前提是entry不为空。

如果如果获取的entry不为空同时entry.currentEditor为空,则会根据entyr构造Editor对象editor,然后将该editor的值赋给entry.currentEditor,然后调用journalWriter.write(DIRTY + ' ' + key + '\n');向日志文件中写入一个DIRTY行,表示该记录正在被操作。最后返回该editor。通过该editor的 public OutputStream newOutputStream(int index)方法可以得到缓存文件输出流。通过该文件输出流就可以将缓存写入到磁盘上保存起来,最后必须调用editor的commit()来提交写入操作,这样才真真正正的把记录写入到磁盘缓存上了。

再来看一下获取缓存的get方法:

 public synchronized Snapshot get(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            return null;
        }         if (!entry.readable) {
            return null;
        }         /*
         * Open all streams eagerly to guarantee that we see a single published
         * snapshot. If we opened streams lazily then the streams could come
         * from different edits.
         */
        InputStream[] ins = new InputStream[valueCount];
        try {
            for (int i = 0; i < valueCount; i++) {
                ins[i] = new FileInputStream(entry.getCleanFile(i));
            }
        } catch (FileNotFoundException e) {
            // a file must have been deleted manually!
            return null;
        }         redundantOpCount++;
        journalWriter.append(READ + ' ' + key + '\n');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }         return new Snapshot(key, entry.sequenceNumber, ins);
    }

同样可以看到get方法也是同步的,它的作用就是根据key返回一个Snapshot对象,可以看到在该方法中同样先调用 validateKey(key);进行合法性检测,如果合法则通过key获取缓存entry,如果entry为空或当前不可读则返回null,否则根据valueCountd的值创建valueCount个文件输入流,这些文件输入流的源即为entry中CLEAN记录的缓存,即 ins[i] = new FileInputStream(entry.getCleanFile(i));然后调用journalWriter.append(READ
+ ' ' + key + '\n');向缓存日志文件中写入一个READ记录行,最后通过key和文件输入流数组来构造一个Snapshot对象,将其返回。当该值返回后会将其移动到缓存队列的头部(If a value is returned, it is moved to the head of the LRU queue)

得到Snapshot对象后,通过该对象的public InputStream getInputStream(int index)方法可以获取到缓存的文件输入流,通过该文件输入流即可将缓存的记录转换为Bitmap对象。

二DiskLruCache的使用

同样DiskLruCache的使用也主要包括上个模块,即创建磁盘缓存,向磁盘缓存中添加记录,从缓存中获取记录。下面先简单介绍这三个模块的使用,然后结合LruCache和DiskLruCache给出安卓缓存策略的完整代码。

创建缓存:创建缓存主要使用的是open函数:public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

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();
}
return new File(cachePath + File.separator + uniqueName);
} 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 mDiskLruCache = null;
try {
File cacheDir = getDiskCacheDir(context, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}

其中getDiskCacheDir函数用来获取缓存的路径,将其返回值作为参数传给open函数的第一个参数,getAppVersion用来获取App的版本号,将其返回值作为参数传给open函数的第二个参数,第三个参数一般置为1,第四个参数一般指定为10M即可。

写入缓存:写入缓存主要是通过DiskLruCache.Editor类来完成的,该类是通过DiskLruCache的edit()方法来获取的。通常写入磁盘缓存是从网络上获取然后写入缓存的,因此我们得定义一个线程从网络上获取图片。

public String hashKeyFromUrl(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();
} new Thread(){
@Override
public void run() {
try {
String imageUrl = "http://www.baidu.com/logo.jpg";
String key = hashKeyFromUrl(imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start(); 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;
}

其中hashKeyFromUrl这个函数用来将网络上图片的Url装换为key,因为网络上的Url可能包含不合法字符,这个在前面的源码分析中已经讲解过。

然后通过mDiskLruCache.edit(key);通过key构造一个Editor对象,然后editor.newOutputStream(DISK_CACHE_INDEX)获取文件输出流(DISK_CACHE_INDEX通常指定为0),然后将该输出流和网络上图片的Url作为参数传递给downloadUrlToStream(String urlString, OutputStream outputStream) 函数,该函数的作用是通过制定的图片的Url和OutputStream 将网络上的图片通过outputStream写入到本地文件中,这里传入的是DiskLruCache的输出流,所以就将其写入到了磁盘缓存中。注意该操作要在一个子线程中进行,下载完成之后还用调用editor的commit方法才能将其真真正正写入缓存。如果下载过程出现错误,则会通过Editor的abort()函数来回退整个操作。

获取缓存:获取缓存主要是通过public synchronized Snapshot get(String key) 函数来完成的。代码如下:

try {
String imageUrl = "http://www.baidu.com/logo.jpg";
String key = hashKeyFromUrl(imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) {
FileInputStream fis =(FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
Bitmap bitmap = BitmapFactory.decodeStream(fis);//注意此种方式未对网络上获取的图片进行压缩处理
mImage.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}

即调用mDiskLruCache.get(key);获取DiskLruCache.Snapshot对象,通过该对象的snapShot.getInputStream(DISK_CACHE_INDEX);获取输入流,获取到该输入流后就基本上和本地文件操作是类似的,很容易将其转换为一个Bitmap对象,注意上述代码中未对获取的图片进行压缩处理,直接显示在ImageView控件上这是不妥的,关于图片压缩的内容请参看我的博客:安卓图片压缩技术

好了,以上就是本人理解的关于DiskLruCache相关的知识点,看官如果觉得不错,请记得点击下方的“顶”或“赞”按钮给我一点小小的鼓励哦,看官也可以看看我的其它博客的文章哦!

【安卓中的缓存策略系列】安卓缓存策略之磁盘缓存DiskLruCache的更多相关文章

  1. 【Web缓存机制系列】2 – Web浏览器的缓存机制

    Web缓存的工作原理 所有的缓存都是基于一套规则来帮助他们决定什么时候使用缓存中的副本提供服务(假设有副本可用的情况下,未被销毁回收或者未被删除修改).这些规则有的在协议中有定义(如HTTP协议1.0 ...

  2. 【Web缓存机制系列】2 – Web浏览器的缓存机制-(新鲜度 校验值)

    Web缓存的工作原理 所有的缓存都是基于一套规则来帮助他们决定什么时候使用缓存中的副本提供服务(假设有副本可用的情况下,未被销毁回收或者未被删除修改).这些规则有的在协议中有定义(如HTTP协议1.0 ...

  3. 安卓中的消息循环机制Handler及Looper详解

    我们知道安卓中的UI线程不是线程安全的,我们不能在UI线程中进行耗时操作,通常我们的做法是开启一个子线程在子线程中处理耗时操作,但是安卓规定不允许在子线程中进行UI的更新操作,通常我们会通过Handl ...

  4. Cache【硬盘缓存工具类(包含内存缓存LruCache和磁盘缓存DiskLruCache)】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 内存缓存LruCache和磁盘缓存DiskLruCache的封装类,主要用于图片缓存. 效果图 代码分析 内存缓存LruCache和 ...

  5. 【安卓中的缓存策略系列】安卓缓存策略之综合应用ImageLoader实现照片墙的效果

    在前面的[安卓缓存策略系列]安卓缓存之内存缓存LruCache和[安卓缓存策略系列]安卓缓存策略之磁盘缓存DiskLruCache这两篇博客中已经将安卓中的缓存策略的理论知识进行过详细讲解,还没看过这 ...

  6. 【安卓中的缓存策略系列】安卓缓存之内存缓存LruCache

    缓存策略在移动端设备上是非常重要的,尤其是在图片加载这个场景下,因为图片相对而言比较大会花费用户较多的流量,因此可用缓存方式来解决,即当程序第一次从网络上获取图片的时候,就将其缓存到存储设备上,这样在 ...

  7. 网络图片的获取以及二级缓存策略(Volley框架+内存LruCache+磁盘DiskLruCache)

    在开发安卓应用中避免不了要使用到网络图片,获取网络图片很简单,但是需要付出一定的代价——流量.对于少数的图片而言问题不大,但如果手机应用中包含大量的图片,这势必会耗费用户的一定流量,如果我们不加以处理 ...

  8. 安卓中的Model-View-Presenter模式介绍

    转载自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0425/2782.html 英文原文:Introduction to M ...

  9. 分享一个安卓中异步获取网络图片并自适应大小的第三方程序(来自github)

    安卓中获取网络图片,生成缓存 用安卓手机,因为手机流量的限制,所以我们在做应用时,要尽量为用户考虑,尽量少耗点用户的流量,而在应用中网络图片的显示无疑是消耗流量最大的,所以我们可以采取压缩图片或者将图 ...

随机推荐

  1. 基于Mapxtreme for JAVA的电子地图设计与实现

    基于Mapxtreme for JAVA的电子地图设计与实现学生毕业设计,适合测绘类专业研究目标:        开发一个基于MapXtreme for JAVA的校园电子地图项目,使用MapInfo ...

  2. java实现生产者/消费者的三种方式

    package com.wenki.thread; import java.util.LinkedList; import java.util.concurrent.LinkedBlockingQue ...

  3. C语言程序设计第五次作业——循环结构

    (一)改错题 1.题目:输出华氏摄氏温度转换表:输入两个整数lower和upper,输出一张华氏摄氏温度转换表,华氏温度的取值范围是{lower,upper},每次增加2℉.计算公式如下: c = 5 ...

  4. pix2code:从截图生成图形用户界面代码

    将设计人员创建的图形用户界面截图转换为计算机代码是开发人员为构建定制的软件,网站和移动应用程序而进行的一项典型任务. 在本文中,我们展示了深入的学习方法可以用于训练一个端对端的模型,以便从三个不同的平 ...

  5. struts2中action的class属性值意义

    整合了spring就不同了,orz struts2单独使用时action由struts2自己负责创建:与spring集成时,action实例由spring负责创建(依赖注入).这导致在两种情况下str ...

  6. nginx模块,模块的配置使用

    nginx模块官方模块(默认支持的)第三方模块 1. --with-http_stub_status_module nginx的客户端状态 配置syntax: sub_status;default:- ...

  7. The specified JRE installation does not exist异常的原因和解决办法

    今天,回首为了学习新框架,于是将JDK的版本从1.7开发标配版换成了1.8,一切前期很顺利,完成了新框架的测试和体验,但在运行原有项目的时候问题出现了,爆出了The specified JRE ins ...

  8. scan函数用法详解

    scan函数: scan(s,n,"char")表示从字串string中以char为分隔符提取第n个字串 功能(function):从字符表达式s中搜取给定的n个单词语法(synt ...

  9. opencv视屏流嵌入wxpython框架

    前几篇博客分享搭建人脸识别与情绪判断的环境和源码,但是没有UI,界面很难看,一打开就是opencv弹出的一个视屏框.处女座的我看着非常难受,于是决定做一个UI,稍微规矩好看一点,再怎么说,这样的话也算 ...

  10. Spring + mybatis 集成

    具体项目可参照:https://github.com/LuoXiaoyi/springmvc 一.环境准备:Spring4.3.5 + Mybatis3.4.6 + Mybatis-Spring 1. ...