LruCache


初始化

  /** * @param maxSize for caches that do not override {@link #sizeOf}, this is * the maximum number of entries in the cache. For all other caches, * this is the maximum sum of the sizes of the entries in this cache. */    public LruCache(int maxSize) {        if (maxSize <= 0) {            throw new IllegalArgumentException("maxSize <= 0");        }        this.maxSize = maxSize;        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);    }

其中指定了最大缓存不能超过maxSize这个数值,其次,初始化了一个LinkedHashMap集合,我们知道,LinkedHashMap就是在HashMap中维护了一个链表记录插入的记录,如果我们把最后一个参数设置为true,那么我们取出的值就是我们按我们访问的顺序去取的。

另外,还有个方法是必须要实现的:

   /**     * Returns the size of the entry for {@code key} and {@code value} in     * user-defined units.  The default implementation returns 1 so that size     * is the number of entries and max size is the maximum number of entries.     *     * <p>An entry's size must not change while it is in the cache.     */    protected int sizeOf(K key, V value) { return 1;    }

这个方法是测量我们我们实体的大小,不实现,它就会默认实现1了。所以,这个是硬要求。


放入缓存

 public final V put(K key, V value) {        if (key == null || value == null) {            throw new NullPointerException("key == null || value == null");        }        V previous;        synchronized (this) {            putCount++;            size += safeSizeOf(key, value);            previous = map.put(key, value);            if (previous != null) {                size -= safeSizeOf(key, previous);            }        }        if (previous != null) {            entryRemoved(false, key, previous, value);        }        trimToSize(maxSize);        return previous;    }

如果插入了空的key和value,它就会抛出异常。然后,放入的计数器加一,内存计数器加上新的Entry的大小。如果内存中已经存在这个值了,那么,我们的的Put操作算是覆盖操作,所以,我们得减去,刚才内存计数器加上的值。接下来,调用了

   if (previous != null) {            entryRemoved(false, key, previous, value);        }

有意思的是这个方法是一个空实现,是留给我们覆盖用的,相当于一个回调把。

    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

大概意思就是说,当我们Put一个实体进去,如果是Map中是有相同key值的话,那么,我们相当于从内存中抹去了一个实体部分,什么key,oldValue,newValue都会给我们,evicted给了个false,先记着,继续看。接下来就调用了trimToSize()方法

    public void trimToSize(int maxSize) {        while (true) {            K key;            V value;            synchronized (this) {                if (size < 0 || (map.isEmpty() && size != 0)) {                    throw new IllegalStateException(getClass().getName()                            + ".sizeOf() is reporting inconsistent results!");                }                if (size <= maxSize || map.isEmpty()) {                    break;                }                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();                key = toEvict.getKey();                value = toEvict.getValue();                map.remove(key);                size -= safeSizeOf(key, value);                evictionCount++;            }            entryRemoved(true, key, value, null);        }    }

这个方法就是一直循环,清理老的实体部分,直到满足,我们的LruCache所占内存小于我们初始化给定的maxSize或者是我们内内部的LinkedHashMap为空为止。有意思的是,每移除一个实体,又会调用entryRemoved()方法,只不过参数变成了 true,key,value,null。


获取缓存

public final V get(K key) {
if (key == null) {
throw new NullPointerException(“key == null”);
}

    V mapValue;    synchronized (this) {        mapValue = map.get(key);        if (mapValue != null) {            hitCount++;            return mapValue;        }        missCount++;    }    /*     * Attempt to create a value. This may take a long time, and the map     * may be different when create() returns. If a conflicting value was     * added to the map while create() was working, we leave that value in     * the map and release the created value.     */    V createdValue = create(key);    if (createdValue == null) {        return null;    }    synchronized (this) {        createCount++;        mapValue = map.put(key, createdValue);        if (mapValue != null) {            // There was a conflict so undo that last put            map.put(key, mapValue);        } else {            size += safeSizeOf(key, createdValue);        }    }    if (mapValue != null) {        entryRemoved(false, key, createdValue, mapValue);        return mapValue;    } else {        trimToSize(maxSize);        return createdValue;    }}

第一步,如果传进来的参数是null的话,那么抛出个异常。第二步,如果缓存找到了,那么命中的计数器加一,返回我们从内存中找到的缓存。否则,没有找到的计数器加一,继续。第三步有点意思:

    V createdValue = create(key);        if (createdValue == null) {            return null;        }

如果LruCache中没有实体部分的话,就创建一个实体部分,好,我们点进create()这个方法:

 protected V create(K key) {        return null;    }

所以,第三步默认就是返回个null给我们了。这不是忽悠我们么,一起看看注释:

If a value for key exists in the cache when this method
returns, the created value will be released with entryRemoved and discarded.

原来,这个方法和是我们之前分析的entryRemoved和硬盘缓存结合起来用的,所以,知道之前,为什么要把抹去的实体部分和新增的实体部分回调给我们了吧?好,我们接着继续看,第四步,

   synchronized (this) {            createCount++;            mapValue = map.put(key, createdValue);            if (mapValue != null) {                // There was a conflict so undo that last put                map.put(key, mapValue);            } else {                size += safeSizeOf(key, createdValue);            }        }

我们把新创建的实体给放到我们的LinkedHashMap里面去,如果mapValue不为空的话,说明,之前LinkedHashMap已经有值了,我们也许是覆盖错了(可能key相同,但是value不相同),那么,我们再重新把覆盖的值给放进去,如果为空的话,那么,我们就要在所占内存的基础上加上这个值了。继续:

  if (mapValue != null) {            entryRemoved(false, key, createdValue, mapValue);            return mapValue;        } else {            trimToSize(maxSize);            return createdValue;        }

第六步就很简单了,就是根据我们的mapValue去选择是回调还是清理数据了。


清理缓存

清理一个实体就不用多说了,就是根据key来移除LinkedHashMap中的实体部分。

  public final V remove(K key) {        if (key == null) {            throw new NullPointerException("key == null");        }        V previous;        synchronized (this) {            previous = map.remove(key);            if (previous != null) {                size -= safeSizeOf(key, previous);            }        }        if (previous != null) {            entryRemoved(false, key, previous, null);        }        return previous;    }

我们来看看,清除所有缓存的方法:

    public final void evictAll() {        trimToSize(-1); // -1 will evict 0-sized elements    }

给trimToSize方法传入了-1,点进去:

public void trimToSize(int maxSize) {        while (true) {            K key;            V value;            synchronized (this) {                if (size < 0 || (map.isEmpty() && size != 0)) {                    throw new IllegalStateException(getClass().getName()                            + ".sizeOf() is reporting inconsistent results!");                }                if (size <= maxSize || map.isEmpty()) {                    break;                }                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();                key = toEvict.getKey();                value = toEvict.getValue();                map.remove(key);                size -= safeSizeOf(key, value);                evictionCount++;            }            entryRemoved(true, key, value, null);        }    }

原来,我们的maxSize就变成了-1,这样就会一直循环删除缓存了,直到LinkedHashMap中的size为0为止。


DiskLruCache


初始化

  private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {        this.directory = directory;        this.appVersion = appVersion;        this.journalFile = new File(directory, JOURNAL_FILE);        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);        this.valueCount = valueCount;        this.maxSize = maxSize;    }

DiskLruCache的构造方法私有化,意味着,我们不能直接从外界new出这个对象,要借助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对象,其中传入的参数,文档上写的也很是详细:

 * @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

继续往下看,如果DiskLruCache对象的日志文件存在的话,我们先读取日志文件,然后处理日志文件,然后生成一个BufferedWriter用于对日志文件的操作,最后返回DiskLruCache对象。如果,日志文件不存在,那么就默认初始化,也没什么难点。


日志文件

 * This cache uses a journal file named "journal". A typical journal file * looks like this: *     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

我们参照这日志文件的格式,来分析对它的操作。

1.新建日志文件

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

首先把头信息写进入,然后内存中的LinkedHashMap的数据写到日志文件中,包括了“脏数据”和“干净的数据”。最后生成了一个日志文件的Writer。

2.读取日志文件

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

首先读取头部信息,如果不是我们的日志文件,那么就抛出个异常,如果是日志文件,就一行一行的读取内容了,

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

我们的内容都是通过空格键来区分的,parts数组的长度肯定大于2,这很好理解,如果小于2,说明这不是一个符合要求的行。如果读取的操作指令是REMOVE,则我们需要在LinkedHashMap删除掉这个记录。根据我们的日志文件的信息从内存中查找操作记录,接下来:

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

如果是“干净的数据”,代码很好理解,至于“脏数据”和 READ标识的数据,我们就要从下面看了。

3.处理日志文件

    private void processJournal() throws IOException {        deleteIfExists(journalFileTmp);        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {            Entry entry = i.next();            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));                    deleteIfExists(entry.getDirtyFile(t));                }                i.remove();            }        }    }

可以看到,通过我们读取日志文件,在内存中生成了对日志文件的映射,下面的代码,我们很清楚它到底要做什么了,根据currentEditor是否为空,来判断数据到底可用,如果currentEditor为空,那么我们记录它的大小,如果不为空,则删除文件。


写入缓存文件

 public Editor edit(String key) throws IOException {        return edit(key, ANY_SEQUENCE_NUMBER);    }    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;    }

在正式读取文件之前,我们需要为它生成一个Editor对象,而这个Editor就是针对一个Entry来操作的,把通过和文件的key获取的Entry的currentEditor赋值成我们刚刚生成的editor,并且把在日志文件中记录这个key的文件是个“脏数据”。接下来,在Editor内部就要生成一个OutputStream用于写入文件了:

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

关于,index我们默认是传入0的,因为一个实体Entry内部可能会维护多个value,就是说,一个key,我们可以保存多个file,这个和初始化的valueCount属性有关系。

当写入操作结束后,或者写入异常,我们会调用这两个方法:

  /** * Commits this edit so it is visible to readers. This releases the * edit lock so another edit may be started on the same key. */        public void commit() throws IOException {            if (hasErrors) {                completeEdit(this, false);                remove(entry.key); // the previous entry is stale            } else {                completeEdit(this, true);            }        }        /** * Aborts this edit. This releases the edit lock so another edit may be * started on the same key. */        public void abort() throws IOException {            completeEdit(this, false);        }

至于hasErrors这个表示当我们写入的时候发生异常,我们就会把这个改为true。然而不管成功失败与否,都调用了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[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');        }        if (size > maxSize || journalRebuildRequired()) {            executorService.submit(cleanupCallable);        }    }

第一步,editor肯定是当前操作的Entry的currentEditor,不然就不合法了;第二步,success && ! entry.readable 这个判断条件是什么意思呢?我们知道,当我们打开DiskLruCache的时候,会根据我们的日志文件来设置给他true的,就是说如果它的readable属性为true,肯定是有一次写入成功的操作的,如果不为true,则是第一次提交,那么,我们肯定有dirtyFile的,因为我们向外提供的输出流是针对dirtyFile操作的,

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

这样是不是一目了然了。第三步,如果成功了,我们就把dirtyFile改成cleanFile,提供外界读取,并累计缓存文件的大小。当然,如果读取失败,我们就删除掉dirtyFile。第四步,就是将readable设置为true,当然这是针对第一次写入操作的,然后就写入,CLEAD指令,如果写入不成功则写入REMOVE操作。第五步,就是针对缓存所做的处理:

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

至于,什么时候rebuild日志文件,看看journalRebuildRequired()方法:

   private boolean journalRebuildRequired() {        final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;        return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD                && redundantOpCount >= lruEntries.size();    }

这个就是防止,日志文件过大的一种处理策略,redundantOpCount是针对日志文件的操作次数。


读取缓存文件

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

相对于写入缓存文件来说,读取就比较简单了。前面都是一些是否可读的判断,到我们的Entry的去找我们的cleanFile,将他们的输入流封装到Snapshot对象供外界读取。


移除缓存文件

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

分析完写入的操作,这个理解起来真的太简单了,无非就是删除实体部分的cleanFile,写入REMOVE指令,并且移除了LinkedHashMap中的数据。

总结

1、LruCache和DiskLruCache内部中都维护了一个LinkedHashMap,而LinkedHashMap中又维护了一条链表用于记录,插入或访问的顺序,我们根据这个特性,可以移除最不经常使用的实体部分。唯一不同的是,LruCache中value维护的就是真的实体部分,而DiskLruCache中value维护的是日志文件中的数据,我们根据DiskLruCache中维护的value去映射成一个个的实体部分,实体部分针对我们文件的操作,比如写入,读取。

2、需要注意的地方是,DiskLruCache是如何根据日志文件知道我们写入的状态呢,是根据日志文件中的指令,比如DIRTY,CLEAN,REMOVE等,如果初始化状态,我们对Entry进行了写入操作,那么写的文件是dirtyFile,只有成功之后,才将Entry的readable的表示改成true,把dirtyFile改成cleanFile,我们读取的时候就是根据readable是否为true,才去读取cleanFile的。

更多相关文章

  1. NPM 和webpack 的基础使用
  2. 【阿里云镜像】使用阿里巴巴DNS镜像源——DNS配置教程
  3. Android(安卓)IjkPlayer 视频播放器
  4. Android实时抓取日志,生成文件
  5. Android调用MediaScanner进行扫描
  6. Android(安卓)广播监听USB插拔
  7. Android(安卓)WebView
  8. android assets/raw 大文件读取
  9. android 代码混淆总结

随机推荐

  1. 浅析Android中的消息机制
  2. Android(安卓)隐藏显示键盘
  3. 安卓报错:java.lang.RuntimeException: Un
  4. ListPreference
  5. Android(安卓)JB 4.2 中InputManager 启
  6. AndroidManifest.xml文件详解(service)
  7. 笔记——Android(安卓)中的小细节
  8. java.lang.ClassCastException: android.
  9. Android创建和删除桌面快捷方式
  10. Android处理各种触摸事件