美文网首页
DiskLruCache最全面刨析

DiskLruCache最全面刨析

作者: 展翅高飞鹏程万里 | 来源:发表于2020-03-22 16:19 被阅读0次

DiskLruCache概述

简单来说DiskLruCache是一款数据缓存框架。此处的Cache特指应用常需要保存的数据。那么什么数据可以进行存储呢?简单来说,就是可以存储一切由二进制组成的数据,常见的包括JSON数据,文件,图片,音视频等等。特别强调一下,DiskLruCache是多线程安全的,支持多个线程同时操作。因为内部巧妙的使用了synchronized关键字(同步监视器),而且内部维护了一个单线程模式的线程池,用来处理核心任务。这款框架可以植入到任何使用java开发的软件上,不仅仅是安卓,因为没有使用安卓SDK提供的api,而是一款纯Java输出的框架

想要深入学习DiskLruCache框架需要储备的知识

LRU概念?如何使用LinkedHashMap实现Lru策略功能?最好把实现的原理了解清楚(拓展:如何使用栈,数组,队列实现LRU策略,各方式的优劣势)

清楚基本操作文件的Api,重要的需要知道如何实现:一行一行的输出一个文件的内容?提醒(可以使用BufferReader实现,不过最好还是基于InputStream来实现,多了解几种实现方案)

需稍微知道正则的匹配规则

DiskLruCahe缓存设计逻辑

框架设计图

如上图在使用框架之前,框架需要提取日志文件来初始化框架需要的数据,比如提取所有已保存数据到内存中。

框架基本操作规则或需要注意地方

  1. 框架保存的数据以key_value形式。为了增加Value的丰富性,支持一对多的保存方式。简单来说,一个key可以保存多个数据

  2. Key作为缓存数据的ID,在框架中具有唯一性,就像Java集合中的Set一样,不能保存相同Key的数据。在命名上Key的规则需要匹配”[a-z0-9_-]{1,64}“规范,特别注意的是不能保存除这以外的符号,否则将会报错。

  3. 在框架中每一个Value将会以一个文件的新式存在。文件的命名规范为:Key+"."+"index"。此处的index的作用为:标识数据保存的位置。比如要保存一对多形式的数据:wnp => 1 wnp => "abc" , 那么保存数据1的文件名为wnp.0 , 保存的数据”abc“的文件名为wnp.1。这里可以看到允许同一个key保存不一样类型的数据,那么如何区别呢?比如在查询的时候,查找wnp的数据,此时输入的index为0,那么将返回1。如果此时输入的index为1,那么将返回”abc“。

  4. 日志操作文件用于记录、管理保存的数据。比如进行查询,插入,删除的时候都得记录在案。在数据管理上框架采用LRU的缓存策略,通过LRU算法对缓存进行管理,以最近最少使用作为管理的依据,删除最近最少使用的数据,保留最近最常用的数据。日志文件除了保存数据的日志外,还会在文件的开头填入关键信息。比如:文件的魔数“libcore.io.DiskLruCache” ,文件的固定值“1” , 版本号,一个Key对应的保存数据数量的最大值。

下图为日志文件结构介绍

libcore.io.DiskLruCache//魔数
1//固定值
1//版本号
2//key可保存的数据数量,此处为一个Key保存两个数据

DIRTY wnp5 //处于正在编辑状态的wnp5
CLEAN wnp5 6 6 //wnp5的数据保存成功 后面的数字为保存的数据大小,单位为字节。第一个6表示第一个数据保存的大小,第二个6表示第二个数据保存的大小
READ wnp5//外部程序访问了wnp5的数据
REMOVE wnp5 //外部程序删除了wnp5的数据

DisLruCache内部代码逻辑

  • 初始化
//进行初始化
DiskLruCache diskLruCache = DiskLruCache.open(new File("D:\\User\\project\\javaProject\\GennericParadigm\\src\\cache"),1, 2, 50 * 1024 * 1024);
        
//源码解析
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {
    //非法参数判断
    if (maxSize <= 0L) {
        throw new IllegalArgumentException("maxSize <= 0");
    } else if (valueCount <= 0) {
        throw new IllegalArgumentException("valueCount <= 0");
    } else {
        //检查是否存在备份文件,如果存在则进行替换正式文件,或者删除备份文件
        File backupFile = new File(directory, "journal.bkp");
        if (backupFile.exists()) {
            File journalFile = new File(directory, "journal");
            if (journalFile.exists()) {
                backupFile.delete();
            } else {
                renameTo(backupFile, journalFile, false);
            }
        }

        //构造DiskLruCache对象,里面进行一些重要数据的初始化,也就是初始化环境数据
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {//如果正式的日志文件已经存在
            try {
                cache.readJournal();//读取日志文件,进行一些数据的初始化
                cache.processJournal();//清除掉初始化多于,失效的数据
                cache.journalWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
                return cache;
            } catch (IOException var8) {
                System.out.println("DiskLruCache " + directory + " is corrupt: " + var8.getMessage() + ", removing");
                cache.delete();
            }
        }

        directory.mkdirs();
        //重新创建对象,初始化数据
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();//重新创建日志文件
        return cache;
    }
}

private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
    //重点研究一下此线程池的创建输入的参数。核心线程池数为0,最大线程数为1,存活时间为60s,使用链表式的阻塞队列
    this.executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
    //进行清除多余数据或者实行LRU策略时执行的任务
    this.cleanupCallable = new Callable<Void>() {
        public Void call() throws Exception {
            synchronized(DiskLruCache.this) {
                if (DiskLruCache.this.journalWriter == null) {
                    return null;
                } else {
                    DiskLruCache.this.trimToSize();//执行Lru策略模式,进行无效无效数据删除
                    if (DiskLruCache.this.journalRebuildRequired()) {//判断是否需要重构日志文件
                        DiskLruCache.this.rebuildJournal();//重构日志文件
                        DiskLruCache.this.redundantOpCount = 0;
                    }

                    return null;
                }
            }
        }
    };
    this.directory = directory;
    this.appVersion = appVersion;
    this.journalFile = new File(directory, "journal");//真正的Journal日志记录文件
    this.journalFileTmp = new File(directory, "journal.tmp");//Journal的临时文件
    this.journalFileBackup = new File(directory, "journal.bkp");//Journal的备份文件
    this.valueCount = valueCount;
    this.maxSize = maxSize;
}

//提取日志文件的关键信息,初始化框架运行环境
private void readJournalLine(String line) throws IOException {
    int firstSpace = line.indexOf(32);//32为空格的ASIIC码
    if (firstSpace == -1) {
        throw new IOException("unexpected journal line: " + line);
    } else {
        int keyBegin = firstSpace + 1;
        int secondSpace = line.indexOf(32, keyBegin);
        String key;
        if (secondSpace == -1) {
            key = line.substring(keyBegin);
            if (firstSpace == "REMOVE".length() && line.startsWith("REMOVE")) {
                this.lruEntries.remove(key);
                return;
            }
        } else {
            key = line.substring(keyBegin, secondSpace);
        }

        DiskLruCache.Entry entry = (DiskLruCache.Entry)this.lruEntries.get(key);//可以代表提取到存储的数据的ID
        if (entry == null) {
            entry = new DiskLruCache.Entry(key);
            this.lruEntries.put(key, entry);
        }

        //初始化各个数据在日志文件保存的情况。
        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);
        } else if (secondSpace == -1 && firstSpace == "DIRTY".length() && line.startsWith("DIRTY")) {
            entry.currentEditor = new DiskLruCache.Editor(entry);
        } else if (secondSpace != -1 || firstSpace != "READ".length() || !line.startsWith("READ")) {
            throw new IOException("unexpected journal line: " + line);
        }

    }
}

//提取完日志文件之后,需要对提取的信息做第二次优化,也就是删除一部分无效的数据
private void processJournal() throws IOException {
    deleteIfExists(this.journalFileTmp);
    Iterator i = this.lruEntries.values().iterator();

    while(true) {
        while(i.hasNext()) {
            DiskLruCache.Entry entry = (DiskLruCache.Entry)i.next();
            int t;
            if (entry.currentEditor == null) {
                for(t = 0; t < this.valueCount; ++t) {
                    this.size += entry.lengths[t];
                }
            } else {
                entry.currentEditor = null;

                for(t = 0; t < this.valueCount; ++t) {
                    deleteIfExists(entry.getCleanFile(t));
                    deleteIfExists(entry.getDirtyFile(t));
                }

                i.remove();
            }
        }

        return;
    }
}
  • 读取
//使用例子:
DiskLruCache.Snapshot snapshot2 = diskLruCache.get("wnp5");
if (snapshot != null) {
    String str = snapshot2.getString(0);
    snapshot.close();
    System.out.println(str);
} else {
    System.out.println("不存在此条缓存");
}

//源码:
public synchronized DiskLruCache.Snapshot get(String key) throws IOException {
    //检查是否为非法输入和环境状态
    this.checkNotClosed();
    this.validateKey(key);
    DiskLruCache.Entry entry = (DiskLruCache.Entry)this.lruEntries.get(key);//从内存中获取对应的key的数据
    if (entry == null) {
        return null;
    } else if (!entry.readable) {//当前数据标志位不可读
        return null;
    } else {
        InputStream[] ins = new InputStream[this.valueCount];//数组用来保存数据的输入流

        try {
            for(int i = 0; i < this.valueCount; ++i) {
                ins[i] = new FileInputStream(entry.getCleanFile(i));//创建保存文件的输入流
            }
        } catch (FileNotFoundException var6) {
            for(int i = 0; i < this.valueCount && ins[i] != null; ++i) {
                Util.closeQuietly(ins[i]);
            }

            return null;
        }

        ++this.redundantOpCount;
        this.journalWriter.append("READ " + key + '\n');//追加记录到日志文件中
        if (this.journalRebuildRequired()) {
            this.executorService.submit(this.cleanupCallable);//清除日志文件无用的数据
        }else{
            this.journalWriter.flush();//完成对日志文件的记录追加。
        }

        return new DiskLruCache.Snapshot(key, entry.sequenceNumber, ins, entry.lengths);//可以对保存的数据进行访问
    }
}
//使用例子:
DiskLruCache diskLruCache = DiskLruCache.open(new File("D:\\User\\project\\javaProject\\GennericParadigm\\src\\cache"),
        1, 2, 50 * 1024 * 1024);

DiskLruCache.Editor editor = diskLruCache.edit("wnp5");//开启对wnp5的编辑
editor.set(0, "微信");//对位置0,进行数据设置
editor.set(1, "微信");//对位置1,进行数据设置
editor.commit();//完成编辑


//源码解析:
private synchronized DiskLruCache.Editor edit(String key, long expectedSequenceNumber) throws IOException {
    // 检查是否为非法输入和环境状态
    this.checkNotClosed();
    this.validateKey(key);
    DiskLruCache.Entry entry = (DiskLruCache.Entry)this.lruEntries.get(key);//从内存中获取对应的数据
    if (expectedSequenceNumber == -1L || entry != null && entry.sequenceNumber == expectedSequenceNumber) {
        //进入此方法体内,可能为修改旧数据操作,可能为新插入的新数据两种操作
        if (entry == null) {//新数据
            entry = new DiskLruCache.Entry(key);
            this.lruEntries.put(key, entry);
        } else if (entry.currentEditor != null) {//如果当前数据已经在编辑了,那么不可以在进行编辑
            return null;
        }

        DiskLruCache.Editor editor = new DiskLruCache.Editor(entry);
        entry.currentEditor = editor;//标志着此数据正在编辑状态
        this.journalWriter.write("DIRTY " + key + '\n');//写入正在编辑状态的记录
        this.journalWriter.flush();
        return editor;//返回可编辑的数据实例
    } else {
        return null;
    }
}

public void set(int index, String value) throws IOException {
    //  创建对应要保存数据的文件输出流。文件名的规则为:this.key + "." + index + ".tmp"
    //  用于临时保存
    OutputStreamWriter writer = null;

    try {
        writer = new OutputStreamWriter(this.newOutputStream(index), Util.UTF_8);
        writer.write(value);//写入到临时文件中去
    } finally {
        Util.closeQuietly(writer);
    }

}

public void commit() throws IOException {
    if (this.hasErrors) {//如果在操作临时文件的过程中发生错误,那么当此次保存失效,需要进行清除
        DiskLruCache.this.completeEdit(this, false);
        DiskLruCache.this.remove(this.entry.key);
    } else {
        DiskLruCache.this.completeEdit(this, true);//b保存成功
    }

    this.committed = true;
}

private synchronized void completeEdit(DiskLruCache.Editor editor, boolean success) throws IOException {
    DiskLruCache.Entry entry = editor.entry;
    if (entry.currentEditor != editor) {//非法操作
        throw new IllegalStateException();
    } else {
        int i;
        if (success && !entry.readable) {//保存成功,但是文件不可读,那么需要将数据进行删除
            for(i = 0; i < this.valueCount; ++i) {
                if (!editor.written[i]) {
                    editor.abort();//将保存数据的临时文件进行删除
                    throw new IllegalStateException("Newly created entry didn't create value for index " + i);
                }

                if (!entry.getDirtyFile(i).exists()) {
                    editor.abort();//将保存数据的临时文件进行删除
                    return;
                }
            }
        }

        for(i = 0; i < this.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;
                    this.size = this.size - oldLength + newLength;//改变占用的物理内存大小记录
                }
            } else {//保存失败
                deleteIfExists(dirty);//删除临时文件
            }
        }

        ++this.redundantOpCount;
        entry.currentEditor = null;
        if (entry.readable | success) {
            entry.readable = true;
            this.journalWriter.write("CLEAN " + entry.key + entry.getLengths() + '\n');//添加记录,保存完成
            if (success) {
                entry.sequenceNumber = (long)(this.nextSequenceNumber++);
            }
        } else {
            this.lruEntries.remove(entry.key);
            this.journalWriter.write("REMOVE " + entry.key + '\n');//添加记录,删除成功
        }

        this.journalWriter.flush();
        if (this.size > this.maxSize || this.journalRebuildRequired()) {//进行Lru缓存策略管理,可能进行清除无用的文件
            this.executorService.submit(this.cleanupCallable);
        }

    }
}
  • 删除
public synchronized boolean remove(String key) throws IOException {
    this.checkNotClosed();//检查是否为正常的环境状态
    this.validateKey(key);//检查Key是否符合命名规则
    DiskLruCache.Entry entry = (DiskLruCache.Entry)this.lruEntries.get(key);//从内存中读取对应的key
    if (entry != null && entry.currentEditor == null) {//不为空,entry.currentEditor == null标识没有在编辑状态
        for(int i = 0; i < this.valueCount; ++i) {
            File file = entry.getCleanFile(i);//根据规则获取对应key保存的数据文件
            if (file.exists() && !file.delete()) {//进行数据文件的删除
                throw new IOException("failed to delete " + file);
            }

            this.size -= entry.lengths[i];//this.size标志着当前缓存数据的大小
            entry.lengths[i] = 0L;
        }

        ++this.redundantOpCount;
        this.journalWriter.append("REMOVE " + key + '\n');//往日志文件中追加REMOVE key记录
        this.lruEntries.remove(key);//移除key对应的内存记录,此处的LruEntries保存的数据类型为LinkedHashMap
        if (this.journalRebuildRequired()) {
            this.executorService.submit(this.cleanupCallable);//清除日志文件无用的数据
        }else{
            this.journalWriter.flush();//完成对日志文件的记录追加。
        }

        return true;//处理成功返回true
    } else {
        return false;//处理失败返回false
    }
}

答疑

进行什么操作的时候DiskLruCache会对已经保存的数据进行淘汰?需要满足什么条件?

DiskLruCache有个成员变量叫:cleanupCallable,这个变量的类型为Callable,用来在线程池中被执行。此变量执行的任务就是将已保存的数据,采用Lru策略进行删除(淘汰)。
源码分析:

Callable<Void> cleanupCallable = new Callable<Void>() {
    public Void call() throws Exception {
        synchronized(DiskLruCache.this) {//同步监视器
            if (DiskLruCache.this.journalWriter == null) {
                return null;
            } else {
                DiskLruCache.this.trimToSize();//执行Lru策略模式,进行无效无效数据删除
                if (DiskLruCache.this.journalRebuildRequired()) {//判断是否需要重构日志文件
                    DiskLruCache.this.rebuildJournal();//重构日志文件
                    DiskLruCache.this.redundantOpCount = 0;
                }
                return null;
            }
        }
    }
};

private void trimToSize() throws IOException {
    while(this.size > this.maxSize) {//这里的maxSize为初始化时,外部传入的参数。作用:保存数据最大值
        //获取符合Lru策略的数据,也就是要淘汰的数据
        java.util.Map.Entry<String, DiskLruCache.Entry> toEvict = (java.util.Map.Entry)this.lruEntries.entrySet().iterator().next();
        this.remove((String)toEvict.getKey());//此处调用remove方法,具体的逻辑可以参考删除操作
    }
}

在什么时候调用会引发我们的回收机制呢?

有如下几个情况:

  • 调用查询数据的时候。---get(keyName)
  • 插入数据或者修改数据的时候。----completeEdit(DiskLruCache.Editor editor, boolean success)
  • 删除数据的时候.----remove(keyName)

DiskLruCahe如何支持多线程编程

1.使用线程池技术

//线程池单线程模式,核心线程为0,为了减少资源占用,挺好的思维。
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
new Callable<Void>() {
    public Void call() throws Exception {
        synchronized(DiskLruCache.this) {//同步监视器
            if (DiskLruCache.this.journalWriter == null) {
                return null;
            } else {
                DiskLruCache.this.trimToSize();//执行Lru策略模式,进行无效无效数据删除
                if (DiskLruCache.this.journalRebuildRequired()) {//判断是否需要重构日志文件
                    DiskLruCache.this.rebuildJournal();//重构日志文件
                    DiskLruCache.this.redundantOpCount = 0;
                }

                return null;
            }
        }
    }
};

//上面两个对象配合使用,解决了下面Callable执行的顺序问题。
  1. 同步监视器机制


    内部方法.png

上面的代码方法都有加入synchronized关键字,用来支持多线程编程。

相关文章

  • DiskLruCache最全面刨析

    DiskLruCache概述 简单来说DiskLruCache是一款数据缓存框架。此处的Cache特指应用常需要保...

  • Redis全面刨析大纲

    学习了很多理论基础之后,我们一起花些时间去探索一下目前在企业使用很广泛Redis。看下这么火热的一款NoSQL是如...

  • 剑指BAT五月至六月课程计划

    Redis全面刨析大纲 企业级别项目实战之《迷你微信红包》 5.6号课程内容 面试总结、经验交流;Redis架构及...

  • 自我刨析

    沉下心来思考,接受的教育、所处的环境、社会文化的熏陶,正面的东西确实不是很多,特别是出了社会以后更加如此,但是更令...

  • 自我刨析

    总感觉每天都很忙,总感觉每天的时间都不够用。总在反问时间都去哪了?除了睡觉的那几个小时之外,几乎每个小时都是...

  • 自我刨析

    “文化属性”,我喜欢这个词的感觉。 我曾经多次预想过,退休后隐居山林。不过,这个计划都被山野之地的各种不...

  • 自我刨析

    近期待业,所以在思考自己的职业规划,职业规划第一步就是自我认知,只有知道自己是个什么样人,喜欢和适合做什么,才能定...

  • 自我刨析

    既然养成自卑胆小懦弱的性格,那就要改。 首先放开自我,没什么大不了。 其次锻炼意志,改掉懒惰。 神经质问题,把精力...

  • DiskLruCache缓存

    DiskLruCache DiskLruCache的创建 DiskLruCache缓存 DiskLruCache的...

  • 随笔

    对小闲的理解,帮助其实就是自己的刨析,

网友评论

      本文标题:DiskLruCache最全面刨析

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