本文主要内容
- 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 文件,类似下图:
稍后会对 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 文件。
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文件。
关于怎么运用内存缓存与硬盘缓存构建二级缓存,我将鄙人一篇博文中具体阐述