DiskLruCache分析


DiskLruCache是Android提供的一个管理磁盘缓存的类。该类可用于在程序中把从网络加载的数据
保存到磁盘上作为缓存数据,例如一个显示网络图片的gridView,可对从网络加载的图片进行缓存,
提高程序的可用性。

一.文件

例如对于一组从网络加载的图片进行缓存,则在DiskLruCache的工作目录下面,可以看到如下所示
的文件:

前8行是对八张图片的缓存文件,第9行的文件叫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

前5行分别为一个常量字符串:libcore.io.DiskLruCache,该DiskLruCache的版本,应用程序的版本,
每个条目中保存值的个数,以及一个空行。

该文件中,随后记录的都是一个entry的状态。每行包括下面几项内容:一个状态,一个key,和可
选择的特定状态的值。
DIRTY:追踪那些活跃的条目,它们目前正在被创建或更新。每一个成功的DIRTY行后应该跟随一个
       CLEAN或REMOVE行。DIRTY行如果没有一个CLEAN或REMOVE行与它匹配,表明那是一个临时
       文件应该被删除。
CLEAN:跟踪一个发布成功的entry,并可以读取。一个发布行后跟着其文件的长度。
READ:跟踪对LRU的访问。
REMOVE:跟踪被删除的entry。

二.变量

下面是DiskLruCache中的一些静态常量,可从定义明白其意思:

static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TMP = "journal.tmp";
    static final String MAGIC = "libcore.io.DiskLruCache";
    static final String VERSION_1 = "1";
    static final long ANY_SEQUENCE_NUMBER = -1;
    private static final String CLEAN = "CLEAN";
    private static final String DIRTY = "DIRTY";
    private static final String REMOVE = "REMOVE";
    private static final String READ = "READ";
 
    private static final Charset UTF_8 = Charset.forName("UTF-8");
    private static final int IO_BUFFER_SIZE = 8 * 1024;



下面是DiskLruCache中定义的一些变量:


private final File directory;
    private final File journalFile;
    private final File journalFileTmp;
    private final int appVersion;
    private final long maxSize;
    private final int valueCount;
    private long size = 0;
    private Writer journalWriter;
    private final LinkedHashMap<String, Entry> lruEntries
            = new LinkedHashMap<String, Entry>(0, 0.75f, true);
    private int redundantOpCount;
private long nextSequenceNumber = 0;

directory:指向该DiskLruCache的工作目录。
journalFile:指向journal文件。
journalFileTmp:当构建一个journal文件时,先会生成一个journalTmp文件,当文件构建完成时,
      会将该journalTmp重命名为journal,这是一个临时文件。
maxSize:是该DiskLruCache所允许的最大缓存空间。
valueCount:每个entry对应的缓存文件的格式,一般情况下,该值为1.
size:DiskLruCache当前缓存的大小。
journalWriter:指向journal文件,主要向该文件中写内容。
lruEntries:当前entry的列表。
redundantOpCount:当前journal文件中entry状态记录的个数,主要用来当该值大于一定限制时,
      对journal文件进行清理。
nextSequenceNumber:新旧entry的特征值。

三.内部类

1.Entry:该类表示DiskLruCache中每一个条目

private final class Entry {
        private final String key;
 
        /** 该entry中每个文件的长度,该数组长度为valueCount */
        private final long[] lengths;
 
        /** 该entry曾经被发布过,该项为true */
        private boolean readable;
 
        /** 该entry所对应的editor */
        private Editor currentEditor;
 
        /** 最近编辑这个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();
        }
 
        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: " + Arrays.toString(strings));
        }
 
        public File getCleanFile(int i) {
            return new File(directory, key + "." + i);
        }
 
        public File getDirtyFile(int i) {
            return new File(directory, key + "." + i + ".tmp");
        }
    }

2.Editor:该类控制对每一个entry的读写操作。


public final class Editor {
        private final Entry entry;
        private boolean hasErrors;
 
        private Editor(Entry entry) {
            this.entry = entry;
        }
 
        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));
            }
        }
 
        public String getString(int index) throws IOException {
            InputStream in = newInputStream(index);
            return in != null ? inputStreamToString(in) : null;
        }
 
        /**
         * 返回该editor的key对应的文件的outputstream,文件名为key.index.tmp
         */
        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)));
            }
        }
 
        public void set(int index, String value) throws IOException {
            Writer writer = null;
            try {
                writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
                writer.write(value);
            } finally {
                closeQuietly(writer);
            }
        }
 
        public void commit() throws IOException {
            if (hasErrors) {
                completeEdit(this, false);
                remove(entry.key); // the previous entry is stale
            } else {
                completeEdit(this, true);
            }
        }
 
        public void abort() throws IOException {
            completeEdit(this, false);
        }
 
        private class FaultHidingOutputStream extends FilterOutputStream {
            private FaultHidingOutputStream(OutputStream out) {
                super(out);
            }
 
            @Override public void write(int oneByte) {
                try {
                    out.write(oneByte);
                } catch (IOException e) {
                    hasErrors = true;
                }
            }
 
            @Override public void write(byte[] buffer, int offset, int length) {
                try {
                    out.write(buffer, offset, length);
                } catch (IOException e) {
                    hasErrors = true;
                }
            }
 
            @Override public void close() {
                try {
                    out.close();
                } catch (IOException e) {
                    hasErrors = true;
                }
            }
 
            @Override public void flush() {
                try {
                    out.flush();
                } catch (IOException e) {
                    hasErrors = true;
                }
            }
        }
    }

3.Snapshot:该类表示DiskLruCache中每一个entry中缓存文件的快照,它持有该entry中每个文件的
        inputStream,通过该inputStream可读取该文件的内容。


public final class Snapshot implements Closeable {
        private final String key;
        private final long sequenceNumber;
        private final InputStream[] ins;
 
        private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
            this.key = key;
            this.sequenceNumber = sequenceNumber;
            this.ins = ins;
        }
 
        public Editor edit() throws IOException {
            return DiskLruCache.this.edit(key, sequenceNumber);
        }
 
        public InputStream getInputStream(int index) {
            return ins[index];
        }
 
        public String getString(int index) throws IOException {
            return inputStreamToString(getInputStream(index));
        }
 
        @Override public void close() {
            for (InputStream in : ins) {
                closeQuietly(in);
            }
        }
    }

四.工作流程

1.初始化DiskLruCache
初始化DiskLruCache时,会调用该类中的一个静态函数:open


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;
    }

该方法用于初始化一个DiskLruCache对象,在初始化过程中,如果之前已经创建过该缓存,则通过存在
的journal文件构建已有的entry列表(其中涉及的方法可在DisklruCache中进行查看),否则则创建一个
新的journal文件,调用rebuildJournal方法。

2.查找是否存在一个key所对应的缓存文件
当需要从缓存中查找一个缓存文件时,可进行如下的调用:


final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
                    if (snapshot != null) {
                        inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
                        if (inputStream != null) {
                            FileDescriptor fd = ((FileInputStream) inputStream).getFD();
                        }
                    }

该功能调用了DiskLruCache测get方法,来查找DiskLruCache中是否存在该key对应的Snapshot,若返回的
snapshot不为空,则可通过snapshot的inputstream来读取缓存文件的内容。若snapshot为空,表明该key
对应的文件不在缓存中。get方法如下:

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;
        }
 
        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!
            return null;
        }
       </i> Log.i("chentest","redundantOpCount="+redundantOpCount);
        redundantOpCount++;
        journalWriter.append(READ + ' ' + key + '\n');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
 
        return new Snapshot(key, entry.sequenceNumber, ins);
    }


该函数首先会检查该DiskLruCache是否关闭,以及key的合法性。然后从lruEntries中查找key所对应的
Entry,若entry不为空,则通过该entry获取缓存文件的路径,并封装到Snapshot中的inputstream中返
回该Snapshot。之后会对journal文件进行READ该key的操作,并检查是否要重新构建该journal。

3.向DiskLruCache中写入一条entry

DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
                    if (snapshot == null) {
                        final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                        if (editor != null) {
                            out = editor.newOutputStream(DISK_CACHE_INDEX);
                            value.getBitmap().compress(
                                    mCacheParams.compressFormat, mCacheParams.compressQuality, out);
                            editor.commit();
                            out.close();
                        }
                    } else {
                        snapshot.getInputStream(DISK_CACHE_INDEX).close();
                    }

该功能首先会调用edit方法,返回一个Editor:


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)) {
            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;
 
        // flush the journal before creating files to prevent file leaks
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
    }


该函数会针对该key创建一个entry,并将该entry加入lruEntries中,并创建一个相应的Editor,同
时在journal文件中加入一条对该key的DIRTY记录。

其次,上面的功能通过Editor返回一个该key所对应的文件的输出流,并将文件保存到输出流中。
最后,会调用edit的commit方法,而commit方法调用了DiskLruCache中的completeEdit方法:


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.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);
                    long oldLength = entry.lengths;
                    long newLength = clean.length();
                    entry.lengths = 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');
        }
 
        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    }

该函数主要会根据保存缓存文件的大小更新DiskLruCache的总大小,并且在journal文件对该key
加入CLEAN的log,最后会判断DiskLruCache的总大小是否超过maxSize,以便对缓存进行清理。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值