美文网首页
DiskLruCache学习

DiskLruCache学习

作者: 王小宝wy | 来源:发表于2017-07-12 14:16 被阅读0次

    一. 用法

    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

    相关文章

      网友评论

          本文标题:DiskLruCache学习

          本文链接:https://www.haomeiwen.com/subject/uhjbhxtx.html