okhttp篇6:DiskLruCache

10 篇文章 0 订阅
6 篇文章 0 订阅
public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,
    int valueCount, long maxSize) {
  if (maxSize <= 0) {
    throw new IllegalArgumentException("maxSize <= 0");
  }
  if (valueCount <= 0) {
    throw new IllegalArgumentException("valueCount <= 0");
  }

  // Use a single background thread to evict entries.
  Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
      new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));

  return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
}

DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize,
    Executor executor) {
  this.fileSystem = fileSystem;
  this.directory = directory;
  this.appVersion = appVersion;
  this.journalFile = new File(directory, JOURNAL_FILE);
  this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
  this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
  this.valueCount = valueCount;
  this.maxSize = maxSize;
  this.executor = executor;
}

create方法是一个静态方法,创建了一个线程池,最大线程个数为1,60s空闲该线程就会被销毁。阻塞队列是LinkedBlockQueue。

在DiskLruCache中,这个线程池只会被用来执行cleanupRunnable。cleanUpRunnable用于在某些时机(如get,edit等方法)执行trimToSize或rebuildJournal方法。

DiskLruCache被创建的时候,就创建了3个文件,journalFile,journalFileTmp,journalFIleBackup。

edit

// DiskLruCache
public @Nullable Editor edit(String key) throws IOException {
  return edit(key, ANY_SEQUENCE_NUMBER);// -1
}

synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
  initialize();

  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.currentEditor != null) {
    return null; // Another edit is in progress.
  }
  if (mostRecentTrimFailed || mostRecentRebuildFailed) {
    // The OS has become our enemy! If the trim job failed, it means we are storing more data than
    // requested by the user. Do not allow edits so we do not go over that limit any further. If
    // the journal rebuild failed, the journal writer will not be active, meaning we will not be
    // able to record the edit, causing file leaks. In both cases, we want to retry the clean up
    // so we can get out of this state!
    executor.execute(cleanupRunnable);
    return null;
  }

  // Flush the journal before creating files to prevent file leaks.
  journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
  journalWriter.flush();

  if (hasJournalErrors) {
    return null; // Don't edit; the journal can't be written.
  }

  if (entry == null) {
    entry = new Entry(key);
    lruEntries.put(key, entry);
  }
  Editor editor = new Editor(entry);
  entry.currentEditor = editor;
  return editor;
}

initialize

public synchronized void initialize() throws IOException {
  assert Thread.holdsLock(this);

  if (initialized) {
    return; // Already initialized.
  }

  // If a bkp file exists, use it instead.
  if (fileSystem.exists(journalFileBackup)) {
    // If journal file also exists just delete backup file.
    if (fileSystem.exists(journalFile)) {
      fileSystem.delete(journalFileBackup);
    } else {
      fileSystem.rename(journalFileBackup, journalFile);
    }
  }

  // Prefer to pick up where we left off.
  if (fileSystem.exists(journalFile)) {
    try {
      readJournal();//
      processJournal();
      initialized = true;
      return;
    } catch (IOException journalIsCorrupt) {
      Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: "
          + journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
    }

    // The cache is corrupted, attempt to delete the contents of the directory. This can throw and
    // we'll let that propagate out as it likely means there is a severe filesystem problem.
    try {
      delete();
    } finally {
      closed = false;
    }
  }

  rebuildJournal();

  initialized = true;
}

initialize干的事情:

  1. 如果存在journalFileBackup,且没有journalFile,就将journalFileBackup重命名成journalFile,否则将journalFileBackup删除。--其实就是journalFileBackup就是个备份。
  2. 如果有journalFile,就执行readJournal和processJournal方法。细节下面再说,大概就是将文件中的内容转换成lruEntries。
  3. 如果没有journalFile,删除directory下面的所有文件(其实就是清空缓存),再执行rebuildJournal(将lruEntries转变成文件存储)。

总结一下,如果有journalFile,就读取内容到lruEntries,如果没有,就将lruEntries的内容硬盘化,存储到文件。

那么下面要分析的主要就是3个方法:

  1. readJournal
  2. processJournal
  3. rebuildJournal

readJournal

private void readJournal() throws IOException {
  BufferedSource source = Okio.buffer(fileSystem.source(journalFile));// 读取journalFile
  try {
    String magic = source.readUtf8LineStrict();// journalFile的格式校验
    String version = source.readUtf8LineStrict();
    String appVersionString = source.readUtf8LineStrict();
    String valueCountString = source.readUtf8LineStrict();
    String blank = source.readUtf8LineStrict();
    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 + "]");
    }

    int lineCount = 0;
    while (true) {// 逐行读取journalFile
      try {
        readJournalLine(source.readUtf8LineStrict());
        lineCount++;
      } catch (EOFException endOfJournal) {// 读完之后直接抛异常,结束while循环
        break;
      }
    }
    redundantOpCount = lineCount - lruEntries.size();

    // If we ended on a truncated line, rebuild the journal before appending to it.
    if (!source.exhausted()) {// 如果都读完了,发现source还有内容,这说明文件出错了。rebuildJournal
      rebuildJournal();
    } else {
      journalWriter = newJournalWriter();// 否则创建journalWriter
    }
  } finally {
    Util.closeQuietly(source);
  }
}

readJournal

  1. 首先读取了journalFile,读取File前面的meta-data做了格式校验。
  2. 然后逐行读取,调用readJournalLine。
  3. 如果读完之后,发现source还没读取完(文件很可能已经损坏了),这时候重建journal文件。否则,给journalWriter重新赋值。

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);
  }// 前面这一大段,都是为了获取这一行中的key

  Entry entry = lruEntries.get(key);// 如果当前的lruEntries中,没有这个key
  if (entry == null) {
    entry = new Entry(key);// 就新创建一个这个Key对应的Entry。这里的Entry存储了这个key对应的cleanFiles和dirtyFiles
    lruEntries.put(key, entry);
  }

// CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 
// clean一行的格式,中间的是key,后面的2个数字会存入entry的lengths
  if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {// 如果这一行是clean开头,说明这个Entry处于可读状态
    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)) {// dirty代表这个entry正在被创建/更新,dirty后面应该跟clean/remove
    entry.currentEditor = new Editor(entry);
  } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {// read代表entry正在
    // This work was already done by calling lruEntries.get().
  } else {
    throw new IOException("unexpected journal line: " + line);
  }
}

readJournalLine,根据journalFile中每一行,取出key,如果lruEntries中没有这个key,就会新建一个Entry(Entry实际是Request的缓存。里面维护了两个文件,一个文件存储了Request和Response的header,另一个文件存储了Response的body)。

取出一行的标志:

  1. CLEAN 设置entry为readable,并且给entry设置lengths。
  2. DIRTY 为entry设置Editor(为了给后面processJournal清空Entry中的文件)。

processJournal

private void processJournal() throws IOException {
  fileSystem.delete(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];// 统计所有文件的size
      }
    } else {
      entry.currentEditor = null;// entry.currentEditor != null,代表这是一行dirty 中的key代表的entry,删除entry中的对应文件
      for (int t = 0; t < valueCount; t++) {
        fileSystem.delete(entry.cleanFiles[t]);
        fileSystem.delete(entry.dirtyFiles[t]);
      }
      i.remove();
    }
  }
}

这个方法主要统计所有entry的size(内存中不可能维护所有Request的Response,为了控制内存大小,DiskLruCache会有一个maxSize的限制,一旦entry的size超过maxSize,就开始移除entry,这也是这个类为什么叫LruCache的关系)。

除了统计size,还会移除所有dirty entry对应的文件。一般dirty行后会跟clean / remove行。clean会将entry的currentEditor置为空。而remove行会将entry移除。

所以,如果到processJournal方法中,entry的currentEditor != null,这说明,很可能这个entry还没有edit完成(请求还没有结束),所以这个entry对应的文件是无效的,所以要将entry的文件全都清除,并将entry从lruEntries中移除。

rebuildJournal

synchronized void rebuildJournal() throws IOException {
  if (journalWriter != null) {// 走到rebuildJournal一般代表journalFile中的文件已经被损坏(有可能只写入一部分,文件就意外关闭了)
    journalWriter.close();
  }
// 往journalFileTmp写入lruEntries
  BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
  try {
    writer.writeUtf8(MAGIC).writeByte('\n');
    writer.writeUtf8(VERSION_1).writeByte('\n');
    writer.writeDecimalLong(appVersion).writeByte('\n');
    writer.writeDecimalLong(valueCount).writeByte('\n');
    writer.writeByte('\n');

    for (Entry entry : lruEntries.values()) {
      if (entry.currentEditor != null) {// dirty的标志就是currentEditor != null
        writer.writeUtf8(DIRTY).writeByte(' ');
        writer.writeUtf8(entry.key);
        writer.writeByte('\n');
      } else {
        writer.writeUtf8(CLEAN).writeByte(' ');
        writer.writeUtf8(entry.key);
        entry.writeLengths(writer);
        writer.writeByte('\n');
      }
    }
  } finally {
    writer.close();
  }

  if (fileSystem.exists(journalFile)) {
    fileSystem.rename(journalFile, journalFileBackup);
  }
  fileSystem.rename(journalFileTmp, journalFile);
  fileSystem.delete(journalFileBackup);

  journalWriter = newJournalWriter();// 对应journalFile的sink
  hasJournalErrors = false;
  mostRecentRebuildFailed = false;
}

相比readJournal和processJournal是读取journalFile的内容到lruEntries的过程,rebuildJournal就是将lruEntries写到journalFile的过程。

这三个方法写了一长串,但实际上都是initialize干的活。

回顾下之前总结的initialize的细节:

如果有journalFile,就将journalFile转换成lruEntries。如果这一行的标志是 DIRTY,就将key对应的Entry的currentEditor设置成不为空。如果是CLEAN,就将Entry设置成readable=true。

如果没有journalFile,就遍历lruEntries,将key写入文件,如果key对应的Entry的currentEditor != null,标志成DIRTY行,否则标志成clean行。

 再回看edit方法

synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {// 返回一个Editor
  initialize();// journalFile与lruEntries的相互转换,只会执行一遍

  checkNotClosed();
  validateKey(key);
  Entry entry = lruEntries.get(key);
  /**中间省略**/
  // Flush the journal before creating files to prevent file leaks.
  journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');// edit方法写入一个DIRTY行
  journalWriter.flush();

  if (hasJournalErrors) {
    return null; // Don't edit; the journal can't be written.
  }

  if (entry == null) {
    entry = new Entry(key);
    lruEntries.put(key, entry);
  }
  Editor editor = new Editor(entry);
  entry.currentEditor = editor;// 给key对应的Entry设置Editor
  return editor;
}

edit方法,最终,是往journalFile写入一个DIRTY行,并为key对应的Entry创建了一个Editor。

Editor实际是对Entry中的两个文件进行读写的。(dirtyFile和cleanFile)

看下Editor的结构:

newSink对应写入,newSource对应读取,commit方法用于将dirtyFiles重命名为cleanFiles。

newSink和newSource方法都有一个int参数。

看引用方式,可以知道Entry的cleanFiles和dirtyFiles之所以是一个数组,并且values为2,是因为一个File会存储META_DATA(包含的具体信息后面会讲),一个File存储Response 的body。

所以这样就可以清楚Entry的具体存储内容:

  1. key对应request的url。
  2. dirtyFiles和cleanFiles都是一个长度为2的File[]数组,ENTRY_METADATA索引对应的File存储的是Request的信息。ENTRY_BODY存储的是Response。

实例

上面介绍了DiskLruCache的部分代码,主要包括lruEntries与journalFile的相互转换。最后看出Entry的cleanFile实际是存储Request和Response信息的载体。这实际就是缓存了。对应DiskLruCache总的DiskCache(硬盘缓存)部分。而Lru部分(内存策略)后面再说。

现在先介绍,CacheInterceptor是怎么利用DiskLruCache的。

读取DiskLruCache中存储的Response

 cache.get

// Cache
@Nullable Response get(Request request) {
  String key = key(request.url());
  DiskLruCache.Snapshot snapshot;
  Entry entry;
  try {
    snapshot = cache.get(key);// 获取key对应的Entry的SnapShot
    if (snapshot == null) {
      return null;
    }
  } catch (IOException e) {
    // Give up because the cache cannot be read.
    return null;
  }

  try {
    entry = new Entry(snapshot.getSource(ENTRY_METADATA));// 传入ENTRY_METADATA对应的文件Source
  } catch (IOException e) {
    Util.closeQuietly(snapshot);
    return null;
  }

  Response response = entry.response(snapshot);// 将文件转成Response

  if (!entry.matches(request, response)) {
    Util.closeQuietly(response.body());
    return null;
  }
  return response;
}

// DiskLruCache
public synchronized Snapshot get(String key) throws IOException {
  initialize();

  checkNotClosed();
  validateKey(key);
  Entry entry = lruEntries.get(key);
  if (entry == null || !entry.readable) return null;
// 跟原来的entry的值几乎一样,只是file[]变成了source[],方便读取文件
  Snapshot snapshot = entry.snapshot();
  if (snapshot == null) return null;
  /****/
  return snapshot;
}

Snapshot snapshot() {
    if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();

    Source[] sources = new Source[valueCount];
    long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
    try {
      for (int i = 0; i < valueCount; i++) {
        sources[i] = fileSystem.source(cleanFiles[i]);// 获取cleanFile对应的Source
      }
      return new Snapshot(key, sequenceNumber, sources, lengths);
    } catch (FileNotFoundException e) {
     /****/
  }
}

总结下:

cache.get方法(传入Request,获取Request对应的Response)

通过调用DiskLruCache.get方法,从lruEntries通过key(request.url),获取到对应的Entry(使用文件存储了META_DATA和Response的body)的snapShot。

然后读取snapshot[ENTRY_METADATA]对应的文件内容,构建Response的Header。

读取snapshot[ENTRY_BODY],构建Response的body。

META_DATA存储的内容包括:

url ,method,request的header(2个),response code,response的header(3个),加密算法,证书链,TLS版本号

具体构建如下:

// Cache.Entry
// 负责将文件转成Response
public Response response(DiskLruCache.Snapshot snapshot) {
  String contentType = responseHeaders.get("Content-Type");
  String contentLength = responseHeaders.get("Content-Length");
  Request cacheRequest = new Request.Builder()
      .url(url)
      .method(requestMethod, null)
      .headers(varyHeaders)
      .build();
  return new Response.Builder()
      .request(cacheRequest)
      .protocol(protocol)
      .code(code)
      .message(message)
      .headers(responseHeaders)
      .body(new CacheResponseBody(snapshot, contentType, contentLength))
      .handshake(handshake)
      .sentRequestAtMillis(sentRequestMillis)
      .receivedResponseAtMillis(receivedResponseMillis)
      .build();
}

而在CacheResponseBody中,使用ENTRY_BODY读取了Response的body。

CacheResponseBody(final DiskLruCache.Snapshot snapshot,
    String contentType, String contentLength) {
  this.snapshot = snapshot;
  this.contentType = contentType;
  this.contentLength = contentLength;

  Source source = snapshot.getSource(ENTRY_BODY);
  bodySource = Okio.buffer(new ForwardingSource(source) {
    @Override public void close() throws IOException {
      snapshot.close();
      super.close();
    }
  });
}

DiskLruCache的LRU机制

LRU

Least-Recently-Used,最近最少使用算法,是一种内存数据淘汰策略。这里的内存数据,指的是DiskLruCache中的lruEntries。

// DiskLruCache
void trimToSize() throws IOException {
  while (size > maxSize) {
    Entry toEvict = lruEntries.values().iterator().next();
    removeEntry(toEvict);
  }
  mostRecentTrimFailed = false;
}

DiskLruCache有一个trimToSize的方法。从命名就可以看出,这个方法是为了将lruEntries的大小缩减至size。

这里的size会在lruEntries有变动的时候,进行变化。比如之前在edit方法的时候,有个initialize方法。这个方法在将journal文件读到lruEntries的时候,就通过Entry(DiskLruCache)的lengths数组统计过size。

并在每一次Editor的commit方法中(有改动才会调用commit方法),重新计算size。

因此,这个size实际就是lruEntries维护的所有Entry的文件大小。

而trimToSize也很简单,只是当size > maxSize的时候,就移除lruEntries中的Entry。

lruEntries是一个LinkedHashMap,可以记录Entry的插入顺序。

调移除的时候,调用values().interator()会从最先插入的Entry开始移除。

但其实这里并没有完全符合Lru。Lru即使是最先插入,但是如果调用频率很高的话,还是不会被最新移除的。

但是考虑到http请求的实际情况,很少会出现,老早请求的Request,还会不断地请求。这样也许就情有可原了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值