Android DiskLruCache源码分析

简介

缓存对于提升一个App的性能至关重要,对于需要使用大量图片的App来说,Android官方推荐使用LruCache+DiskLruCache对图片进行两级缓存,这样当用户重新打开App时,不需要再从网络上获取图片。DiskLruCache是一套磁盘缓存方案,采用LRU算法。

基本概念

DiskLruCache以key-value的形式在磁盘上缓存,支持一对多,线程安全。不支持多进程同时访问一个缓存目录。一般App在SD卡的缓存目录在/sdcard/Android/data/<your packagename>/cache/。DiskLruCache在缓存目录下会生成一个日志文件journal,记录了所有对缓存的操作,数据文件名称以key.index方式存储在缓存目录,index指key-value对应关系中一对多的情况value的下标,如果key-value是一对一的情况,那么一般数据文件名称以key.0的方式存储。如下图所示

journal文件解读

journal文件包含journal文件头+日志,如下图所示

文件前五行组成一个完整的journal头

  1. 第一行是一个字符串常量libcore.io.DiskLruCache,标识版权信息
  2. 第二行代表DiskLruCache缓存的版本号1
  3. 第三行代表App的版本号也就是versionCode为1
  4. 第三行代表key-value是1对N的关系,此例中N=1
  5. 第五行是空行,与日志内容分隔

日志内容一行代表一条操作记录,格式为status key value[0].length ... value[N-1].length,用空格分隔,status代表文件状态,如下

  • DIRTY 脏数据,代表正在对此条数据进行操作,每条DIRTY日志后面都应该是一条CLEAN或者REMOVE日志,否则将删除此key对应的文件
  • CLEAN 代表已经成功写入缓存的数据,可供读取
  • READ 代表读取一条数据
  • REMOVE 代表删除一条数据

journal文件是整个DiskLruCache的核心,在使用过程中要对journal文件进行大量的操作

创建缓存

DiskLruCache的构造方法是private的,创建一个DiskLruCache实例需要调用静态方法open

   /**
     * @param directory 缓存写入的目录,一般写在Sd卡的缓存目录,传入getExternalCacheDir(),在app卸载时会清空
     * @param appVersion app的版本,对应versionCode
     * @param valueCount 每条缓存对应的值的数量
     * @param maxSize 总缓存的大小,单位byte
     */
    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) {
                cache.delete();
            }
        }

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

open方法创建了DiskLurCache实例,如果不存在journal文件,则创建并写入journal头,调用rebuildJournal()方法

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

        Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
        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);
        journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
}

如果已经存在journal文件,则调用readJournal()processJournal()对已经存在的缓存数据进行初始化,核心部分如下,

  private void readJournal() throws IOException {
        InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
        try {
		//验证journal文件头
		...

            while (true) {
                try {
				// 按行读取每一条记录
                    readJournalLine(readAsciiLine(in));
                } catch (EOFException endOfJournal) {
                    break;
                }
            }
        } finally {
            closeQuietly(in);
        }
}

readJournalLine()将每一行记录解析后添加到lruEntries,lruEntries是一个全局的LinkedHashMap<String, Entry>,用来保存所有的缓存对象Entry,排序方式以访问频次为权重,从少到多(from least-recently accessed to most-recently accessed)。Entry类包含了缓存的一些属性,如下:

 private final class Entry {
        private final String key;

        /** Lengths of this entry's files. */
        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;

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

readJournalLine()

private void readJournalLine(String line) throws IOException {
        String[] parts = line.split(" ");
        //line like 'status key value[0].length ... value[N-1].length'
        //parts[0]=status,parts[1]=key
        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;
            //数组下标从2到最后代表对应value的长度
            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);
        }
}

processJournal()方法对lruEntries中的数据进行加工,删除脏数据,计算可用缓存数据的总长度,代码如下

private void processJournal() throws IOException {
        deleteIfExists(journalFileTmp);//删除journal临时文件'journal.tmp'
        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
            Entry entry = i.next();
            //没有Editor的数据表示可用的数据
            if (entry.currentEditor == null) {
                for (int t = 0; t < valueCount; t++) {
                    size += entry.lengths[t];
                }
            } else {
                //删除脏数据
                entry.currentEditor = null;
                for (int t = 0; t < valueCount; t++) {
                    deleteIfExists(entry.getCleanFile(t)); //删除 'key.index'
                    deleteIfExists(entry.getDirtyFile(t)); //删除 'key.index.tmp'
                }
                i.remove();
            }
        }
}

写入缓存

缓存的写入需要通过Editor类,调用edit(String key)获取此key对应的Editor对象。

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        checkNotClosed(); //检测journalWriter是否为空
        validateKey(key); //key不能包含空格,'\r','\n'
        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
        //记录一个dirty日志
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
}

Editor类结构如下:

  • newOutputStream()方法获取一个输出流,将需要缓存的数据写入此OutputStream
  • newInputStream(int index)方法获取对应index的输入流,对应最后一次写入的数据
  • getString(index)是将newInputStream(int index)方法的返回值转化为String返回
  • set(int index, String value)将value的值写入index对应的缓存中
  • commit()提交此次操作
  • abort()放弃此次操作

每次操作完成后,必须要调用commit()或者abort(),commit()abort()方法内部都调用了DiskLruCache的completeEdit(Editor editor, boolean success)来通知DiskLruCache完成了一次edit操作,completeEdit(Editor editor, boolean success)方法内部对当前缓存所占用的内存进行检查,来决定是否需要释放一部分缓存,另外对journal日志文件的行数进行检查,来决定是否需要重写journal,代码如下

    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[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) {
                //scquenceNumber用来判断缓存是否过期,每一次写入都会生成一个sequenceNumber到entry中
                //获取到的Snapshot对象中也有一个sequenceNumber,如果snapshot.sequenceNumber != entry.sequenceNumber
                //则代表获取到的Snapshot是过时的
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            lruEntries.remove(entry.key);
            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }

        //缓存总大小大于设置的缓存上限
        //日志记录大于2000条并且日志记录大于当前lruEntries.size();
        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
}

journalRebuildRequired()方法在journal文件日志行数大于2000并且日志行数大于lruEntries的大小时返回true。

cleanupCallable代码如下,主要对lruEnties的大小进行优化,删除最近使用次数较少的条目

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

trimToSize()方法将lruEntries中使用较少的条目删除,直到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;
        }
};

读取缓存

读取缓存通过调用get(String 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!
            return null;
        }

        redundantOpCount++;
        journalWriter.append(READ + ' ' + key + '\n');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }

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

get(String key)方法内部也会检查是否需要重写journal,是否需要释放内存

Snapshot类结构如下:

  • edit()获取对应缓存的Editor
  • getInputStream(int index)获取缓存的输出流
  • getString(int index) 以String的方式返回缓存的值

移除缓存

通过remove(String key)方法移除key对应的缓存,remove(String key)方法也会对内存和journal进行审查,如下

  public synchronized boolean remove(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null || entry.currentEditor != null) {
            return false;
        }

        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            if (!file.delete()) {
                throw new IOException("failed to delete " + file);
            }
            size -= entry.lengths[i];
            entry.lengths[i] = 0;
        }

        redundantOpCount++;
        journalWriter.append(REMOVE + ' ' + key + '\n');
        lruEntries.remove(key);

        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }

        return true;
}

关闭缓存

通过close()方法来关闭缓存

    public synchronized void close() throws IOException {
        if (journalWriter == null) {
            return; // already closed
        }
        for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
            if (entry.currentEditor != null) {
                entry.currentEditor.abort();
            }
        }
        trimToSize();
        journalWriter.close();
        journalWriter = null;
}

其他API

  • deleteContents(File dir) 删除dir或dir目录下所有文件
  • getDirectory() 获取缓存的目录
  • maxSize() 获取缓存上限
  • size() 当前缓存占用内存总大小
  • isClosed() 判断缓存是否关闭
  • delete() 删除所有缓存
  • flush() 将日志同步到journal文件中

参考

Caching Bitmaps
Android DiskLruCache完全解析,硬盘缓存的最佳方案
DiskLruCache.java

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值