DiskLruCache学习

一. 用法

DiskLruCache是Google官方推荐的磁盘缓存方案,很多优秀的App都在使用这一方案,在Android DiskLruCache完全解析, 硬盘缓存的最佳方案这篇博客中,很详细的介绍了如何使用DiskLruCache,通过这篇博文可以将DiskLruCache的用法总结为以下几个步骤:

1.1 写缓存

  1. 确定缓存目录, 获取App版本号, 调用DiskaLruCache.open创建DiskLruCache对象
  2. 通过DiskLruCache.editor()获取DiskLruCache.Editor对象
  3. 通过Editor.newOutputStream()获取输出流,之后利用该输出流将缓存文件写入磁盘
  4. 调用Editor.commit(),DiskLruCache.flush()刷新日志文件

1.2 读缓存

  1. 通过DiskLruCache.get()获取Snapshot对象
  2. 通过Snapshot.getInputStream()获取输入流,利用该输入流读取缓存文件

1.3 日志文件

DiskLruCache主要通过日志文件来记录和管理缓存文件,在DiskLruCache的源码中有一段注释详细陈述了日志文件的格式:

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

前五行是日志文件的头部信息,其意义分别是

  • 第一行的libcore.io.DiskLruCache是文件的MAGIC, 用来标识该文件是DiskLruCache的日志文件
  • 第二行是DiskLruCache自身的版本号
  • 第三行是App的版本号,通过DiskLruCache.open的第二个参数设置
  • 第四行是DiskLruCache.open的第三个参数,代表一个key值可以缓存多少个Entry
  • 第五行是一个空行

从第六行开始记录了缓存文件的相应操作:

  • 每次调用DiskLruCache.edit()时,都会向日志文件写入一条DIRTY数据,表示当前正在准备写入一条缓存数据, DIRTY后面各一个空格写入缓存的key值
  • 当调用Editor.commit()将缓存写入成功之后,会在DIRTY数据下一行写入一条key值相同的CLEAN数据, CLEAN后面隔一个空格写入相同的key值,key值后隔一个空格写入以字节为单位的该缓存文件的大小,如果在DiskLruCache.open中第三个参数valueCount传大于1的值,那么每一个key值可以对应多个缓存文件,相应的一条CLEAN数据后面就会记录多个缓存文件的大小,其数目等于valueCount;如果调用Editor.abort(),那么会在DIRTY数据下一行下入一条REMOVE记录。也就是说, 每一条DIRTY数据下一行都有条CLEAN数据或REMOVE
    数据,DIRTY数据不可以单独存在,否则这条数据就会被删除掉; 当调用DiskLruCache.get()时都会想日志文件写如一条READ数据,表示正在读取缓存文件

二.源码分析

下面就根据这几个步骤结合源码来看一下DiskLruCache的具体实现

2.1 重要变量和类

2.1.1 变量

  • journalWriter: Writer 用于向日志文件写入内容
  • lruEntries: LinkedHashMap<String, Entry> 每一个缓存文件都有一个对应的Entry对象,lruEntries用来存放key值对应的Entry对象
  • redundantOpCount:记录操作缓存的次数,如果该值达到2000,DiskLruCache就会重新构建日志文件,将其中一些冗余的数据删除
  • size: 记录总有缓存文件总大小
  • maxSize: 所有缓存文件大小的总和的上限值,如果超过该值,那么就会删除一些缓存文件
  • cleanupCallable: Callable 当缓存文件总大小超过上限时会触发该任务,用于清除一些缓存文件,从而减少缓存文件总和

2.1.2 类

  • Snapshot: 调用DiskLruCache.get()会获取一个Snapshot对象,通过Snapshot可以获取缓存文件的输入流
  • Editor: 编辑器,对于缓存文件的操作以及日志文件的更新都是通过这个类完成
  • Entry: 每一个缓存文件都有一个对应的Entry对象, DiskLruCache通过操作Entry对象来完成对缓存文件的操作
  • StrictLineReader: 封装了输入流,提供可以每次读取一行内容的方法

2.2 DiskLruCache.open


    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_BACKUP = "journal.bkp";

    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");
        }

        //如果备份文件存在则使用备份文件
        File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
        if (backupFile.exists()) {
            File journalFile = new File(directory, JOURNAL_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);
        //如果日志文件存在的话,读取日志文件并处理,填充lruEntries, 然后直接返回DiskLruCache对象
        if (cache.journalFile.exists()) {
            try {
                //读取journal文件, 【2.2.1】
                cache.readJournal();
                //处理读取的journal文件内容, 【2.2.2】
                cache.processJournal();
                return cache;
            } catch (IOException journalIsCorrupt) {
                ...
                cache.delete();
            }
        }

        //如果没有已有的日志文件,创建对应的缓存目录, 并初始化日志文件
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        //新建日志文件,【2.2.3】
        cache.rebuildJournal();
        return cache;
    }

  1. 首先检查备份文件是否存在,之后再确定日志文件是否存在, 如果日志文件存在,则删除备份文件,如果日志文件不存在但备份文件村咋,将备份文件重命名为日志文件
  2. 创建DiskLruCache对象,构造函数中几个参数的意义分别是:directory - 缓存目录; appVersion - 应用版本号; valueCount - 一个可以可以缓存几个Entry, 一般都传1; maxSize - 所有缓存文件大小的总和占据的最大存储空间
  3. 如果日志文件已存在,读取日志文件并根据日志文件进行一些必要的操作,如删除以DIRTY开头的文件
  4. 如果日志文件不存在,创建缓存目录,并新建日志文件

2.2.1 DiskLruCache.readJournal

    
    private void readJournal() throws IOException {
        StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
        try {
            //读取第一行魔数
            String magic = reader.readLine();
            //读取第二行version
            String version = reader.readLine();
            //读取第三行appVersion
            String appVersionString = reader.readLine();
            //读取第四行valueCount
            String valueCountString = reader.readLine();
            //读取第五行空行
            String blank = reader.readLine();
            //确保头部信息正确
            if (!MAGIC.equals(magic)
                    || !VERSION_1.equals(version)
                    || !Integer.toString(appVersion).equals(appVersionString)
                    || !Integer.toString(valueCount).equals(valueCountString)
                    || !"".equals(blank)) {
                throw new IOException(...);
            }

            int lineCount = 0;
            while (true) {
                try {
                    //处理这一行内容,【2.2.1.1】
                    readJournalLine(reader.readLine());
                    lineCount++;
                } catch (EOFException endOfJournal) {
                    break;
                }
            }
            //处理了多少行 - lruEntries.size()
            redundantOpCount = lineCount - lruEntries.size();

            // 如果遇到IO异常, 重新构建日志文件
            if (reader.hasUnterminatedLine()) {
                rebuildJournal();
            } else {
                journalWriter = new BufferedWriter(new OutputStreamWriter(
                        new FileOutputStream(journalFile, true), Util.US_ASCII));
            }
        } finally {
            Util.closeQuietly(reader);
        }
    }

从代码中可以看到,读取日志文件每一行内容主要使用了StrictLineReader这个类, 这个类实际上封装了InputStream, 内部有一个缓存数组,每次缓存8192个字节,从而提高了读取的效率,当遇到换行符时即判定为一行内容

  1. 首先读取前五行,校验是否是正确的头部信息
  2. c处理完前五行之后,依次读取每一行内容并根据内容进行操作
  3. 如果遇到IO异常,重新构建日志文件;否则一切正常的话, 初始化journalWriter(用来写入日志文件)

2.2.1.1 DiskLruCache.readJournalLine


    private void readJournalLine(String line) throws IOException {
        //获取第一个空格的位置
        int firstSpace = line.indexOf(' ');
        if (firstSpace == -1) {
            throw new IOException("unexpected journal line: " + line);
        }

        //第一个空格后面是key的起始位置
        int keyBegin = firstSpace + 1;
        //获取第二个空格的位置, 一般如果是CLEAN的话会有第二个空格
        int secondSpace = line.indexOf(' ', keyBegin);
        final String key;
        if (secondSpace == -1) {
            key = line.substring(keyBegin);
            //如果这一行的开头是REMOVE, 从lruEntries中删除key
            if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
                lruEntries.remove(key);
                return;
            }
        } else {
            key = line.substring(keyBegin, secondSpace);
        }

        //从lruEntries中获取key值对应的Entry, 如果lruEntries中没有对应的Entry,生成一个新的Entry并放入lruEntries
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        }

        //如果这一行是以CLEAN开头, 获取第二个空格之后内容
        if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
            String[] parts = line.substring(secondSpace + 1).split(" ");
            entry.readable = true;
            entry.currentEditor = null;
            entry.setLengths(parts);
        }
        //如果这一行是以DIRTY开头,将currentEditor指向一个新的Editor
        else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
            entry.currentEditor = new Editor(entry);
        }
        //如果这一行以READ开头,啥也不干
        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);
        }
    }

  1. 根据空格依次获得每一行的开头标识(DIRTY, CLEAN, REMOVE)以及对应的key值
  2. 如果这一行是以REMOVE开头,从lruEntries中删除key对应的Entry并返回
  3. lruEntries中获取key值对应的Entry,如果没有则生成一个新的Entry对象并放入lruEntries,这个操作的目的是为了保持日志文件和内存中lruEntries的数据的一致性
  4. 如果这一行是以CLEAN开头,获取key值以后的内容(记录一个或多个对象缓存文件的大小),同时标记entry.readable = true, entry.currentEditor = null,即表示该缓存文件为可读的
  5. 如果这一行是以DIRTY开头的,将entry.currentEditor指向一个新创建的Editor对象

2.2.2 DiskLruCache.processJournal

private void processJournal() throws IOException {
        //删除journal.tmp文件
        deleteIfExists(journalFileTmp);
        //遍历lruEntries
        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
            Entry entry = i.next();
            if (entry.currentEditor == null) {
                //如果currentEditor为null, 代表该entry是CLEAN的,增加size
                for (int t = 0; t < valueCount; t++) {
                    size += entry.lengths[t];
                }
            } else {
                //如果currentEditor不为null, 代表该entry是DIRTY的,删除对应的缓存文件和临时文件
                //并从lruEntries中删除该entry
                entry.currentEditor = null;
                for (int t = 0; t < valueCount; t++) {
                    deleteIfExists(entry.getCleanFile(t));
                    deleteIfExists(entry.getDirtyFile(t));
                }
                i.remove();
            }
        }
    }

  1. 如果临时日志文件存在,删除临时文件
  2. 遍历lruEntries, 如果entry.currentEditor == null代表这一个entry是CLEAN的,将entry对应的文件大小统计到所有缓存文件大小总和中;相反,则代表entry是DIRTY的,删除对应的缓存文件,并从lruEntries中删除(NOTE:这里其实针对的是只有DIRTY记录的缓存文件,因为正常情况下,每一条DIRTY数据后都会紧跟一条CLEAN或者REMOVE数据,如果有CLEAN或REMOVE数据,在之前的【2.2.1.1】readJournalLine中都已经经过了处理,其所对应的entry的currentEditor肯定为null或不在lruEntries中了)

2.2.3 DiskLruCache.rebuildJournal

    private synchronized void rebuildJournal() throws IOException {
        if (journalWriter != null) {
            journalWriter.close();
        }

        //先写入journal.tmp文件
        Writer writer = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
        try {
            //写入头部信息
            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) {
                    //如果currentEditor不为null, 则写入DIRTY开头的行
                    writer.write(DIRTY + ' ' + entry.key + '\n');
                } else {
                    //写入以CLEAN开头的行
                    writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
                }
            }
        } finally {
            writer.close();
        }

        if (journalFile.exists()) {
            renameTo(journalFile, journalFileBackup, true);
        }
        renameTo(journalFileTmp, journalFile, false);
        journalFileBackup.delete();

        journalWriter = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
    }

  1. 首先写入固定头部
  2. 遍历lruEntries,根据entry.currentEditor是否等于null,写入DIRTY或者CLEAN记录

2.3 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 {
        //如果journalWritter == null, 抛出异常
        checkNotClosed();
        //校验key是否合法
        validateKey(key);
        //从lruEntries中获取Entry
        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;

        // 先写入DIRTY行
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
    }
  1. 检查journal是否为null, 如果是抛出异常
  2. 检查key值是否符合[a-z0-9_-]{1,120}规则
  3. 根据key获取对应的Entry
  4. 新建一个Editor对象,将entry.currentEditor指向新建的Editor对象
  5. 向日志文件写入DIRTY数据

每当调用editor()时都会先在日志文件中写入一条DIRTY数据,表示正在准备操作缓存文件

2.4 Editor.newOutputStream

    public OutputStream newOutputStream(int index) throws IOException {
            if (index < 0 || index >= valueCount) {
                throw new IllegalArgumentException(...);
            }
            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;
                    }
                }
                //FaultHidingOutputStream是一个代理类,实际还是调用outputStream的方法
                //只不过异常发生时会不会抛出异常
                return new FaultHidingOutputStream(outputStream);
            }
        }

获取的是临时文件的输出流

2.5 Editor.commit

      public void commit() throws IOException {
          if (hasErrors) {
              //如果有错误,删除缓存文件, 【2.5.1】
              completeEdit(this, false);
              remove(entry.key); // The previous entry is stale.
          } else {
              completeEdit(this, true);
          }
          committed = true;
      }

如果IO输出过程有错误发生,从lruEntries中删除相应entry,同时删除对应的缓存文件; 无论是否错误,都会调用DiskLruCache.compleEdit方法, 区别在于错误时传入的第二个参数为false, 正常时为true

hasError是在FaultHidingOutputStream中当出现IO异常时设为true

2.5.1 DiskLruCache.completeEdit

    
    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 (!editor.written[i]) {
                    editor.abort();
                    throw new IllegalStateException(...);
                }
                //确保临时文件存在
                if (!entry.getDirtyFile(i).exists()) {
                    editor.abort();
                    return;
                }
            }
        }

        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;
            //向日志文件写入CLEAN行
            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
            if (success) {
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            //从lruEntries中删除,并向日志文件写入REMOVE行
            lruEntries.remove(entry.key);
            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }
        journalWriter.flush();

        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    }
  1. 确保临时的缓存文件存在,不存在则调用Editor.abort
  2. 如果传入的success = true即代表IO输出成功,则将临时缓存文件重命名为正式的缓存文件,同时更新缓存文件大小总和;如果sucess = false代表出现错误,则删除临时缓存文件
  3. 递增redundantOpCount
  4. 如果IO输出成功,想日志文件写入CLEAN行数据,否则从lruEntries中删除对应的entry,并向日志文件写入REMOVE行内容
  5. 如果缓存文件总大小超出上限或者redundantOpCount大于等于2000时,在线程池中执行cleanupCallable任务

2.5.1.1 DiskLruCache.cleanupCallable

    private final Callable<Void> cleanupCallable = new Callable<Void>() {
        public Void call() throws Exception {
            synchronized (DiskLruCache.this) {
                if (journalWriter == null) {
                    return null; // Closed.
                }
                trimToSize();
                if (journalRebuildRequired()) {
                    //【2.2.3】
                    rebuildJournal();
                    redundantOpCount = 0;
                }
            }
            return null;
        }
    };
    
    private void trimToSize() throws IOException {
        //如果缓存的文件总大小超过了maxSize, 删除缓存文件直到小于上限,并更新日志文件
        while (size > maxSize) {
            Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
            remove(toEvict.getKey());
        }
    }
    
    private boolean journalRebuildRequired() {
        final int redundantOpCompactThreshold = 2000;
        return redundantOpCount >= redundantOpCompactThreshold //
                && redundantOpCount >= lruEntries.size();
    }

2.6 DiskLruCache.flush

   public synchronized void flush() throws IOException {
        //确保journalWriter不等于null
        checkNotClosed();
        //【2.5.1.1】, 删除缓存文件,直到总大小小于上限
        trimToSize();
        journalWriter.flush();
    }

2.7 DiskLruCache.get

    
    public synchronized Snapshot get(String key) throws IOException {
        //确保journalWriter不为null
        checkNotClosed();
        //验证key值符合规则
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            return null;
        }

        if (!entry.readable) {
            return null;
        }
        //创建缓存文件输入流
        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++;
        //更新日志文件,添加READ行
        journalWriter.append(READ + ' ' + key + '\n');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }

        return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
    }
  1. 确保journalWriter不等于null, key值符合规则
  2. 创建缓存文件输入流
  3. 向日志文件中写入READ行
  4. 返回Snapshot对象

2.8 DiskLruCache.close

    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;
    }
  1. 遍历lruEntries,如果entry.currentEditor != null, 调用Editor.abort(abort方法实际调用DiakLruache.completeEdit【2.5.1】, 第二个参数传入false)
  2. 检查缓存总大小是否超出上限,如果超出,删除一些缓存文件直到小于上限
  3. 关闭journalWrite
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容