美文网首页
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最全面刨析

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