DiskLruCache 源码解析

DiskLruCache 描述:

DiskLruCache 是用来缓存一些数据,比如网络访问的Json,加载的图片等等。 LruCache
是把数据缓存到内存中,而DisLruCache 是把数据缓存到设备里面。DiskLruCache 使用和LruCache 的一样的设计思想。

参考LruCahce 的地址:https://blog.csdn.net/u013270444/article/details/104852681

DiskLruCache类的职责:

  1. 负责维护缓存列表表和缓存文件的对应关系
  2. 负责维护缓存区域的大小,当缓存的内容超过设定的额度后,使用最近最少使用算法,删除不常用的缓存。
  3. 提供缓存文件的存取接口

实现原理分析:

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.

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值