本文主要内容

  • LruCache运用
  • LruCache原理
  • DiskLruCache运用
  • DiskLruCache原理

缓存是一种十分常见的策略,它能有用加强资源加载速度,提高用户体验,降低要害资源运用(比方流量等),Android开发中,加载Bitmap经常需求运用缓存。

Android中缓存一般来说,有两种类型,内存缓存和硬盘缓存,它们都是运用Lru算法(最近最少运用算法),算法的完结都是依赖于LinkedHashMap,关于LinkedHashMap可参考LinkedHashMap原理剖析,本文主要介绍下两种缓存的运用办法以及源码解析。

LruCache运用

先看看LruCache的结构办法:

    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
    };

结构函数中指定最大内存运用值,一起需求重写sizeOf办法,统计保存元素的内存占用巨细。

保存及获取也十分简略:

mMemoryCache.get(key);
mMemoryCache.put(key, bitmap);

运用进程十分简略,当保存元素巨细超越LruCache的最大值时,LruCache会自动收回近期最少运用的元素,以收回内存。接下来我们读读源码,看它是怎么完结的

LruCache原理

LruCache中有3个十分重要的成员变量:

//运用LinkedHashMap存储元素
private final LinkedHashMap<K, V> map;
//当时一切元素总的巨细值
private int size;
//允许的最大值
private int maxSize;

LruCache运用LinkedHashMap来保存元素,依赖于LinkedHashMap,才干知道哪个元素是最近最少运用的。假如size大于maxSize,则开端删去内存保存的元素,收回内存。

看看它的put办法:

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);
        //假如key对应着一个旧的目标,要删去并且减去旧目标的巨细
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
    //调查是否超越maxSize,假如超越则删去近期最少运用的元素
    trimToSize(maxSize);
    return previous;
}

put办法的逻辑比较简略,保存键值对到LinkedHashMap中,一起核算巨细,在trimToSize办法中,假如当时巨细超越最大值了,则要删去近期最少运用元素。

public void trimToSize(int maxSize) {
    //循环删去,直到 size <= maxSize 建立
    while (true) {
        K key;
        V value;
        synchronized (this) {
            if (size <= maxSize) {
                break;
            }
            //找出近期最少运用的Entry目标
            //map.eldest办法,应该便是LinkedHashMap的header目标的after节点
            Map.Entry<K, V> toEvict = map.eldest();
            if (toEvict == null) {
                break;
            }
            //删去并从头核算一切元素的总巨细
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            size -= safeSizeOf(key, value);
            evictionCount++;
        }
        entryRemoved(true, key, value, null);
    }
}

trimToSize办法也比较简略,只要一个疑问点,本人下载的是openJDK 7.0的源码,怎么也找不到 LinkedHashMap的eldest 办法,在Android源码中也找不到此办法,结合对 LinkedHashMap 的了解,只能推断是eader目标的after节点。

LruCache的get办法更加简略:

public final V get(K key) {
    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
    //create办法默许回来为null,除非用户重写create办法,不然接下来的逻辑都不会发生
    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;
    }
}

get办法看起来很长,但十分简略,从LinkedHashMap看取值并回来即可。假如不重写create,后边的逻辑都不会执行。在做Bitmap缓存时,一般也不会重写create办法。

DiskLruCache运用

DiskLruCache运用十分广泛。一般硬盘缓存保存在/sdcard/Android/data/pkg/cache/ 目录下,且缓存文件夹中保存着许多未知文件格局的文件,以及 journal 文件,类似下图:

Android缓存原理

稍后会对 journal 文件进行解说,此处先按下不表。

DiskLruCache的运用稍显杂乱,首要,它并不存在于Android源码中,无法直接引用到,需求下载到本地才可运用。我会把DiskLruCache上传到自己的github中,欢迎取用。

先看看 DiskLruCache 的初始化:

mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1, 50 * 1024 * 1024);

在初始化时需求4个参数:

  • 缓存方位,用于保存缓存文件的路径,通常是/sdcard/Android/data/pkg/cache/
  • appVersion,指当时运用的版本号,并没有特殊的含义,我调查直播吧等app这个值都是1
  • valueCount,一般都传1,标明一个entry中保存着几个缓存文件,一般都是1
  • maxSize,缓存的最大值,上述代码标明的最大值是50M

下面来看它的get办法:

//获取办法
snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
   fileInputStream = (FileInputStream) snapshot.getInputStream(0);
   fileDescriptor = fileInputStream.getFD();
}

get回来的目标并不是我们想要的Bitmap或其它的,而是一个Snapshot目标,它能够供给文件读入流供我们运用,进而拿到Bitmap

接下来看看它的保存办法:

Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(0);
    //拿到输出流,向缓存文件中写入,完结保存
    if (downloadUrlToStream(imageUrl, outputStream)) {
       editor.commit();
    } else {
       editor.abort();
    }
 }

DiskLruCache供给Editor目标,Editor 目标供给文件输出流,用户拿到文件输出流,写文件,文件写完后需求调用commit办法,完结保存。

LruCache和DiskLruCache的完好事例,鄙人一篇博文中将会完好贴出来,还会上传到github中,本文中暂不加以细节描绘。

DiskLruCache原理

DiskLruCache依然运用LinkedHashMap来保存目标,不过目标并不直接保存在LinkedHashMap中,而是保存在文件里,经过文件的输入输出流完结读取与写入。

个人觉得或许DiskLruCache最难了解的大概有两个点:

  • journal 文件
  • 保存的元素与文件的对应联系

DiskLruCache是线程安全的,并且文件的写入是需求时间的,怎么防止文件还未写入完结就被读取呢?

private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";

DiskLruCache添加了4种文件操作状况:

  • DIRTY ,正在写入缓存文件时,此状况下,缓存文件不行读取
  • CLEAN ,写入完结时
  • REMOVE ,删去缓存文件
  • READ ,读取缓存文件

DiskLruCache操作缓存文件,并将操作状况写入journal 文件。

Android缓存原理

journal 文件前4行的含义分别是:

  • libcore.io.DiskLruCache,魔数,类似Java文件的魔数是babycoffee一样,表征文件格局
  • 1,标明DiskLruCache的版本,它的值现在是1,代码中界说的final值,或许后续版本号会增加
  • 1,标明运用版本号,在DiskLruCache的初始化函数中传入的值
  • 1,valueCount,在初始化中传入的值,一般也是1

接下来解说另一个问题,保存的元素与缓存文件之间的对应联系,检查Entry的代码:

public File getCleanFile(int i) {
        return new File(directory, key + "." + i);
    }

回来的文件以key为名,十分容易就一一对应起来。比较有意思的是文件的后缀,假如i等于0,那么后缀则为 “.0”。我们在DiskLruCache的初始化办法中传入valueCount值为1,标明当时Entry对应着1个缓存文件。后缀与valueCount相关,假如valueCount值为2,则i的值能够取0或1,事实上Entry内部有一个数组,数组的长度便是valueCount,它标明对应缓存文件的巨细。

// 标明缓存文件的巨细值
private final long[] lengths;

现在来看看缓存文件的保存进程:

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    checkNotClosed();
    validateKey(key);
    //获取key对应的Entry目标,lruEntries是LinkedHashMap目标
    Entry entry = lruEntries.get(key);
    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && 
            (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
        return null; // Snapshot is stale.
    }
    //假如entry为null,则结构一个新的entry并保存到lruEntries中
    if (entry == null) {
        entry = new Entry(key);
        lruEntries.put(key, entry);
    } else if (entry.currentEditor != null) {
        //假如entry不为null,并且它的currentEditor 也不为null
        //则标明它正在保存傍边,后续能够看到只要正在保存傍边的entry,currentEditor 才不为空
        return null; // Another edit is in progress.
    }
    Editor editor = new Editor(entry);
    //要开端保存了,才赋值currentEditor ,默许currentEditor 是空的
    entry.currentEditor = editor;
    //向journal文件中写入事情,当时文件的状况为DIRTY,是一个脏数据,不行读取的
    // Flush the journal before creating files to prevent file leaks.
    journalWriter.write(DIRTY + ' ' + key + '\n');
    journalWriter.flush();
    return editor;
}

获取Editor 之后,就能够向用户供给文件输出流了,用户运用文件输出流保存缓存文件。

public OutputStream newOutputStream(int index) throws IOException {
        if (index < 0 || index >= valueCount) {
            throw new IllegalArgumentException("Expected index " + index + " to " + "be greater than 0 and less than the maximum value count " + "of " + valueCount);
        }
        synchronized (DiskLruCache.this) {
            if (entry.currentEditor != this) {
                throw new IllegalStateException();
            }
            //entry.readable是一个状况值,标明当时entry是否可读,假如是DIRTY状况,则不行读
            //readable为false,则不行读,那么Editor目标的written值则为true,标明开端来写文件了
            if (!entry.readable) {
                written[index] = true;
            }
            //从entry中获取Dirty file,当写入成功后,会将文件重命名成正式缓存文件
            File dirtyFile = entry.getDirtyFile(index);
            FileOutputStream outputStream;
            try {
                outputStream = new FileOutputStream(dirtyFile);
            } catch (FileNotFoundException e) {
                // Attempt to recreate the cache directory.
                directory.mkdirs();
                try {
                    outputStream = new FileOutputStream(dirtyFile);
                } catch (FileNotFoundException e2) {
                    // We are unable to recover. Silently eat the writes.
                    return NULL_OUTPUT_STREAM;
                }
            }
            return new FaultHidingOutputStream(outputStream);
        }
    }

文件有不同的状况,DIRTY和CLEAN等,假如是脏数据则不行读取,只要是clean状况,用户才干读取。源码中有几个变量来表征这一进程:

  • entry.readable,标明缓存文件是否可读,假如是正在写入文件阶段,则不行读取,会回来空值
  • editor.written,标明文件是否正在写入
  • entry.currentEditor,只要正在写入,脏数据,currentEditor才不为空

用户获取到了文件输出流,比方说从网络上读取,再写入到此文件输出流中,当文件写入成功后,需求调用commit办法,终究调用到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++) {
            //假如文件写完了,written还为false,则标明出现异常,要abort这次提交
            if (!editor.written[i]) {
                editor.abort();
                throw new IllegalStateException("Newly created entry didn't create value for index " + i);
            }
            //假如要写入的dirty file不存在,也是异常情况
            if (!entry.getDirtyFile(i).exists()) {
                editor.abort();
                return;
            }
        }
    }
    //从0遍历到valueCount,假如是commit,那么将dirty file改名成正式文件,一起核算巨细值加入到总巨细值傍边
    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) {
        //写成功后,将readable置为true,一起向 journal 文件写入 clean状况值
        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();
    // LRU算法的要害之处,假如超越巨细了,则需求删去部分缓存文件
    if (size > maxSize || journalRebuildRequired()) {
        executorService.submit(cleanupCallable);
    }
}

在写入成功后,会检测巨细是不是超越最大值了,假如超越,则需求删去部分缓存文件。在cleanupCallable中,最终调用 trimToSize 办法。

private void trimToSize() throws IOException {
    while (size > maxSize) {
        Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
        remove(toEvict.getKey());
    }
}

回想LinkedHashMap中的内容,lruEntries.entrySet().iterator().next(),它其实便是指代的header的after节点,便是近期最少运用节点,删去它,Lru算法也完结了。

接下来我们看看get办法,这个办法十分的简略:

public synchronized Snapshot get(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    //假如没有保存过此key,回来为null
    if (entry == null) {
        return null;
    }
    //假如readable值为false,那么也会回来为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++) {
            //回来clean file的文件输入流
            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++;
    //journal 文件中写入read状况
    journalWriter.append(READ + ' ' + key + '\n');
    if (journalRebuildRequired()) {
        executorService.submit(cleanupCallable);
    }
    //回来Snapshot节点,Snapshot节点中包括缓存文件的输入流,便于用户读取
    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}

从保存缓存文件和读取缓存文件,能够看到 DiskLruCache 规划得十分精妙,它保存时,写入的文件是dirty文件,假如保存成功,将dirty文件改名成clean文件,用户读取缓存文件的时候,则直接回来为clean文件。

关于怎么运用内存缓存与硬盘缓存构建二级缓存,我将鄙人一篇博文中具体阐述