DiskLruCache 描述:
DiskLruCache 是用来缓存一些数据,比如网络访问的Json,加载的图片等等。 LruCache
是把数据缓存到内存中,而DisLruCache 是把数据缓存到设备里面。DiskLruCache 使用和LruCache 的一样的设计思想。参考LruCahce 的地址:https://blog.csdn.net/u013270444/article/details/104852681
DiskLruCache类的职责:
- 负责维护缓存列表表和缓存文件的对应关系
- 负责维护缓存区域的大小,当缓存的内容超过设定的额度后,使用最近最少使用算法,删除不常用的缓存。
- 提供缓存文件的存取接口
实现原理分析:
DiskLruCache 有一张journal 的日志文件,记录了所有的缓存的文件。
文件格式如下:
libcore.io.DiskLruCache
1
100
2
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
文件含义如下:
最上面的五行,第一行表示这个是一个DiskLruCache 日志文件,是个字符串没有特别的含义,第二行表示硬盘缓存的版本,第三行表示app
版本号,第四行表示一个文件key 对应几个文件。第五行是一个空行。
每一条数据的含义:
CLEAN: 表示数据是干净的
DIRTY : 表示这条数据是脏的,不会使用这条数据。
REMOVE: 表示这条数据被移除了。
READ: 表示这个数据被读取了一次。
3400330d1dfc7f3f7f4b8d4d803dfcf6 表示当前这条数据的key,当我们调用get 的时候会用到。
后面的832 21054
表示这个数据对应的一个或多个文件。前面那个代表第一个文件的大小,第二个表示第二个文件的大小,以此类推。
当我们初始化DiskLruCache 的时候,会读取这个日志文件。把文件里面的数据读取出来。
只有当读取到CLEAN 的条目的时候,缓存才是可用的。
当读取到remove 的时候,会把这条数据从Map 里面移除掉。
相关代码如下:
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
//如果是remove 那么移除掉这条数据
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
// clean 的数据 才是可读的
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
//dirty 的数据是不可读取的
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
关于DIRTY CLEAN REMOVE READ的写入时机
当我们存一条缓存数据到设备上的时候,DiskLruCache 会先写一条Dirty> 的信息到日志里面,当缓存文件成功写入到设备的时候,会写一条Clean 的数据,key 和 dirty 的key
是一样的。但是如果写入文件失败,就会写一条Remove 的数据。当一条缓存数据被读取的时候,会写一条Read 的数据。当我们删除缓存的时候,会有一条Remove 的数据。
作为一个缓存的提供者,那么肯定有存放和读取的操作。我们看下:
存放:
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.readable | success) {
entry.readable = true;
journalWriter.append(CLEAN);
journalWriter.append(' ');
journalWriter.append(entry.key);
journalWriter.append(entry.getLengths());
journalWriter.append('\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.append(REMOVE);
journalWriter.append(' ');
journalWriter.append(entry.key);
journalWriter.append('\n');
}
flushWriter(journalWriter);
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
如果发现大小超过了最大的缓存大小,那么执行清理的Runnable
private final Callable<Void> cleanupCallable = new Callable<Void>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
trimToSize();
return null;
}
};
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
执行删除文件的操作
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
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);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
lruEntries.remove(key);
return true;
}
读取:
public synchronized Value get(String key) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (!entry.readable) {
return null;
}
redundantOpCount++;
journalWriter.append(READ);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}
如上面的代码,每次读取都会写一个Read.
/** A snapshot of the values for an entry. */
public final class Value {
private Value(String key, long sequenceNumber, File[] files, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.files = files;
this.lengths = lengths;
}
/**
* Returns an editor for this snapshot's entry, or null if either the
* entry has changed since this snapshot was created or if another edit
* is in progress.
*/
public Editor edit() throws IOException {
return DiskLruCache.this.edit(key, sequenceNumber);
}
public File getFile(int index) {
return files[index];
}
/** Returns the string value for {@code index}. */
public String getString(int index) throws IOException {
InputStream is = new FileInputStream(files[index]);
return inputStreamToString(is);
}
/** Returns the byte length of the value for {@code index}. */
public long getLength(int index) {
return lengths[index];
}
}
DiskLruCache 的使用:
初始化:
private void initLruCache() {
//初始化lru cache
File file = new File("/sdcard/Lru/");
file.mkdir();
try {
mDiskLruCache = DiskLruCache.open(file,1,1,1024 * 1024 * 250);
} catch (IOException e) {
e.printStackTrace();
}
}
存储:
/**
* 注意 mDiskLruCache = DiskLruCache.open(file,1,1,1024 * 1024 * 250);
* 如果说valueCount 参数你设置的2个,那么你提交的时候 不能只提交一个 会直接报错
*/
try {
DiskLruCache.Editor edit = mDiskLruCache.edit("1238988383");
File file1 = edit.getFile(0);
PrintStream fileOutputStream = new PrintStream(new FileOutputStream(file1));
fileOutputStream.print("测试测试");
fileOutputStream.flush();
fileOutputStream.close();
edit.commit();
} catch (IOException e) {
e.printStackTrace();
}
读取:
try {
DiskLruCache.Value value = mDiskLruCache.get("1238988383");
//判空处理
if (value != null) {
String string = value.getString(0);
Toast.makeText(getActivity(), string, Toast.LENGTH_SHORT).show();
}
} catch (IOException e) {
e.printStackTrace();
}
删除
try {
mDiskLruCache.remove("1238988383");
//如果没有flush 的话 可能已经删除的item 不会写到journal 里面
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
注意:
如果你写的一个key 对应了两个value,但是如果你只写了一个文件,那么就会报下面的错。
2020-03-30 17:21:46.970 24743-24743/com.pipiyang.cn03 E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.pipiyang.cn03, PID: 24743
java.lang.IllegalStateException: Newly created entry didn't create value for index 1
at com.bumptech.glide.disklrucache.DiskLruCache.completeEdit(DiskLruCache.java:518)
at com.bumptech.glide.disklrucache.DiskLruCache.access$2100(DiskLruCache.java:90)
at com.bumptech.glide.disklrucache.DiskLruCache$Editor.commit(DiskLruCache.java:835)
at com.example.fragment.DiskLruCacheFragment.putSomeThing(DiskLruCacheFragment.java:98)
at com.example.fragment.DiskLruCacheFragment.onCreateView(DiskLruCacheFragment.java:85)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2439)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1460)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:802)
at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManager.java:2625)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2411)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2366)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2273)
at androidx.fragment.app.FragmentManagerImpl$1.run(FragmentManager.java:733)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:230)
at android.app.ActivityThread.main(ActivityThread.java:7742)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034)
相关截图资料:
日志文件读取到内存中的数据结构是什么?
final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
private final class Entry {
final String key;
/** Lengths of this entry's files. */
final long[] lengths;
final File[] cleanFiles;
final File[] dirtyFiles;
/** True if this entry has ever been published. */
boolean readable;
/** The ongoing edit or null if this entry is not being edited. */
Editor currentEditor;
/** The sequence number of the most recently committed edit to this entry. */
long sequenceNumber;
}
怎么实现的Lru?
和LruCache 一样,使用的是LinkedHashMap,经常访问的放到链表最后面,移除的时候,移除最不常使用的。
日志文件会不会越来越大?
不会,如果冗余的日志多于2000 条的时候,会进行日志重写。触发时机为打开日志的时候,和往DiskLruCache 存放数据的时候。
关键 变量为redundantOpCount (冗余的日志行数),如果这个变量大于2000,就会重写日志。
/**
* We only rebuild the journal when it will halve the size of the journal and eliminate at least
* 2000 ops.
*/
boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold
&& redundantOpCount >= lruEntries.size();
}
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
try {
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
};
redundantOpCount 改变的地方:
okhttp3.internal.cache.DiskLruCache#readJournal
在我们初始化读取日志的时候 会用文件行数减去我们真实数据的个数 得到当前冗余的数量
redundantOpCount = lineCount - lruEntries.size();
okhttp3.internal.cache.DiskLruCache#removeEntry
redundantOpCount++;
当我们移除一个无用的数据的时候,也会增加redundantOpCount 的个数
okhttp3.internal.cache.DiskLruCache#get
redundantOpCount++;
因为我们读取到的时候,也会写日志,所以也会增加
okhttp3.internal.cache.DiskLruCache#completeEdit
一个文件写完成 也会增加 因为会有两条日志 一条是DIRTY 一条是CLEAN
每次重写日志之后,都没有DIRTY,只有CLEAN.