DiskLruCache源码分析

DiskLruCache源码地址:

https://developer.android.com/samples/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.html?hl=zh-cn#l100

open方法

open方法是获DiskLruCache实例的构造方法,open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。

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);
        //如果备份文件存在,而正经的文件 不存在的话 就把备份文件 重命名为正经的journal文件
        //如果正经的journal文件存在 那就把备份文件删除掉。
        if (backupFile.exists()) {
            File journalFile = new File(directory, JOURNAL_FILE);
            if (journalFile.exists()) {
                backupFile.delete();
            } else {
                renameTo(backupFile, journalFile, false);
            }
        }

        //这个构造函数 无非就是 把值赋给相应的对象罢了
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        //如果这个日志文件存在的话 就开始读里面的信息并返回
        //主要就是构建entry列表
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                return cache;
            } catch (IOException journalIsCorrupt) {
                System.out
                        .println("DiskLruCache "
                                + directory
                                + " is corrupt: "
                                + journalIsCorrupt.getMessage()
                                + ", removing");
                cache.delete();
            }
        }

        //如果日志文件不存在 就新建
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }
rebuildJournal方法

当缓存日志不存在或者日志内容有异常的时候,会调用rebuildJournal方法来重构缓存文件

//这个就是我们可以直接在disk里面看到的journal文件 主要就是对他的操作
    private final File journalFile;
    //journal文件的temp 缓存文件,一般都是先构建这个缓存文件,等待构建完成以后将这个缓存文件重新命名为journal
    private final File journalFileTmp;

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

        //这个地方要注意了 writer 是指向的journalFileTmp 这个日志文件的缓存文件
        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) {
                    writer.write(DIRTY + ' ' + entry.key + '\n');
                } else {
                    writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
                }
            }
        } finally {
            writer.close();
        }

        if (journalFile.exists()) {
            renameTo(journalFile, journalFileBackup, true);
        }
        //所以这个地方 构建日志文件的流程主要就是先构建出日志文件的缓存文件,如果缓存构建成功 那就直接重命名这个缓存文件
        //可以想想这么做有什么好处
        renameTo(journalFileTmp, journalFile, false);
        journalFileBackup.delete();

        //这里也是把写入日志文件的writer初始化
        journalWriter = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
    }

readJournal方法主要是在获取cache实例的时候如果存在有效日志则根据日志重构缓存lruEntries,由此可以看出disklrucache也是依赖于LinkedHashMap来实现的

  private final LinkedHashMap<String, Entry> lruEntries =
            new LinkedHashMap<String, Entry>(0, 0.75f, true);
private void readJournal() throws IOException {
        //StrictLineReader 这个类挺好用的,大家可以拷出来,这个类的源码大家可以自己分析 不难 以后还可以自己用
        StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
        try {
            //这一段就是读文件头的信息
            String magic = reader.readLine();
            String version = reader.readLine();
            String appVersionString = reader.readLine();
            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("unexpected journal header: [" + magic + ", " + version + ", "
                        + valueCountString + ", " + blank + "]");
            }

            //从这边开始 就要开始读下面的日志信息了 前面的都是日志头
            int lineCount = 0;
            //利用读到文件末尾的异常来跳出循环
            while (true) {
                try {
                    //就是在这里构建的lruEntries entry列表的
                    readJournalLine(reader.readLine());
                    lineCount++;
                } catch (EOFException endOfJournal) {
                    break;
                }
            }
            redundantOpCount = lineCount - lruEntries.size();

            // If we ended on a truncated line, rebuild the journal before appending to it.
            if (reader.hasUnterminatedLine()) {
                rebuildJournal();
            } else {
                //在这里把写入日志文件的Writer 初始化
                journalWriter = new BufferedWriter(new OutputStreamWriter(
                        new FileOutputStream(journalFile, true), Util.US_ASCII));
            }
        } finally {
            Util.closeQuietly(reader);
        }
    }

输入输出的对象不一样,save方法存入的是一个entity对象,get返回的事一个snapshot对象

private final class Entry {
        private final String key;

        /**
         * Lengths of this entry's files.
         * 这个entry中 每个文件的长度,这个数组的长度为valueCount 一般都是1
         */
        private final long[] lengths;

        /**
         * True if this entry has ever been published.
         * 曾经被发布过 那他的值就是true
         */
        private boolean readable;

        /**
         * The ongoing edit or null if this entry is not being edited.
         * 这个entry对应的editor
         */
        private Editor currentEditor;

        @Override
        public String toString() {
            return "Entry{" +
                    "key='" + key + '\'' +
                    ", lengths=" + Arrays.toString(lengths) +
                    ", readable=" + readable +
                    ", currentEditor=" + currentEditor +
                    ", sequenceNumber=" + sequenceNumber +
                    '}';
        }

        /**
         * The sequence number of the most recently committed edit to this entry.
         * 最近编辑他的序列号
         */
        private long sequenceNumber;

        private Entry(String key) {
            this.key = key;
            this.lengths = new long[valueCount];
        }

        public String getLengths() throws IOException {
            StringBuilder result = new StringBuilder();
            for (long size : lengths) {
                result.append(' ').append(size);
            }
            return result.toString();
        }

        /**
         * Set lengths using decimal numbers like "10123".
         */
        private void setLengths(String[] strings) throws IOException {
            if (strings.length != valueCount) {
                throw invalidLengths(strings);
            }

            try {
                for (int i = 0; i < strings.length; i++) {
                    lengths[i] = Long.parseLong(strings[i]);
                }
            } catch (NumberFormatException e) {
                throw invalidLengths(strings);
            }
        }

        private IOException invalidLengths(String[] strings) throws IOException {
            throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
        }

        //臨時文件創建成功了以後 就會重命名為正式文件了
        public File getCleanFile(int i) {
            Log.v("getCleanFile","getCleanFile path=="+new File(directory, key + "." + i).getAbsolutePath());
            return new File(directory, key + "." + i);
        }

        //tmp开头的都是临时文件
        public File getDirtyFile(int i) {
            Log.v("getDirtyFile","getDirtyFile path=="+new File(directory, key + "." + i + ".tmp").getAbsolutePath());
            return new File(directory, key + "." + i + ".tmp");
        }


    }
/**
     * A snapshot of the values for an entry.
     * 这个类持有该entry中每个文件的inputStream 通过这个inputStream 可以读取他的内容
     */
    public final class Snapshot implements Closeable {
        private final String key;
        private final long sequenceNumber;
        private final InputStream[] ins;
        private final long[] lengths;

        private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
            this.key = key;
            this.sequenceNumber = sequenceNumber;
            this.ins = ins;
            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);
        }

        /**
         * Returns the unbuffered stream with the value for {@code index}.
         */
        public InputStream getInputStream(int index) {
            return ins[index];
        }

        /**
         * Returns the string value for {@code index}.
         */
        public String getString(int index) throws IOException {
            return inputStreamToString(getInputStream(index));
        }

        /**
         * Returns the byte length of the value for {@code index}.
         */
        public long getLength(int index) {
            return lengths[index];
        }

        public void close() {
            for (InputStream in : ins) {
                Util.closeQuietly(in);
            }
        }
    }

save方法跟lrucache不一样,是先通过editor获取输出流然后写入。

  public Editor edit(String key) throws IOException {
 2         return edit(key, ANY_SEQUENCE_NUMBER);
 3     }
 4 
 5     //根据传进去的key 创建一个entry 并且将这个key加入到entry的那个map里 然后创建一个对应的editor
 6     //同时在日志文件里加入一条对该key的dirty记录
 7     private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
 8         //因为这里涉及到写文件 所以要先校验一下写日志文件的writer 是否被正确的初始化
 9         checkNotClosed();
10         //这个地方是校验 我们的key的,通常来说 假设我们要用这个缓存来存一张图片的话,我们的key 通常是用这个图片的
11         //网络地址 进行md5加密,而对这个key的格式在这里是有要求的 所以这一步就是验证key是否符合规范
12         validateKey(key);
13         Entry entry = lruEntries.get(key);
14         if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
15                 || entry.sequenceNumber != expectedSequenceNumber)) {
16             return null; // Snapshot is stale.
17         }
18         if (entry == null) {
19             entry = new Entry(key);
20             lruEntries.put(key, entry);
21         } else if (entry.currentEditor != null) {
22             return null; // Another edit is in progress.
23         }
24 
25         Editor editor = new Editor(entry);
26         entry.currentEditor = editor;
27 
28         // Flush the journal before creating files to prevent file leaks.
29         journalWriter.write(DIRTY + ' ' + key + '\n');
30         journalWriter.flush();
31         return editor;
32     }

get方法就比较熟悉了,get最终返回的其实就是entry根据key 来取的snapshot对象,这个对象直接把inputStream暴露给外面。

/**
//通过key 来取 该key对应的snapshot
    public synchronized Snapshot get(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            return null;
        }

        if (!entry.readable) {
            return null;
        }

        // Open all streams eagerly to guarantee that we see a single published
        // snapshot. If we opened streams lazily then the streams could come
        // from different edits.
        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++;
        //在取得需要的文件以后 记得在日志文件里增加一条记录 并检查是否需要重新构建日志文件
        journalWriter.append(READ + ' ' + key + '\n');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }

        return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
    }

save之后要commit

public void commit() throws IOException {
            if (hasErrors) {
                completeEdit(this, false);
                remove(entry.key); // The previous entry is stale.
            } else {
                completeEdit(this, true);
            }
            committed = true;
        }
/这个就是根据缓存文件的大小 更新disklrucache的总大小 然后再日志文件里对该key加入clean的log
    //最后判断是否超过最大的maxSize 以便对缓存进行清理
    private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
        Entry entry = editor.entry;
        if (entry.currentEditor != editor) {
            throw new IllegalStateException();
        }

        // If this edit is creating the entry for the first time, every index must have a value.
        if (success && !entry.readable) {
            for (int i = 0; i < 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 (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;
            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
            if (success) {
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            lruEntries.remove(entry.key);
            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }
        journalWriter.flush();

        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • 在开发中我们会经常碰到一些资源需要做缓存优化,例如Bitmap,Json等,那么今天我们来学习本地磁盘来做缓存的实...
    TruthKeeper阅读 1,956评论 1 8
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 10.MemoryCache MemoryCache是实现内存缓存的类,不管是内存缓存还是磁盘缓存,对于Image...
    反复横跳的龙套阅读 474评论 1 0
  • 终于知道月子里为什么很多人抑郁了。 身体很虚弱,还每天有淌不完的汗水,动一下就浑身湿透,喝口水也会满身大汗,睡在空...
    子川vs玄冰阅读 185评论 0 0
  • 提问:我一直有一个很大的困惑,不知道怎样去深入理解一个垂直行业。严格来说,我不知道怎样挑选一个垂直行业。如果没有很...
    纯银V阅读 9,584评论 2 40