Android最全Android DiskLruCache 源码解析 硬盘缓存的绝佳方案,腾讯 三面

文末

那么对于想坚持程序员这行的真的就一点希望都没有吗?
其实不然,在互联网的大浪淘沙之下,留下的永远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
Android希望=技能+面试

  • 技能
  • 面试技巧+面试题

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

try {

readJournalLine(reader.readLine());

lineCount++;

} catch (EOFException endOfJournal) {

break;

}

}

redundantOpCount = lineCount - lruEntries.size();

// If we ended on a truncated line, rebuild the journal before appending to it.

if (reader.hasUnterminatedLine()) {

rebuildJournal();

} else {

journalWriter = new BufferedWriter(new OutputStreamWriter(

new FileOutputStream(journalFile, true), Util.US_ASCII));

}

} finally {

Util.closeQuietly(reader);

}

}

首先校验文件头,接下来调用readJournalLine按行读取内容。我们来看看readJournalLine中的操作。

private void readJournalLine(String line) throws IOException {

int firstSpace = line.indexOf(’ ');

if (firstSpace == -1) {

throw new IOException("unexpected journal line: " + line);

}

int keyBegin = firstSpace + 1;

int secondSpace = line.indexOf(’ ', keyBegin);

final String key;

if (secondSpace == -1) {

key = line.substring(keyBegin);

if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {

lruEntries.remove(key);

return;

}

} else {

key = line.substring(keyBegin, secondSpace);

}

Entry entry = lruEntries.get(key);

if (entry == null) {

entry = new Entry(key);

lruEntries.put(key, entry);

}

if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {

String[] parts = line.substring(secondSpace + 1).split(" ");

entry.readable = true;

entry.currentEditor = null;

entry.setLengths(parts);

} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {

entry.currentEditor = new Editor(entry);

} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {

// This work was already done by calling lruEntries.get().

} else {

throw new IOException("unexpected journal line: " + line);

}

}

大家可以回忆下:每个记录至少有一个空格,有的包含两个空格。首先,拿到key,如果是REMOVE的记录呢,会调用lruEntries.remove(key);

如果不是REMOVE记录,继续往下,如果该key没有加入到lruEntries,则创建并且加入。

接下来,如果是CLEAN开头的合法记录,初始化entry,设置readable=true,currentEditor为null,初始化长度等。

如果是DIRTY,设置currentEditor对象。

如果是READ,那么直接不管。

ok,经过上面这个过程,大家回忆下我们的记录格式,一般DIRTY不会单独出现,会和REMOVE、CLEAN成对出现(正常操作);也就是说,经过上面这个流程,基本上加入到lruEntries里面的只有CLEAN且没有被REMOVE的key。

好了,回到readJournal方法,在我们按行读取的时候,会记录一下lineCount,然后最后给redundantOpCount赋值,这个变量记录的应该是没用的记录条数(文件的行数-真正可以的key的行数)。

最后,如果读取过程中发现journal文件有问题,则重建journal文件。没有问题的话,初始化下journalWriter,关闭reader。

readJournal完成了,会继续调用processJournal()这个方法内部:

private void processJournal() throws IOException {

deleteIfExists(journalFileTmp);

for (Iterator 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();

}

}

}

统计所有可用的cache占据的容量,赋值给size;对于所有非法DIRTY状态(就是DIRTY单独出现的)的entry,如果存在文件则删除,并且从lruEntries中移除。此时,剩的就真的只有CLEAN状态的key记录了。

ok,到此就初始化完毕了,太长了,根本记不住,我带大家总结下上面代码。

根据我们传入的dir,去找journal文件,如果找不到,则创建个,只写入文件头(5行)。

如果找到,则遍历该文件,将里面所有的CLEAN记录的key,存到lruEntries中。

这么长的代码,其实就两句话的意思。经过open以后,journal文件肯定存在了;lruEntries里面肯定有值了;size存储了当前所有的实体占据的容量;。


四、存入缓存

还记得,我们前面说过是怎么存的么?

String key = generateKey(url);

DiskLruCache.Editor editor = mDiskLruCache.edit(key);

OuputStream os = editor.newOutputStream(0);

//…after op

editor.commit();

那么首先就是editor方法;

/**

  • Returns an editor for the entry named {@code key}, or null if another

  • edit is in progress.

*/

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;

}

首先验证key,可以必须是字母、数字、下划线、横线(-)组成,且长度在1-120之间。

然后通过key获取实体,因为我们是存,只要不是正在编辑这个实体,理论上都能返回一个合法的editor对象。

所以接下来判断,如果不存在,则创建一个Entry加入到lruEntries中(如果存在,直接使用),然后为entry.currentEditor进行赋值为new Editor(entry);,最后在journal文件中写入一条DIRTY记录,代表这个文件正在被操作。

注意,如果entry.currentEditor != null不为null的时候,意味着该实体正在被编辑,会retrun null ;

拿到editor对象以后,就是去调用newOutputStream去获得一个文件输入流了。

/**

  • Returns a new unbuffered output stream to write the value at

  • {@code index}. If the underlying output stream encounters errors

  • when writing to the filesystem, this edit will be aborted when

  • {@link #commit} is called. The returned output stream does not throw

  • IOExceptions.

*/

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

}

if (!entry.readable) {

written[index] = true;

}

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

}

}

首先校验index是否在valueCount范围内,一般我们使用都是一个key对应一个文件所以传入的基本都是0。接下来就是通过entry.getDirtyFile(index);拿到一个dirty File对象,为什么叫dirty file呢,其实就是个中转文件,文件格式为key.index.tmp。

将这个文件的FileOutputStream通过FaultHidingOutputStream封装下传给我们。

最后,别忘了我们通过os写入数据以后,需要调用commit方法。

public void commit() throws IOException {

if (hasErrors) {

completeEdit(this, false);

remove(entry.key); // The previous entry is stale.

} else {

completeEdit(this, true);

}

committed = true;

}

首先通过hasErrors判断,是否有错误发生,如果有调用completeEdit(this, false)且调用remove(entry.key);。如果没有就调用completeEdit(this, true);

那么这里这个hasErrors哪来的呢?还记得上面newOutputStream的时候,返回了一个os,这个os是FileOutputStream,但是经过了FaultHidingOutputStream封装么,这个类实际上就是重写了FilterOutputStream的write相关方法,将所有的IOException给屏蔽了,如果发生IOException就将hasErrors赋值为true.

这样的设计还是很nice的,否则直接将OutputStream返回给用户,如果出错没法检测,还需要用户手动去调用一些操作。

接下来看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 (!editor.written[i]) {

editor.abort();

throw new IllegalStateException("Newly created entry didn’t create value for index " + i);

}

if (!entry.getDirtyFile(i).exists()) {

editor.abort();

return;

}

}

}

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

}

journalWriter.flush();

if (size > maxSize || journalRebuildRequired()) {

executorService.submit(cleanupCallable);

}

}

首先判断if (success && !entry.readable)是否成功,且是第一次写入(如果以前这个记录有值,则readable=true),内部的判断,我们都不会走,因为written[i]在newOutputStream的时候被写入true了。而且正常情况下,getDirtyFile是存在的。

接下来,如果成功,将dirtyFile 进行重命名为 cleanFile,文件名为:key.index。然后刷新size的长度。如果失败,则删除dirtyFile.

接下来,如果成功或者readable为true,将readable设置为true,写入一条CLEAN记录。如果第一次提交且失败,那么就会从lruEntries.remove(key),写入一条REMOVE记录。

写入缓存,肯定要控制下size。于是最后,判断是否超过了最大size,或者需要重建journal文件,什么时候需要重建呢?

private boolean journalRebuildRequired() {

final int redundantOpCompactThreshold = 2000;

return redundantOpCount >= redundantOpCompactThreshold //

&& redundantOpCount >= lruEntries.size();

}

如果redundantOpCount达到2000,且超过了lruEntries.size()就重建,这里就可以看到redundantOpCount的作用了。防止journal文件过大。

ok,到此我们的存入缓存就分析完成了。再次总结下,首先调用editor,拿到指定的dirtyFile的OutputStream,你可以尽情的进行写操作,写完以后呢,记得调用commit.

commit中会检测是你是否发生IOException,如果没有发生,则将dirtyFile->cleanFile,将readable=true,写入CLEAN记录。如果发生错误,则删除dirtyFile,从lruEntries中移除,然后写入一条REMOVE记录。


五、读取缓存

DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);

if (snapShot != null) {

InputStream is = snapShot.getInputStream(0);

}

那么首先看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;

}

// 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!

for (int i = 0; i < valueCount; i++) {

if (ins[i] != null) {

Util.closeQuietly(ins[i]);

} else {

break;

}

}

return null;

}

redundantOpCount++;

journalWriter.append(READ + ’ ’ + key + ‘\n’);

if (journalRebuildRequired()) {

executorService.submit(cleanupCallable);

}

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

}

get方法比较简单,如果取到的为null,或者readable=false,则返回null.否则将cleanFile的FileInputStream进行封装返回Snapshot,且写入一条READ语句。

然后getInputStream就是返回该FileInputStream了。

好了,到此,我们就分析完成了创建DiskLruCache,存入缓存和取出缓存的源码。

除此以外,还有一些别的方法我们需要了解的。


六、其他方法

remove()

/**

  • Drops the entry for {@code key} if it exists and can be removed. Entries

  • actively being edited cannot be removed.

  • @return true if an entry was removed.

*/

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.exists() && !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);

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

lable);

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

[外链图片转存中…(img-Sa0G57Xt-1715224809723)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-aRSiOa47-1715224809723)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值