DiskLruCache完全解析
重新看了下郭霖的disklrucache,现在在这里做下记录
disklrucache 是J神为解决实际开发项目中的本地缓存问题而贡献的一个小框架,整个小框架仅仅有三个类:
DiskLruCache.java
StrictLineReader.java
Util.java
我们在使用的时候的大致步骤是:
1、先得到DiskLruCache这个对象
2、操作这个对象,其中包括存,取,删等操作
3、清理缓存
下面依次介绍一下:
1、先得到DiskLruCache这个对象(源自源码):
/* Opens the cache in {@code directory}, creating a cache if none exists there.
@param directory a writable directory
@param valueCount the number of values per cache entry. Must be positive. @param maxSize the maximum number of bytes this cache should use to store
@throws IOException if reading or writing the cache directory fails /
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”);
}
// If a bkp file exists, use it instead.
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
// 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();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println(“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;
}
我们可以看到,DiskLruCache需要通过open的方法拿到实例对象(因为DiskLruCache本身已经被单例模式了)
这个open方法里面大致做了
journal文件的创建 journal备份文件的创建 根据输入文件目录创建出文件夹 一些全局变量的初始化操作
我们在用open方法时需要传递的参数是
1.要创建的文件目录名
2.当前我们这个应用的版本号
3.设置一个文件名对应的文件数量,一般传1就可以
4.设置缓存文件的大小
需要注意的是,open的第一个参数,我们一般设置成为本机的缓存目录,现在我们的手机都已经内置内存卡了,不过我们还是可以进行一次判断,从而拿到手机中的cache缓存目录
拿缓存目录的方法是:getExternalCacheDir()或者是getCacheDir()
cache缓存目录结构是“/sdcard/Android/data/\
当然如果手机比较老,而且没有挂在sd卡的话,
只能拿到这样的目录了“/data/data/\
2、使用DiskLruCache这个对象进行本地缓存文件的存、取、删
我们通过open(目录,app版本号,同名文件数量,缓存控件大小)拿到DiskLruCache之后,下面我们来看一下DiskLruCache的使用方法:
存:edit +commit
取:get
删:remove
清缓存:delete
1⃣️ 存
使用方法其实很简单,其中需要注意的是,一般我们在存数据的时候,存的数据名称并不是数据本来的名称
比如说网络加载的图片,我们一般是把这个图片的网址当做名称标记,把图片网址经过md5加密方式进行加密
得到一串16进制数组成的String字符串当做文件的名字。
这里提供一个md5工具类,网上随便一搜都能搜到
public String 2Md5(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的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(key,-1) 思路是向以lruEntries为对象名的LinkedHashMap里面存入文件名称和Entry,那么Entry就是以后我们存入的流文件
细心的同学应该已经注意到了,edit方法执行完后,就会返回一个Editor对象,因为我们的edit方法只能传入名称,所以往DiskLruCache里面存数据的重任就交给了Editor了
往Editor里面存入数据的方法是:
public OutputStream newOutputStream(int index) throws IOException {
if (index < 0 || index >= valueCount) {
throw new IllegalArgumentException("Expected index " + index + " to "
+ "be greater than 0 and less than the maximum value count "
+ "of " + valueCount);
}
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.getDirtyFile(index);
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e) {
// Attempt to recreate the cache directory.
directory.mkdirs();
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e2) {
// We are unable to recover. Silently eat the writes.
return NULL_OUTPUT_STREAM;
}
}
return new FaultHidingOutputStream(outputStream);
}
}
主要思路塑造了一个 “dirtyFile”的输出流,这个“dirtyFile”是一个准备好的“缓存文件”,后缀是.tmp(“作者提供了两个文件,一个是dirtyFile 一个是cleanFile,dirtyFile会被后期清理时及时清理掉,只有经过clean后的dirtyFile才能转化成为cleanFile”)
拿到上方的OutputStream之后,我们把从网络下载下来的流写入即可存入到dirtyFile中去了。
2⃣️ 取
取出本地缓存的就比较简单了,下面是源码:
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!
for (int i = 0; i < valueCount; i++) {
if (ins[i] != null) {
Util.closeQuietly(ins[i]);
} else {
break;
}
}
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
那么里面的操作基本上有这么几点
1.检查本地缓存是否关闭,确保处于未关闭状态,这是通过journalWriter来判断的(文件的存取删都跟日志文件有关系)
2.检验传入参数“key”的有效性,应为J神在设计的时候对存入的文件名有特殊的规定,必须是1到120个字符以内的,并且取值范围是0-9,a-Z,只能包括下划线和中划线,所以这对文件名做了限定。
3.下面就直接从LinkedHashMap中读取名称为key的Entry数据了(因为我们存的时候就存到了此linkedHashMap之中)
4.由于日志文件和读 取 删 文件都是相关的,所以在取文件的时候也要对journal进行记录
5.比较重要的一步“redundantOpCount++;”这句代码是对journal 中的日志记录进行计数,当达到一定的临界值的时候
journal文件就会被重新创建。代码如下
/**
* We only rebuild the journal when it will halve the size of the journal
* and eliminate at least 2000 ops.
*/
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
3⃣️ 删
有时候我们需要对特定的文件进行删除,比如我们的splash页面可能会设置成可以变化的图片,这个时候就没有必要对这个图片网址进行缓存了,而我们又不能对某个图片加载框架进行单独的 “不缓存“的设定,所以只能进行这个缓存文件的删除了。
删的操作比取还要简单
/**
* Drops the entry for {@code key} if it exists and can be removed. Entries
* actively being edited cannot be removed.
*
* @return true if an entry was removed.
*/
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.exists() && !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;
}
除了跟取有几个相似点之外,其他的就是对LinkedHashMap进行了remove的操作,并且对日志文件进行了新的添加
4⃣️ 其他的一些api的使用
- size()
这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。
J神的DiskLruCache中存在着size的long类型的变量,变量在linkedhashmap操作的过程中也在不停的加 减控件大小,所以当用户调用的时候,只需要把这个变量返回给用户即可:(注意:我们在初始化的时候已经设定了DiskLruCache的大小,所以J神在editer提交全部的时候对size和缓存大小做了比较,如果size较大,那么就会对Journal日志文件进行重置,对缓存文件即LinkedHashMap进行释放(释放规则是下方第二块代码))
/**
* Returns the number of bytes currently being used to store the values in
* this cache. This may be greater than the max size if a background
* deletion is pending.
*/
public synchronized long size() {
return size;
}
–
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry
remove(toEvict.getKey());
}
}
释放规则我们可以看懂:即释放了最先存入的那个文件,每次当缓存内存空间大于设定的空间时,就会释放最先存入的那个文件。
2.flush()
这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。其实此方法并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。
/** Force buffered operations to the filesystem. */
public synchronized void flush() throws IOException {
checkNotClosed();
trimToSize();
journalWriter.flush();
}
3.close()
这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。
/** Closes this cache. Stored values will remain on the filesystem. */
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;
}
4.delete()
这个方法用于将所有的缓存数据全部删除,比如说软件中的那个手动清理缓存功能,其实只需要调用一下DiskLruCache的delete()方法就可以实现了。
/**
* Closes the cache and deletes all of its stored values. This will delete
* all files in the cache directory including files that weren't created by
* the cache.
*/
public void delete() throws IOException {
close();
Util.deleteContents(directory);
}
-------------------------------------------------------------------------------
/**
* Deletes the contents of {@code dir}. Throws an IOException if any file
* could not be deleted, or if {@code dir} is not a readable directory.
*/
static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files == null) {
throw new IOException("not a readable directory: " + dir);
}
for (File file : files) {
if (file.isDirectory()) {
deleteContents(file);
}
if (!file.delete()) {
throw new IOException("failed to delete file: " + file);
}
}
}
J神的代码大致上就这样,值得注意的是,此类是实现自Closeable这个接口的,这个接口的实际用处不大,但是却强调了本类是一个可以关闭的数据源,调用这个接口的实现方法用于释放这个类中保存的数据源。