这几天一直优哉游哉的查看图片三级缓存,其中DiskLruCache磁盘缓存是三级缓存中非常重要的知识点(另外有内存软引用,LruCache缓存工具类、线程、线程锁等会另外详细阐述)
DiskLruCache网上有很多阐述,基本都是对的,我再次不过是整合一下,或者可以认为是ctrl+c,ctrl+v。只是因为我将代码都彻底“看”了一遍,还有个非常重要的因素,我对代码进行了代码检测,可以说基本规范还是很明了的。
不得不说,写的真心好,而且很有调理。因为水平有限,到现在只是看懂了思路,代码还有很多地方没有彻底连贯,搞懂。
一、DiskLruCache作用
见名知意,这个应该是软件开发最基本的东西,所以DiskLruCache是引用于磁盘存储的类。
主要功能是为了防止安卓手机加载文件(主要是图片)出现OOM(Out Of Memory,即内存泄漏)。
但是防止OOM,直接引用LruCache就OK了。为什么还需要引入磁盘缓存这个类。磁盘缓存其根本目的还是为了增强用户体验,如果一个app每次进入都需要从网络获取,尤其是文件非常耗流量,而且加载也需要时间,但是如果我们加入磁盘缓存最后一道防线,判断如果磁盘中有资源,那么直接从磁盘加载,那么是不是即解决了耗流量问题又节约时间(当然也是牺牲手机存储为代价的,当然这是小量的存储,何乐而不为)。
磁盘缓存的位置,可以自由地进行设定,但是通常情况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/<application package>/cache 这个路径。选择在这个位置有两点好处:第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现删除程序之后手机上还有很多残留数据的问题。
二、DiskLruCache使用思路
转自http://blog.it985.com/7784.html
1.打开缓存
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException
open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。
其中缓存地址前面已经说过了,通常都会存放在 /sdcard/Android/data/<application package>/cache 这个路径下面,但同时我们又需要考虑如果这个手机没有SD卡,或者SD正好被移除了的情况,因此比较优秀的程序都会专门写一个方法来获取缓存地址,如下所示:
private File getDiskCacheDir(Context context, String uniqueName)
可以看到,当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,否则就调用getCacheDir()方法来获取缓存路径。前者获取到的就是 /sdcard/Android/data/<application package>/cache 这个路径,而后者获取到的是 /data/data/<application package>/cache 这个路径。
接着又将获取到的路径和一个uniqueName进行拼接,作为最终的缓存路径返回。那么这个uniqueName又是什么呢?其实这就是为了对不同类型的数据进行区分而设定的一个唯一值,比如说在网易新闻缓存路径下看到的bitmap、object等文件夹。
接着是应用程序版本号,我们可以使用如下代码简单地获取到当前应用程序的版本号:
public static int getAppVersion(Context context)
需要注意的是,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。
后面两个参数就没什么需要解释的了,第三个参数传1,第四个参数通常传入10M的大小就够了,这个可以根据自身的情况进行调节。
2.写入缓存
private boolean downloadUrlToStream(String urlString, OutputStream outputStream)
这段代码相当基础,相信大家都看得懂,就是访问urlString中传入的网址,并通过outputStream写入到本地。有了这个方法之后,下面我们就可以使用DiskLruCache来进行写入了,写入的操作是借助DiskLruCache.Editor这个类完成的。类似地,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,接口如下所示:
public Editor edit(String key) throws IOException
可以看到,edit()方法接收一个参数key,这个key将会成为缓存文件的文件名,并且必须要和图片的URL是一一对应的。那么怎样才能让key和图片的URL能够一一对应呢?直接使用URL来作为key?不太合适,因为图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。其实最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,并且只会包含0-F这样的字符,完全符合文件的命名规则。
那么我们就写一个方法用来将字符串进行MD5编码,代码如下所示:
public String hashKeyForDisk(String key)
代码很简单,现在我们只需要调用一下hashKeyForDisk()方法,并把图片的URL传入到这个方法中,就可以得到对应的key了。
有了DiskLruCache.Editor的实例之后,我们可以调用它的newOutputStream()方法来创建一个输出流,然后把它传入到downloadUrlToStream()中就能实现下载并写入缓存的功能了。注意newOutputStream()方法接收一个index参数,由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了。在写入操作执行完之后,我们还需要调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。
由于这里调用了downloadUrlToStream()方法来从网络上下载图片,所以一定要确保这段代码是在子线程当中执行的。注意在代码的最后我还调用了一下flush()方法,这个方法并不是每次写入都必须要调用的,但在这里却不可缺少,我会在后面说明它的作用。
3.读取缓存
缓存已经写入成功之后,接下来我们就该学习一下如何读取了。读取的方法要比写入简单一些,主要是借助DiskLruCache的get()方法实现的,接口如下所示:
public synchronized Snapshot get(String key) throws IOException
4.移除缓存
学习完了写入缓存和读取缓存的方法之后,最难的两个操作你就都已经掌握了,那么接下来要学习的移除缓存对你来说也一定非常轻松了。移除缓存主要是借助DiskLruCache的remove()方法实现的,接口如下所示:
public synchronized boolean remove(String key) throws IOException
4.其他
除了写入缓存、读取缓存、移除缓存之外,DiskLruCache还提供了另外一些比较常用的API,我们简单学习一下。
1. size()
这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。比如网易新闻中就有这样一个功能
2.flush()
这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。
3.close()
这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。
4.delete()
这个方法用于将所有的缓存数据全部删除,比如说网易新闻中的那个手动清理缓存功能,其实只需要调用一下DiskLruCache的delete()方法就可以实现了。
由于现在只缓存了一张图片,所以journal中并没有几行日志,我们一行行进行分析。第一行是个固定的字符串“libcore.io.DiskLruCache”,标志着我们使用的是DiskLruCache技术。第二行是DiskLruCache的版本号,这个值是恒为1的。第三行是应用程序的版本号,我们在open()方法里传入的版本号是什么这里就会显示什么。第四行是valueCount,这个值也是在open()方法中传入的,通常情况下都为1。第五行是一个空行。前五行也被称为journal文件的头,这部分内容还是比较好理解的,但是接下来的部分就要稍微动点脑筋了。
第六行是以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。通常我们看到DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
前面我们所学的size()方法可以获取到当前缓存路径下所有缓存数据的总字节数,其实它的工作原理就是把journal文件中所有CLEAN记录的字节数相加,求出的总合再把它返回而已。
除了DIRTY、CLEAN、REMOVE之外,还有一种前缀是READ的记录,这个就非常简单了,每当我们调用get()方法去读取一条缓存数据时,就会向journal文件中写入一条READ记录。因此,像网易新闻这种图片和数据量都非常大的程序,journal文件中就可能会有大量的READ记录。
那么你可能会担心了,如果我不停频繁操作的话,就会不断地向journal文件中写入数据,那这样journal文件岂不是会越来越大?这倒不必担心,DiskLruCache中使用了一个redundantOpCount变量来记录用户操作的次数,每执行一次写入、读取或移除缓存的操作,这个变量值都会加1,当变量值达到2000的时候就会触发重构journal的事件,这时会自动把journal中一些多余的、不必要的记录全部清除掉,保证journal文件的大小始终保持在一个合理的范围内。
三、DiskLruCache源码注解
代码注释如下:
/*
* 文 件 名: DiskLruCache.java
* 版 权: Anhui Shixu Intelligent Technology CO.,LTD. Copyright YYYY-YYYY, All rights reserved
* 描 述: <描述>
* 修 改 人: **
* 修改时间: 2016-10-8
* 跟踪单号: <跟踪单号>
* 修改单号: <修改单号>
* 修改内容: <修改内容>
*/
package com.example.demo.utils.imagecache;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Closeable 是可以关闭的数据源或目标。调用 close 方法可释放对象保存的资源(如打开文件)
*
* @author gaoqiang
* @version [版本号, 2016-10-8]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class DiskLruCache implements Closeable
{
/** 图片保存的磁盘位置 */
private final File directory;
/** 日志文件,这个就是我们可以直接在disk里面看到的journal文件 主要就是对他的操作 */
private final File journalFile;
/** 构建临时缓存文件,完成后将缓存文件重命名为journal */
private final File journalFileTmp;
/** app版本号 */
private final int appVersion;
/** 最大容量 */
private final long maxSize;
/** 每个entry对应的缓存文件的格式 一般为1 */
private final int valueCount;
/** 文件名 */
private static final String JOURNAL_FILE = "journal";
/** 临时文件名 */
private static final String JOURNAL_FILE_TMP = "journal.tmp";
/** 输入流缓冲区大小设置为8k */
private static final int IO_BUFFER_SIZE = 8 * 1024;
/** 字符串信息,用于和io流对比 */
static final String MAGIC = "libcore.io.DiskLruCache";
/** 版本号,用于和io流对比 */
static final String VERSION_1 = "1";
/** remove标识,表示移除 */
private static final String REMOVE = "REMOVE";
/** clean标识,表示提取有效信息 */
private static final String CLEAN = "CLEAN";
/** dirty标识,表示需要重新编辑 */
private static final String DIRTY = "DIRTY";
/** read标识,表示只读 */
private static final String READ = "READ";
/** 磁盘被应用多少 */
private long size = 0;
/** LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap */
private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true);
/** 这个是专门用于写入日志文件的writer */
private Writer journalWriter;
/** 这个值大于一定数目时 就会触发对journal文件的清理了 */
private int redundantOpCount;
/** 为了区分旧的和当前的快照,每个条目都在每次提交一个编辑时给出一个序列号。快照是陈旧的,如果它的序列号不等于它的条目的序列号 */
private long nextSequenceNumber = 0;
/** 任意序列号 */
static final long ANY_SEQUENCE_NUMBER = -1;
/** 新建一个线程池 */
private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
/** 线程Callable 和 Runnable 的使用方法大同小异 */
private final Callable<Void> cleanupCallable = new Callable<Void>()
{
@Override
public Void call()
throws Exception
{
synchronized (DiskLruCache.this)
{
if (journalWriter == null)
{
return null; // closed
}
trimToSize();
// 是否超出了范围,否则就重构日志文件
if (journalRebuildRequired())
{
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
/**
* 创建一个新的journal,省去了冗余信息。如果它存在,取代当前的日记
*
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
private synchronized void rebuildJournal()
throws IOException
{
if (journalWriter != null)
{
journalWriter.close();
}
// 这个地方要注意了 writer 是指向的journalFileTmp 这个日志文件的缓存文件
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初始化
journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
}
/**
* 整理长度,不被允许超过最大长度maxSize
*
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
private void trimToSize()
throws IOException
{
while (size > maxSize)
{
// Map.Entry<String, Entry> toEvict = lruEntries.eldest();
final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
/**
* 因为这里涉及到写文件 所以要先校验一下写日志文件的writer 是否被正确的初始化
*
* @see [类、类#方法、类#成员]
*/
private void checkNotClosed()
{
if (journalWriter == null)
{
throw new IllegalStateException("cache is closed");
}
}
/**
* 验证key是否正常,网络地址 进行md5加密,而对这个key的格式在这里是有要求的 所以这一步就是验证key是否符合规范
*
* @param key 键值
* @see [类、类#方法、类#成员]
*/
private void validateKey(String key)
{
if (key.contains(" ") || key.contains("\n") || key.contains("\r"))
{
throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key + "\"");
}
}
/**
* 删除键值对
*
* @param key 键值
* @return 是否被删除
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
public synchronized boolean remove(String key)
throws IOException
{
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null)
{
return false;
}
for (int i = 0; i < valueCount; i++)
{
File file = entry.getCleanFile(i);
if (!file.delete())
{
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(REMOVE + ' ' + key + '\n');
lruEntries.remove(key);
if (journalRebuildRequired())
{
executorService.submit(cleanupCallable);
}
return true;
}
/**
* <默认构造函数>
*/
public 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;
}
/**
* 非法参数异常
*
* @param string 传递异常信息
* @return 处理非法参数异常
* @throws IllegalArgumentException 异常处理
* @see [类、类#方法、类#成员]
*/
private static IllegalArgumentException invalidParameter(String string)
throws IllegalArgumentException
{
throw new IllegalArgumentException(string);
}
/**
* 打开缓存,如果没有存在的话,创建一个缓存
*
* @param directory 缓存目录
* @param appVersion app版本
* @param valueCount 每次缓存的个数,必须是有效的,即大于1,小于maxsize
* @param maxSize 最大缓存个数
* @return 硬盘缓存
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException
{
// 非法参数异常
if (maxSize <= 0)
{
invalidParameter("maxSize <= 0");
}
if (valueCount <= 0)
{
invalidParameter("valueCount <= 0");
}
// 构造方法赋值
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
// 如果journalFile日志文件存在,就直接去构建entry列表
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();
}
}
// 如果日志文件不存在,需要新建
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
/**
* 关闭缓存并删除其存储的值的所有值。这将删除缓存目录中的所有文件,包括未由缓存创建的文件。
*
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
public void delete()
throws IOException
{
close();
deleteContents(directory);
}
/**
* 递归删除所有相关文件
*
* @param dir 文件
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
public static void deleteContents(File dir)
throws IOException
{
File[] files = dir.listFiles();
if (files == null)
{
throw new IllegalArgumentException("not a directory: " + dir);
}
for (File file : files)
{
if (file.isDirectory())
{
deleteContents(file);
}
if (!file.delete())
{
throw new IOException("failed to delete file: " + file);
}
}
}
/**
* 计算初始大小,并收集作为打开缓存的一部分的垃圾。对不符合条件的数据进行删除
*
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
private void processJournal()
throws IOException
{
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext();)
{
Entry entry = i.next();
if (entry.currentEditor == null)
{
for (int t = 0; t < valueCount; t++)
{
size += entry.lengths[t];
}
}
else
{
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++)
{
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
/**
* 本意是如果临时文件存在将其删除,但是现在只是判断如果文件存在并且未执行删除操作将抛出异常
*
* @param file 文件
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
private static void deleteIfExists(File file)
throws IOException
{
// try {
// Libcore.os.remove(file.getPath());
// } catch (ErrnoException errnoException) {
// if (errnoException.errno != OsConstants.ENOENT) {
// throw errnoException.rethrowAsIOException();
// }
// }
if (file.exists() && !file.delete())
{
throw new IOException();
}
}
/**
* 读取journalFile
*
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
private void readJournal()
throws IOException
{
// 创建输入流读取journalFile,并且设置size
InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
try
{
// 这一段就是读文件头的信息
String magic = readAsciiLine(in);
String version = readAsciiLine(in);
String appVersionString = readAsciiLine(in);
String valueCountString = readAsciiLine(in);
String blank = readAsciiLine(in);
if (!MAGIC.equals(magic) || !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank))
{
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + valueCountString
+ ", " + blank + "]");
}
// 利用读到文件末尾的异常来跳出循环
while (true)
{
try
{
// 构建的lruEntries entry列表
readJournalLine(readAsciiLine(in));
}
catch (EOFException endOfJournal)
{
break;
}
}
}
finally
{
closeQuietly(in);
}
}
/**
* 关闭输入流
*
* @param closeable 关闭的数据源或目标
* @see [类、类#方法、类#成员]
*/
public static void closeQuietly(Closeable closeable)
{
if (closeable != null)
{
try
{
closeable.close();
}
catch (RuntimeException rethrown)
{
throw rethrown;
}
catch (Exception ignored)
{
}
}
}
/**
* 读取Journal文件中的字符串
*
* @param line Journal内容
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
private void readJournalLine(String line)
throws IOException
{
// 以空格问分割线进行分割,并且保存到字符串数组里
String[] parts = line.split(" ");
if (parts.length < 2)
{
throw new IOException("unexpected journal line: " + line);
}
String key = parts[1];
if (parts[0].equals(REMOVE) && parts.length == 2)
{
lruEntries.remove(key);
return;
}
Entry entry = lruEntries.get(key);
if (entry == null)
{
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount)
{
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(copyOfRange(parts, 2, parts.length));
}
else if (parts[0].equals(DIRTY) && parts.length == 2)
{
entry.currentEditor = new Editor(entry);
}
else if (parts[0].equals(READ) && parts.length == 2)
{
// this work was already done by calling lruEntries.get()
}
else
{
throw new IOException("unexpected journal line: " + line);
}
}
/**
* 指定范围内赋值并且返回
*
* @param original 数组
* @param start 开始范围
* @param end 结束范围
* @return 需要的值
* @see [类、类#方法、类#成员]
*/
@SuppressWarnings("unchecked")
private static <T> T[] copyOfRange(T[] original, int start, int end)
{
// 用于判断异常处理优先级
final int originalLength = original.length;
if (start > end)
{
throw new IllegalArgumentException();
}
if (start < 0 || start > originalLength)
{
throw new ArrayIndexOutOfBoundsException();
}
final int resultLength = end - start;
final int copyLength = Math.min(resultLength, originalLength - start);
final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(), resultLength);
// 第一个是要复制的数组,第二个是从要复制的数组的第几个开始,第三个是复制到那,四个是复制到的数组第几个开始,最后一个是复制长度
System.arraycopy(original, start, result, 0, copyLength);
return result;
}
/**
* 返回ASCII字符,不包含\r\n等特殊符
*
* @param in 输入流
* @return 字符串
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
public static String readAsciiLine(InputStream in)
throws IOException
{
// String 对象是不可改变的,StringBuilder 对象是动态对象,允许扩充它所封装的字符串中字符的数量,但是您可以为它可容纳的最大字符数指定一个值
StringBuilder result = new StringBuilder(80);
while (true)
{
int c = in.read();
if (c == -1)
{
throw new EOFException();
}
else if (c == '\n')
{
break;
}
result.append((char)c);
}
int length = result.length();
if (length > 0 && result.charAt(length - 1) == '\r')
{
result.setLength(length - 1);
}
return result.toString();
}
/**
* 关闭此流并释放与此流关联的所有系统资源。如果已经关闭该流,则调用此方法无效。
*
* @throws IOException 异常
*/
@Override
public synchronized void close()
throws IOException
{
if (journalWriter == null)
{
return; // already closed
}
for (Entry entry : new ArrayList<Entry>(lruEntries.values()))
{
if (entry.currentEditor != null)
{
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
}
/**
*
* 编辑类
*
* @author gaoqiang
* @version [版本号, 2016-10-9]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public final class Editor
{
/** 存放信息 */
private final Entry entry;
/** 判断是否错误 */
private boolean hasErrors;
/**
* <默认构造函数>
*/
private Editor(Entry entry)
{
this.entry = entry;
}
/**
* 提交editor,所以他对读者可见。同时释放了编辑锁,所以其他的editor也可以开始编辑了
*
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
public void commit()
throws IOException
{
if (hasErrors)
{
completeEdit(this, false);
remove(entry.key); // the previous entry is stale
}
else
{
completeEdit(this, true);
}
}
/**
* 中止此entry,这样释放了编辑锁,所以另外一个entry可以开始编辑(即在同一个key中)
*
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
public void abort()
throws IOException
{
completeEdit(this, false);
}
/**
* 这个index 其实一般传0 就可以了
*
* DiskLruCache 认为 一个key 下面可以对应多个文件,这些文件 用一个数组来存储,
*
* 所以正常情况下 我们都是一个key 对应一个缓存文件 所以传0
*
* @param index 标记
* @return 输出流
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
public OutputStream newOutputStream(int index)
throws IOException
{
synchronized (DiskLruCache.this)
{
if (entry.currentEditor != this)
{
throw new IllegalStateException();
}
return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
}
}
/**
*
* 文件输出流类
*
* @author gaoqiang
* @version [版本号, 2016-10-9]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
private class FaultHidingOutputStream extends FilterOutputStream
{
/**
* <默认构造函数>
*/
public FaultHidingOutputStream(OutputStream out)
{
super(out);
}
@Override
public void write(int oneByte)
{
try
{
out.write(oneByte);
}
catch (IOException e)
{
hasErrors = true;
}
}
@Override
public void write(byte[] buffer, int offset, int length)
{
try
{
out.write(buffer, offset, length);
}
catch (IOException e)
{
hasErrors = true;
}
}
@Override
public void close()
{
try
{
out.close();
}
catch (IOException e)
{
hasErrors = true;
}
}
@Override
public void flush()
{
try
{
out.flush();
}
catch (IOException e)
{
hasErrors = true;
}
}
}
}
/**
* 这个就是根据缓存文件的大小 更新disklrucache的总大小 然后再日志文件里对该key加入clean的log.
*
* 最后判断是否超过最大的maxSize 以便对缓存进行清理
*
* @param editor 编辑类
* @param success 是否成功
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
private synchronized void completeEdit(Editor editor, boolean success)
throws IOException
{
Entry entry = editor.entry;
if (entry.currentEditor != editor)
{
throw new IllegalStateException();
}
// 如果是成功的,并且没有被加载过
if (success && !entry.readable)
{
for (int i = 0; i < valueCount; i++)
{
if (!entry.getDirtyFile(i).exists())
{
editor.abort();
throw new IllegalStateException("edit didn't create file " + i);
}
}
}
for (int i = 0; i < valueCount; i++)
{
File dirty = entry.getDirtyFile(i);
if (success)
{
if (dirty.exists())
{
File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
}
else
{
deleteIfExists(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success)
{
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success)
{
entry.sequenceNumber = nextSequenceNumber++;
}
}
else
{
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
if (size > maxSize || journalRebuildRequired())
{
executorService.submit(cleanupCallable);
}
}
/**
* 是否被允许重建:只有重建什么时候会使其大小和消除至少2000处
*
* @return 是否重建
* @see [类、类#方法、类#成员]
*/
private boolean journalRebuildRequired()
{
final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD && redundantOpCount >= lruEntries.size();
}
/**
*
* class类,存放需要的信息
*
* @author gaoqiang
* @version [版本号, 2016-10-9]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
private final class Entry
{
/** 键值对 */
private final String key;
/** 这个entry中 每个文件的长度,这个数组的长度为valueCount 一般都是1 */
private final long[] lengths;
/** 曾经被发布过 那他的值就是true */
private boolean readable;
/** 如果未编辑,则无效 */
private Editor currentEditor;
/** 最新序列号 */
private long sequenceNumber;
/**
* <默认构造函数>
*/
private Entry(String key)
{
this.key = key;
this.lengths = new long[valueCount];
}
/**
* get entry长度的内容
*
* @return 一定长度的内容
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
public String getLengths()
throws IOException
{
StringBuilder result = new StringBuilder();
for (long size : lengths)
{
result.append(' ').append(size);
}
return result.toString();
}
/**
* 设置Entry的长度
*
* @param strings 字符串数组
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
private void setLengths(String[] strings)
throws IOException
{
if (strings.length != valueCount)
{
throw invalidLengths(strings);
}
try
{
for (int i = 0; i < strings.length; i++)
{
lengths[i] = Long.parseLong(strings[i]);
}
}
catch (NumberFormatException e)
{
throw invalidLengths(strings);
}
}
/**
* 对ioe异常的处理
*
* @param strings 字符串数组
* @return 异常信息
* @throws IOException 异常处理
* @see [类、类#方法、类#成员]
*/
private IOException invalidLengths(String[] strings)
throws IOException
{
throw new IOException("unexpected journal line: " + Arrays.toString(strings));
}
/**
* 临时文件创建成功后,就会重命名正式文件
*
* @param i 键值下标
* @return 空文件
* @see [类、类#方法、类#成员]
*/
public File getCleanFile(int i)
{
return new File(directory, key + "." + i);
}
/**
* 创建空临时文件
*
* @param i 键值下标
* @return 空临时文件
* @see [类、类#方法、类#成员]
*/
public File getDirtyFile(int i)
{
return new File(directory, key + "." + i + ".tmp");
}
}
/**
* 向文件系统缓冲的操作
*
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
public synchronized void flush()
throws IOException
{
checkNotClosed();
trimToSize();
journalWriter.flush();
}
/**
*
* 这个类持有该entry中每个文件的inputStream 通过这个inputStream 可以读取他的内容。
*
* @author gaoqiang
* @version [版本号, 2016-10-9]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public final class Snapshot implements Closeable
{
/** io流集合 */
private final InputStream[] ins;
/**
* <默认构造函数>
*/
private Snapshot(InputStream[] ins)
{
this.ins = ins;
}
/**
* 返回缓冲流索引值,下标为index
*
* @param index 下标
* @return 输入流
* @see [类、类#方法、类#成员]
*/
public InputStream getInputStream(int index)
{
return ins[index];
}
/**
* 调用 close 方法可释放对象保存的资源(如打开文件)
*
* @throws IOException 异常
*/
@Override
public void close()
throws IOException
{
for (InputStream in : ins)
{
closeQuietly(in);
}
}
}
/**
* 获取Snapshot对象
*
* @param key 键值对
* @return Snapshot对象
* @throws IOException io流异常
* @see [类、类#方法、类#成员]
*/
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;
}
// 打开所有的流,以保证我们找到想要的snapshot。如果我们打开流缓慢,那么可能来自不同的edits
InputStream[] ins = new InputStream[valueCount];
try
{
for (int i = 0; i < valueCount; i++)
{
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
}
catch (FileNotFoundException e)
{
return null;
}
redundantOpCount++;
// //在取得需要的文件以后 记得在日志文件里增加一条记录 并检查是否需要重新构建日志文件
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired())
{
executorService.submit(cleanupCallable);
}
return new Snapshot(ins);
}
/**
* 返回一个正在编辑的Editor,如果有别的在编辑,则返回null
*
* @param key 键值对
* @return editor
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
public Editor edit(String key)
throws IOException
{
return edit(key, ANY_SEQUENCE_NUMBER);
}
/**
* 根据传进去的key 创建一个entry 并且将这个key加入到entry的那个map里 然后创建一个对应的editor,同时在日志文件里加入一条对该key的dirty记录
*
* @param key 键值对
* @param expectedSequenceNumber 序列号
* @return Editor 对象
* @throws IOException 异常
* @see [类、类#方法、类#成员]
*/
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;
// 在创建文件之前刷新日记以防止文件泄漏
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
}
工具类
/*
* 文 件 名: Utils.java
* 版 权: Anhui Shixu Intelligent Technology CO.,LTD. Copyright YYYY-YYYY, All rights reserved
* 描 述: <描述>
* 修 改 人: gaoqiang
* 修改时间: 2016-10-8
* 跟踪单号: <跟踪单号>
* 修改单号: <修改单号>
* 修改内容: <修改内容>
*/
package com.example.demo.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
/**
* 工具类
*
* @author **
* @version [版本号, 2016-10-8]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class Utils
{
/**
* 获取当前应用程序的版本号。
*
* @param context 资源
* @return 版本号
* @see [类、类#方法、类#成员]
*/
public static int getAppVersion(Context context)
{
try
{
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
}
catch (NameNotFoundException e)
{
e.printStackTrace();
}
return 1;
}
/**
* 使用MD5算法对传入的key进行加密并返回。 图片的url一般都会有特殊字符,是不符合这里的验证的
*
* @param key 图片网络全路径
* @return 加密之后的网络全路径
* @see [类、类#方法、类#成员]
*/
public static String hashKeyForDisk(String key)
{
String cacheKey;
try
{
// 拿到一个MD5转换器
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
byte[] bytes = mDigest.digest();
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);
}
cacheKey = sb.toString();
sb = null;
}
catch (NoSuchAlgorithmException e)
{
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
}