Glide源码分析(一)——DiskLruCache磁盘缓存的实现

Glide磁盘的实现主要是通过DiskLruCache来实现的。DiskLruCache并非针对Glide编写的,而是一个通用的磁盘缓存实现,虽然并非Google官方的代码,但是已经在很多应用中得到了引入使用。

journal日志

DiskLruCache通过日志来辅助保证磁盘缓存的有效性。在应用程序运行阶段,可以通过内存数据来保证缓存的有效性,但是一旦应用程序退出或者被意外杀死,下次再启动的时候就需要通过journal日志来重新构建磁盘缓存数据记录,保证上次的磁盘缓存是有效和可用的。

journal日志的基本数据

为了理解journal日志是如何起作用的,首先需要理解journal日志的结构。以下是journal日志的基本数据:

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;第二行是DiskLruCache的版本,目前固定为1;第三行表示所属应用的版本号;第四行valueCount表示一个缓存key可能对应多少个缓存文件,它决定了后面一个CLEAN状态记录最多可以size大小数据;第五行是空行。此后记录的就是DiskLruCache针对磁盘缓存的操作记录了。其中几个状态表示如下:

  • CLEAN 表示缓存处于一个稳定状态,即当前没有对该缓存数据进行写操作,在该状态下,对缓存文件的读写都是安全的。
  • DIRTY 表示当前该key对应的缓存文件正在被修改,该状态下对缓存文件的读写都是不安全的,需要阻塞到对文件的修改完成,使该key对应的状态转变成CLEAN为止。
  • REMOVE 表示该key对应的缓存文件被删除了,在缓存整理的过程中可能会出现多条这样的记录。
  • READ 表示一个对key对应的缓存文件进行读取的操作记录。

每个操作记录状态后面都有一个字符串,表示缓存的key,其中CLEAN状态在后面还会有一个或者多个数字,这些数字表示对应缓存文件的大小。之所以允许一个key对应多个文件,主要是考虑到满足类似于一张图片可能存在多个大小和分辨率不同的缓存的功能。

Entry实现

一个Entry对应一条缓存,DiskLruCache通过Entry对相应的缓存进行操作。其主要的成员为:

  private final class Entry {
        private final String key;

        /**
         * Lengths of this entry's files.
         * 之所以可能会存在多个length,是因为一个Key也可能存在多个大小不同的缓存文件,例如尺寸或者分辨率不同的图片
         */
        private final long[] lengths;

        /** True if this entry has ever been published */
        private boolean readable;

        /** The ongoing edit or null if this entry is not being edited. */
        private Editor currentEditor;       // 正在操作entry的editor

        /** The sequence number of the most recently committed edit to this entry. */
        private long sequenceNumber;

        ...
    }

其中key对应缓存的key,lengths表示key对应的若干个缓存文件的大小,readable表示该缓存是否能够被读取,只有在缓存状态为CLEAN的时候才为true;currentEditor是Entry用来修改缓存文件的类;sequenceNumber是一个唯一的序号,每当对应的缓存文件被改变后,该序号就改变,在通过snapshot获取缓存映像的时候,snapshot内部的sequenceNumber和当时刻的Entry的sequenceNumber相同,如果在此后Entry对应的缓存文件被改变,那么通过snapshot获取缓存的时候就能发现二者的sequenceNumber不相同,因此snapshot就失效了,这样避免通过snapshot获取到旧的缓存数据信息。

Editor实现

Editor是用来对缓存文件进行修改的操作的封装,他和Entry一一对应,一个entry如果其中的currentEditor不为空,表示这个Entry对应的缓存文件正在被修改,即该Entry处于DIRTY状态。总体来说,Editor的功能单一实现简单。唯一需要说明一下的是在使用它对文件进行操作后,需要调用它的commit方法,以使它对缓存文件的更改记录更新到内存lruEntries和日志当中,否则对应的缓存将会始终处于DIRTY状态而无法被再次修改和读取。

Snapshot实现

Snapshot就是一个缓存记录的映像,它代表在某一时刻缓存的状态,它和某一时刻的Entry一一对应,前面也提到过,一旦Snapshot对应的Entry被修改了,那么虽然该Snapshot虽然还和这个Entry有对应关系,但是因为缓存文件的内容已经发生了改变,所以该Snapshot处于失效状态,不能被使用。
Snapshot是封装给外部使用的,它封装了接口专门用来对缓存文件进行读写,方便外部通过它直接访问缓存文件而不管具体的缓存处理细节。

lruEntries内存记录和日志

lruEntries是一个LinkedHashMap,DiskLruCache用它来实现LRU的功能。它是缓存在内存中的记录,能够反映当前磁盘中的缓存文件的状态,通过读取这个数据结构,能够快速对磁盘中是否有对应key的缓存文件做出判断。
每当DiskLruCache在初始化的时候,就会根据journal日志来初始化其中的lruEntries,其相关代码如下:

/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory a writable directory
* @param appVersion
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
* @throws java.io.IOException if reading or writing the cache directory fails
*/
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");
    }

    // prefer to pick up where we left off
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
        try {
            cache.readJournal();
            cache.processJournal();
            cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
                    IO_BUFFER_SIZE);
            return cache;
        } catch (IOException journalIsCorrupt) {
    //                System.logW("DiskLruCache " + directory + " is corrupt: "
    //                        + journalIsCorrupt.getMessage() + ", removing");
            cache.delete();
        }
    }

    // create a new empty cache
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
}

/**
* 读取日志文件中的内容,根据日志文件中的记录来初始化对应entry的状态并构建lruEntries
* @throws IOException
*/
private void readJournal() throws IOException {
    InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
    try {
        String magic = readAsciiLine(in);
        String version = readAsciiLine(in);
        String appVersionString = readAsciiLine(in);
        String valueCountString = readAsciiLine(in);
        String blank = readAsciiLine(in);
        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 + "]");
        }

        while (true) {
            try {
                readJournalLine(readAsciiLine(in));
            } catch (EOFException endOfJournal) {
                break;
            }
        }
    } finally {
        closeQuietly(in);
    }
}

/**
* Computes the initial size and collects garbage as a part of opening the
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
*/
private void processJournal() throws IOException {
    deleteIfExists(journalFileTmp);
    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
        Entry entry = i.next();
        if (entry.currentEditor == null) {      // currenEditor为null,表示当前为clean状态
            for (int t = 0; t < valueCount; t++) {
                size += entry.lengths[t];       // 累计大小
            }
        } else {        
            // 在初始化阶段才调用该函数,如果此时日志文件中记录该项为dirty,那么就直接放弃这条缓存,删除其相关的文件以及在lruEntries中的记录
            entry.currentEditor = null;
            for (int t = 0; t < valueCount; t++) {
                deleteIfExists(entry.getCleanFile(t));
                deleteIfExists(entry.getDirtyFile(t));
            }
            i.remove();
        }
    }
}
/**
 * 读取每行日志,初始化日志对应的entry状态
 * @param line
 * @throws IOException
 */
private void readJournalLine(String line) throws IOException {
    String[] parts = line.split(" ");
    if (parts.length < 2) {
        throw new IOException("unexpected journal line: " + line);
    }

    String key = parts[1];
    if (parts[0].equals(REMOVE) && parts.length == 2) {
        lruEntries.remove(key);
        return;
    }

    Entry entry = lruEntries.get(key);
    if (entry == null) {
        entry = new Entry(key);
        lruEntries.put(key, entry);
    }

    if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
        entry.readable = true;
        entry.currentEditor = null;
        entry.setLengths(copyOfRange(parts, 2, parts.length));
    } else if (parts[0].equals(DIRTY) && parts.length == 2) {
        entry.currentEditor = new Editor(entry);
    } else if (parts[0].equals(READ) && parts.length == 2) {
        // this work was already done by calling lruEntries.get()
    } else {
        throw new IOException("unexpected journal line: " + line);
    }
}

在open函数中通过readJournal和processJournal两个函数来完成从journal日志到lruEntries的构建。其中readJournal和readJournalLine分别通过解析日志的每一行内容,通过简单的容错逻辑来初步构建lruEntries的内容,包括缓存key和其状态和缓存文件大小等。其中解析是从第一行到最后一行,因此一个key可能在lruEntries中被操作多次,特别是对缓存状态的改变。在readJournal完成后,lruEntries基本上就是上次引用结束后的DiskLruCache的缓存状态,之后再调用processJournal对异常状态进行处理后就得到了一个有效且能反映当前磁盘缓存状态的lruEntries记录了。
此后,在对缓存进行修改的时候会同步修改journal日志和lruEntries,以使二者保证状态一致,但对于读操作,仅仅只需要在日志中记录一条记录即可。
在缓存大小达到上线的时候,DiskLruCache会将长时间不使用的缓存清理掉,同时如果日志的条数也达到了上线,就会利用lruEntries来重新构建日志,这正好是和初始化的时候的一个逆过程,相关代码如下:

private final Callable<Void> cleanupCallable = new Callable<Void>() {       // 日志清理任务
    @Override public Void call() throws Exception {
        synchronized (DiskLruCache.this) {
            if (journalWriter == null) {
                return null; // closed
            }
            trimToSize();
            if (journalRebuildRequired()) {
                rebuildJournal();
                redundantOpCount = 0;
            }
        }
        return null;
    }
};

/**
 * 使缓存大小保证在最大限制之内。
 * 由于lruEntries是LinkedHashMap,能保证是实现lru特性的删除操作
 * @throws IOException
 */
private void trimToSize() throws IOException {
    while (size > maxSize) {
    //            Map.Entry<String, Entry> toEvict = lruEntries.eldest();
        final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
        remove(toEvict.getKey());
    }
}

/**
 * Creates a new journal that omits redundant information. This replaces the
 * current journal if it exists.
 * 根据内部数据结构lruEntries来构造新的日志文件,然后替换原来的日志文件。
 * 在初始化阶段,一般构建的日志文件中没有dirty记录,如果是日志清理线程调用的,那么存在dirty的记录
 * 注意是同步方法
 */
private synchronized void rebuildJournal() throws IOException {
    if (journalWriter != null) {
        journalWriter.close();
    }

    Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);     // 先写到tmp文件中,注意该方法是同步方法
    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');
        }
    }

    writer.close();
    journalFileTmp.renameTo(journalFile);       // 重命名成journal文件
    journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
}
DiskLruCache缓存读写流程

DiskLruCache的读写操作实际上分别是通过Snapshot和Editor来完成的,二者封装了文件IO的操作,对外暴露简单的接口,使用很方便。但是在多线程的环境下,为了保证缓存文件、日志文件和lruEntries在同一时刻只能被同一线程修改,在很多细节的地方都做了同步处理。例如在获取缓存映像,构造输入输出流等操作上要么使用了同步方法,要么使用同步块。
DiskLruCache的写操作主要由Editor完成,其中构建Editor的方法为:

/**
 * 注意需要写缓存和日志文件,因此是同步方法
 * @param key
 * @param expectedSequenceNumber
 * @return
 * @throws IOException
 */
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
            && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
        // entry为空,表示该缓存已经被删除,
        // sequenceNumber和snapshot不匹配,说明entry对应的内容已经改变,不能通过该snapshot来获取缓存内容
        return null; // snapshot is stale
    }
    if (entry == null) {        // expectedSequenceNumber == ANY_SEQUENCE_NUMBER
        entry = new Entry(key);
        lruEntries.put(key, entry);
    } else if (entry.currentEditor != null) {   // 已经有editor正在进行修改
        return null; // another edit is in progress
    }

    Editor editor = new Editor(entry);
    entry.currentEditor = editor;

    // flush the journal before creating files to prevent file leaks
    journalWriter.write(DIRTY + ' ' + key + '\n');  // 表示正在进行修改
    journalWriter.flush();
    return editor;
}

由于在获取Editor的时候需要写入一个READ状态到journal中,因此该方法是同步方法。Editor中对文件的读写方法如下:

  /**
    * Returns an unbuffered input stream to read the last committed value,
    * or null if no value has been committed.
    */
   public InputStream newInputStream(int index) throws IOException {
       synchronized (DiskLruCache.this) {
           if (entry.currentEditor != this) {
               throw new IllegalStateException();
           }
           if (!entry.readable) {
               return null;
           }
           return new FileInputStream(entry.getCleanFile(index));
       }
   }

   /**
    * Returns the last committed value as a string, or null if no value
    * has been committed.
    */
   public String getString(int index) throws IOException {
       InputStream in = newInputStream(index);
       return in != null ? inputStreamToString(in) : null;
   }

   /**
    * Returns a new unbuffered output stream to write the value at
    * {@code index}. If the underlying output stream encounters errors
    * when writing to the filesystem, this edit will be aborted when
    * {@link #commit} is called. The returned output stream does not throw
    * IOExceptions.
    */
   public OutputStream newOutputStream(int index) throws IOException {
       synchronized (DiskLruCache.this) {
           if (entry.currentEditor != this) {
               throw new IllegalStateException();
           }
           return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); // 注意是写到dirty文件中的
       }
   }

可以看到在构建输入输出流的时候都做了同步处理,同时注意,通过Editor操作的都是临时文件,在通过IO对文件进行了操作后,需要在调用commit方法来确认操作,该操作会调用DiskLruCache的completeEdit方法,代码如下:

/**
 * 在editor修改了缓存后调用,用来同步缓存内容和更新日志及相关状态
 * 要写缓存和日志,同步方法
 * @param editor
 * @param success
 * @throws IOException
 */
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 (!entry.getDirtyFile(i).exists()) {      // 因为editor在edit的时候是将缓存内容写到dirty文件中的,因此这里要求dirty文件一定要存在
                editor.abort();
                throw new IllegalStateException("edit didn't create file " + i);
            }
        }
    }

    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);      // 写入成功,将dirty文件编程clean 文件
                long oldLength = entry.lengths[i];
                long newLength = clean.length();
                entry.lengths[i] = newLength;
                size = size - oldLength + newLength;    // 更新缓存大小
            }
        } else {
            deleteIfExists(dirty);      // dirty文件的使命完成
        }
    }

    redundantOpCount++;     // 增加日志一条
    entry.currentEditor = null;     // entry clean了
    if (entry.readable | success) {
        entry.readable = true;
        journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
        if (success) {
            entry.sequenceNumber = nextSequenceNumber++;    // 更新sequenceNumber,这样之前的旧的snapshot就不会读错数据
        }
    } else {
        lruEntries.remove(entry.key);
        journalWriter.write(REMOVE + ' ' + entry.key + '\n');
    }

    if (size > maxSize || journalRebuildRequired()) {
        executorService.submit(cleanupCallable);
    }
}

同样,该方法也是一个同步方法,在通过容错判断后,将临时文件重命名为缓存文件,然后将日志写入到journal中,并且更新entry的sequenceNumber,表示该缓存已经被改变。
snapshot由于只涉及到文件的读取,并不会修改日志和内存信息,因此完全不需要进行同步,逻辑也很简单,不再进行分析。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值