OkHttp深入学习(三)——Cache

来源:互联网 发布:中关村软件破解 编辑:程序博客网 时间:2024/06/16 02:30

转载请注明出处:http://blog.csdn.net/evan_man/article/details/51182087

 通过前面《OkHttp深入学习(一)——初探》和《OkHttp深入学习(二)——网络》两节的学习基本上对于okhttp的使用和实现有了一定的了解,不过还有一些比较重要的概念如缓存、ConnectionPool和OkHttpClient等都没有进行详细的说明。因此本节对okhttp的Cache如何实现进行介绍.

Cache.class

该对象拥有一个DiskLruCache引用。
private final DiskLruCache cache; 
Cache()@Cache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public Cache(File directory, long maxSize) {  
  2.     this(directory, maxSize, FileSystem.SYSTEM);  
  3.   }  
  4. Cache(File directory, long maxSize, FileSystem fileSystem) {  
  5.     this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);  
  6. }  
public Cache(File directory, long maxSize) {    this(directory, maxSize, FileSystem.SYSTEM);  }Cache(File directory, long maxSize, FileSystem fileSystem) {    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);}
Cache构造器接受两个参数,意味着如果我们想要创建一个缓存必须指定缓存文件存储的目录和缓存文件的最大值。下面看两个常用方法,get()&put()。
get()@Cache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. Response get(Request request) {  
  2.     String key = urlToKey(request); //note 1  
  3.     DiskLruCache.Snapshot snapshot;  
  4.     Entry entry;  
  5.     snapshot = cache.get(key); //note 2  
  6.       if (snapshot == null) {  
  7.         return null;  
  8.     }  
  9.    entry = new Entry(snapshot.getSource(ENTRY_METADATA)); //note 3 getEntry  
  10.     Response response = entry.response(snapshot); //note4  
  11.     if (!entry.matches(request, response)) { //note5  
  12.       Util.closeQuietly(response.body());    
  13.       return null;  
  14.     }  
  15.     return response;  
  16.   }  
Response get(Request request) {    String key = urlToKey(request); //note 1    DiskLruCache.Snapshot snapshot;    Entry entry;    snapshot = cache.get(key); //note 2      if (snapshot == null) {        return null;    }   entry = new Entry(snapshot.getSource(ENTRY_METADATA)); //note 3 getEntry    Response response = entry.response(snapshot); //note4    if (!entry.matches(request, response)) { //note5      Util.closeQuietly(response.body());        return null;    }    return response;  }
1、Util.md5Hex(request.url().toString());将客户的请求的url换成成32个字符的MD5字符串
2、等价于DiskLruCache.Snapshot = DiskLruCache.get(String)利用前面得到的key从DiskLruCache中获取到对应的DiskLruCache.Snapshot。该方法底层实现稍后我们看DiskLruCache的代码
3、利用前面的Snapshot创建一个Entry对象。Entry是Cache的一个内部类,存储的内容是响应的Http数据包Header部分的数据。snapshot.getSource得到的是一个Source对象。
4、利用entry和snapshot得到Response对象,该方法内部会利用前面的Entry和Snapshot得到响应的Http数据包Body(body的获取方式通过snapshot.getSource(ENTRY_BODY)得到)创建一个CacheResponseBody对象;再利用该CacheResponseBody对象和第三步得到的Entry对象构建一个Response的对象,这样该对象就包含了一个网络响应的全部数据了。
5、对request和Response进行比配检查,成功则返回该Response。匹配方法就是url.equals(request.url().toString()) && requestMethod.equals(request.method()) && OkHeaders.varyMatches(response, varyHeaders, request);其中Entry.url和Entry.requestMethod两个值在构建的时候就被初始化好了,初始化值从命中的缓存中获取。因此该匹配方法就是将缓存的请求url和请求方法跟新的客户请求进行对比。最后OkHeaders.varyMatches(response, varyHeaders, request)是检查命中的缓存Http报头跟新的客户请求的Http报头中的键值对是否一样。如果全部结果为真,则返回命中的Response。
在这个方法我们使用了DiskLruCache.get(String)获取DiskLruCache.Snapshot和iskLruCache.Snapshot.getSource(int)方法获取一个Source对象,这里我们先记录下这两个方法,随后在学习DiskLruCache的时候再看。

put()@Cache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private CacheRequest put(Response response) throws IOException {  
  2.     String requestMethod = response.request().method();  
  3.     if (HttpMethod.invalidatesCache(response.request().method())) { //note1  
  4.       remove(response.request());  
  5.       return null;  
  6.     }  
  7.     if (!requestMethod.equals(“GET”)) { //note 2  
  8.       return null;  
  9.     }  
  10.     if (OkHeaders.hasVaryAll(response)) { //note3  
  11.       return null;  
  12.     }  
  13.     Entry entry = new Entry(response); //note4  
  14.     DiskLruCache.Editor editor = null;  
  15.     try {  
  16.       editor = cache.edit(urlToKey(response.request()));//note5  
  17.       if (editor == null) {  
  18.         return null;  
  19.       }  
  20.       entry.writeTo(editor); //note 6  
  21.       return new CacheRequestImpl(editor); //note 7  
  22.     } catch (IOException e) {  
  23.       abortQuietly(editor);  
  24.       return null;  
  25.     }  
  26.   }  
private CacheRequest put(Response response) throws IOException {    String requestMethod = response.request().method();    if (HttpMethod.invalidatesCache(response.request().method())) { //note1      remove(response.request());      return null;    }    if (!requestMethod.equals("GET")) { //note 2      return null;    }    if (OkHeaders.hasVaryAll(response)) { //note3      return null;    }    Entry entry = new Entry(response); //note4    DiskLruCache.Editor editor = null;    try {      editor = cache.edit(urlToKey(response.request()));//note5      if (editor == null) {        return null;      }      entry.writeTo(editor); //note 6      return new CacheRequestImpl(editor); //note 7    } catch (IOException e) {      abortQuietly(editor);      return null;    }  }
1、判断请求如果是”POST”、”PATCH”、”PUT”、”DELETE”、”MOVE”中的任何一个则调用DiskLruCache.remove(urlToKey(request));将这个请求从缓存中移除出去。
2、判断请求如果不是Get则不进行缓存,直接返回null。官方给的解释是缓存get方法得到的Response效率高,其它方法的Response没有缓存效率低。通常通过get方法获取到的数据都是固定不变的的,因此缓存效率自然就高了。其它方法会根据请求报文参数的不同得到不同的Response,因此缓存效率自然而然就低了。
3、判断请求中的http数据包中headers是否有符号”*”的通配符,有则不缓存直接返回null
4、由Response对象构建一个Entry对象
5、通过调用DiskLruCache.edit(urlToKey(response.request()));方法得到一个DiskLruCache.Editor对象。
6、方法内部是通过Okio.buffer(editor.newSink(ENTRY_METADATA));获取到一个BufferedSink对象,随后将Entry中存储的Http报头数据写入到sink流中。
7、构建一个CacheRequestImpl对象,构造器中通过editor.newSink(ENTRY_BODY)方法获得Sink对象。
这里我们使用了DiskLruCache.remove(urlToKey(request))移除请求、DiskLruCache.edit(urlToKey(response.request()));获得一个DiskLruCache.Editor对象,通过Editor获得一个sink流。同样的等下面学习DiskLruCache的时候再详细看该部分的内容。

update()@Cache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private void update(Response cached, Response network) {  
  2.     Entry entry = new Entry(network); //note 1  
  3.     DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; //note2  
  4.     DiskLruCache.Editor editor = null;  
  5.     try {  
  6.       editor = snapshot.edit(); // note 3  
  7.       if (editor != null) {  
  8.         entry.writeTo(editor); //note4  
  9.         editor.commit();  
  10.       }  
  11.     } catch (IOException e) {  
  12.       abortQuietly(editor);  
  13.     }  
  14. }  
private void update(Response cached, Response network) {    Entry entry = new Entry(network); //note 1    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; //note2    DiskLruCache.Editor editor = null;    try {      editor = snapshot.edit(); // note 3      if (editor != null) {        entry.writeTo(editor); //note4        editor.commit();      }    } catch (IOException e) {      abortQuietly(editor);    }}
1、首先利用network即我们刚刚从网络得到的响应,构造一个Entry对象
2、从命中的缓存中获取到DiskLruCache.Snapshot
3、从DiskLruCache.Snapshot获取到DiskLruCache.Editor对象
4、将entry数据写入到前面的editor中
对Cache暂时就介绍到这里,梳理回顾一下在该类中我们都对DiskLruCache哪些方法进行了访问。
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. DiskLruCache.get(String)获取DiskLruCache.Snapshot  
  2. DiskLruCache.remove(String)移除请求  
  3. DiskLruCache.edit(String);获得一个DiskLruCache.Editor对象,  
  4. DiskLruCache.Editor.newSink(int);获得一个sink流  
  5. DiskLruCache.Snapshot.getSource(int);获取一个Source对象。  
  6. DiskLruCache.Snapshot.edit();获得一个DiskLruCache.Editor对象,  
DiskLruCache.get(String)获取DiskLruCache.SnapshotDiskLruCache.remove(String)移除请求DiskLruCache.edit(String);获得一个DiskLruCache.Editor对象,DiskLruCache.Editor.newSink(int);获得一个sink流DiskLruCache.Snapshot.getSource(int);获取一个Source对象。DiskLruCache.Snapshot.edit();获得一个DiskLruCache.Editor对象,
下面我们就来学习一下DiskLruCache中的这些方法。

内部类@DiskLruCache.class


在正式介绍DiskLruCache的上面几个方法之前,我们先来看看DiskLruCache中的几个常用内部类。
Entry内部类是实际的用于存储存储缓存数据的实体,每个url对应一个Entry实体。
Entry.class@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. 该内部类有如下的几个域:  
  2. private final String key;  
  3. /** 实体对应的缓存文件 */  
  4. private final long[] lengths; //文件比特数  
  5. private final File[] cleanFiles;  
  6. private final File[] dirtyFiles;  
  7. /** 实体可读该对象为真*/  
  8. rivate boolean readable;  
  9. /** 实体未被编辑过,则该对象为null*/  
  10. private Editor currentEditor;  
  11. /** 最近像该Entry提交的序列数 */  
  12. private long sequenceNumber;  
该内部类有如下的几个域:private final String key;/** 实体对应的缓存文件 */private final long[] lengths; //文件比特数private final File[] cleanFiles;private final File[] dirtyFiles;/** 实体可读该对象为真*/rivate boolean readable;/** 实体未被编辑过,则该对象为null*/private Editor currentEditor;/** 最近像该Entry提交的序列数 */private long sequenceNumber;
简单的看下其构造器
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private Entry(String key) {  
  2.  this.key = key; //note1  
  3.  lengths = new long[valueCount]; //note2  
  4.  cleanFiles = new File[valueCount];  
  5.  dirtyFiles = new File[valueCount];  
  6.  //note 3  
  7.  StringBuilder fileBuilder = new StringBuilder(key).append(‘.’);  
  8.  int truncateTo = fileBuilder.length();  
  9.  for (int i = 0; i < valueCount; i++) {  
  10.    fileBuilder.append(i);  
  11.    cleanFiles[i] = new File(directory, fileBuilder.toString());  
  12.    fileBuilder.append(”.tmp”);  
  13.    dirtyFiles[i] = new File(directory, fileBuilder.toString());  
  14.    fileBuilder.setLength(truncateTo);  
  15.  }  
     private Entry(String key) {      this.key = key; //note1      lengths = new long[valueCount]; //note2      cleanFiles = new File[valueCount];      dirtyFiles = new File[valueCount];      //note 3      StringBuilder fileBuilder = new StringBuilder(key).append('.');      int truncateTo = fileBuilder.length();      for (int i = 0; i < valueCount; i++) {        fileBuilder.append(i);        cleanFiles[i] = new File(directory, fileBuilder.toString());        fileBuilder.append(".tmp");        dirtyFiles[i] = new File(directory, fileBuilder.toString());        fileBuilder.setLength(truncateTo);      }    }

1、构造器接受一个String key参数,意味着一个url对应一个Entry
2、valueCount在构造DiskLruCache时传入的参数默认大小为2。好奇的童鞋肯定问,为啥非得是2?我们知道在Cache中有如下的定义:
  private static final int ENTRY_METADATA = 0;
  private static final int ENTRY_BODY = 1;
  private static final int ENTRY_COUNT = 2; 这下应该知道为何是2了吧,每个Entry对应两个文件。key.1文件存储的是Response的headers,key,2文件存储的是Response的body
3、创建valueCount个key.i文件,和valueCount个key.i.tmp文件,i的取值为0,1…valueCount

看看其snapshot()方法
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. Snapshot snapshot() {  
  2.   if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();  
  3.   Source[] sources = new Source[valueCount];    
  4.   long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.  
  5.   try {  
  6.     for (int i = 0; i < valueCount; i++) {  
  7.       sources[i] = fileSystem.source(cleanFiles[i]); //note1  
  8.     }  
  9.     return new Snapshot(key, sequenceNumber, sources, lengths);  
  10.   } catch (FileNotFoundException e) {  
  11.     //文件被手动删除,关闭得到的Source  
  12.     for (int i = 0; i < valueCount; i++) {  
  13.       if (sources[i] != null) {  
  14.         Util.closeQuietly(sources[i]);  
  15.       } else {  
  16.         break;  
  17.       }  
  18.     }  
  19.     return null;  
  20.   }  
  21. }  
    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]); //note1        }        return new Snapshot(key, sequenceNumber, sources, lengths);      } catch (FileNotFoundException e) {        //文件被手动删除,关闭得到的Source        for (int i = 0; i < valueCount; i++) {          if (sources[i] != null) {            Util.closeQuietly(sources[i]);          } else {            break;          }        }        return null;      }    }
1、获取cleanFile的Source,用于读取cleanFile中的数据,并用得到的sources、Entry.key、Entry.lengths、sequenceNumber数据构造一个Snapshot对象。
到此为止Entry还有setLengths(String[] strings)、writeLengths(BufferedSink writer)两个方法没有介绍,不过这两个方法比较简单,都是对Entry.lengths进行操作的。前者将string[]和long[]之间进行映射,后者是将long[]写入到一个sink流中。

既然遇到了Snapshot那么我们就看看该对象是个什么玩意儿,从名字来看快照,应该适用于从entry中读取数据的。
Snapshot.class@DiskLruCache.class
首先看看它都有哪些域
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private final String key; //对应的url的md5值  
  2. private final long sequenceNumber; //序列数  
  3. private final Source[] sources; //可以读入数据的流数组,果然存有这么多source当然是利用它来从cleanFile中读取数据了。  
  4. private final long[] lengths; //与上面的流数一一对应  
private final String key; //对应的url的md5值private final long sequenceNumber; //序列数private final Source[] sources; //可以读入数据的流数组,果然存有这么多source当然是利用它来从cleanFile中读取数据了。private final long[] lengths; //与上面的流数一一对应
构造器内容就是对上面这些域进行赋值
该类中的其它都方法都很简单,如getSource(int index)就是等于source[index]所以下面只对edit方法进行介绍。
edit方法
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public Editor edit() throws IOException {  
  2.       return DiskLruCache.this.edit(key, sequenceNumber);  
  3. }  
public Editor edit() throws IOException {      return DiskLruCache.this.edit(key, sequenceNumber);}
该方法内部是调用DiskLruCache的edit方法,不过参数是跟该Snapshot对象关联的key和sequenceNumber。限于篇幅问题,这里就不进入到edit方法内部了,这里大概讲一下它完成的事情。对于各种逻辑判断和异常处理在此不进行描述,只是介绍它正常情况下是如何执行的。核心代码如下:
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. {  
  2.     journalWriter.writeUtf8(DIRTY).writeByte(’ ’).writeUtf8(key).writeByte(‘\n’);  
  3.     journalWriter.flush();  
  4.     if (entry == null) {  
  5.       entry = new Entry(key);  
  6.       lruEntries.put(key, entry);  
  7.     }  
  8.     Editor editor = new Editor(entry);  
  9.     entry.currentEditor = editor;  
  10.     return editor;  
  11. }  
{    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');    journalWriter.flush();    if (entry == null) {      entry = new Entry(key);      lruEntries.put(key, entry);    }    Editor editor = new Editor(entry);    entry.currentEditor = editor;    return editor;}
首先在日志报告中写入DIRTY key这样一行数据,表明该key对应的Entry当前正被编辑中。
随后利用该Entry创建一个Editor对象。我了个乖乖,下面又得瞄一眼Editor类,总感觉没完没了。
Editor.class@DiskLruCache.class
首先按照惯例看看它有什么域
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private final Entry entry;  
  2. private final boolean[] written;  
  3. private boolean hasErrors;  
  4. private boolean committed;  
private final Entry entry;private final boolean[] written;private boolean hasErrors;private boolean committed;
好像看不出啥东西,待老夫看一眼构造器
构造器
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private Editor(Entry entry) {  
  2.       this.entry = entry;  
  3.       this.written = (entry.readable) ? null : new boolean[valueCount];  
  4. }  
private Editor(Entry entry) {      this.entry = entry;      this.written = (entry.readable) ? null : new boolean[valueCount];}
好像也没什么卵用。是时候放出它的几个方法出来镇镇场了。
newSource方法
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public Source newSource(int index) throws IOException {  
  2.     …..   
  3.     return fileSystem.source(entry.cleanFiles[index]);  
  4. }  
public Source newSource(int index) throws IOException {    .....     return fileSystem.source(entry.cleanFiles[index]);}
该方法这么简单??其实还有很多判断语句和异常处理,这里限于篇幅就删掉了。它核心就是return这句。返回指定idnex的cleanFile的读入流
newSink方法
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public Sink newSink(int index) throws IOException {  
  2.         if (!entry.readable) {  
  3.           written[index] = true;  
  4.         }  
  5.         File dirtyFile = entry.dirtyFiles[index];  
  6.         Sink sink;  
  7.         try {  
  8.           sink = fileSystem.sink(dirtyFile);  
  9.         } catch (FileNotFoundException e) {  
  10.           return NULL_SINK;  
  11.         }  
  12.         return new FaultHidingSink(sink) {  
  13.           @Override protected void onException(IOException e) {  
  14.             synchronized (DiskLruCache.this) { hasErrors = true;  }   
  15.           }  
  16.         };  
  17.     }  
public Sink newSink(int index) throws IOException {        if (!entry.readable) {          written[index] = true;        }        File dirtyFile = entry.dirtyFiles[index];        Sink sink;        try {          sink = fileSystem.sink(dirtyFile);        } catch (FileNotFoundException e) {          return NULL_SINK;        }        return new FaultHidingSink(sink) {          @Override protected void onException(IOException e) {            synchronized (DiskLruCache.this) { hasErrors = true;  }           }        };    }
方法也还算简单,首先给Editor的boolean数组written赋值为true表明该位置对应的文件已经被写入新的数据。这里要注意的是写入的文件对象不是cleanFile而是dirtyFiles!
commit方法
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public void commit() throws IOException {  
  2.       synchronized (DiskLruCache.this) {  
  3.         if (hasErrors) {  
  4.           completeEdit(thisfalse);  
  5.           removeEntry(entry); // The previous entry is stale.  
  6.         } else {  
  7.           completeEdit(thistrue);  
  8.         }  
  9.         committed = true;  
  10.       }  
  11. }  
public void commit() throws IOException {      synchronized (DiskLruCache.this) {        if (hasErrors) {          completeEdit(this, false);          removeEntry(entry); // The previous entry is stale.        } else {          completeEdit(this, true);        }        committed = true;      }}
这里执行的工作是提交写入数据,通知DiskLruCache刷新相关数据。Editor还有相关的如abortXX方法等最后都是执行completeEdit(this, ??);成功提交则??等于true否则等于false。这样的提交都什么影响呢?
success情况提交:dirty文件会被更名为clean文件,entry.lengths[i]值会被更新,DiskLruCache,size会更新(DiskLruCache,size代表的是所有整个缓存文件加起来的总大小),redundantOpCount++,在日志中写入一条Clean信息
failed情况:dirty文件被删除,redundantOpCount++,日志中写入一条REMOVE信息
DiskLruCache内部类的基本情况就介绍到这里。下面我们对在Cache中使用的几个方法。
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. DiskLruCache.get(String)获取DiskLruCache.Snapshot  
  2. DiskLruCache.remove(String)移除请求  
  3. DiskLruCache.edit(String);获得一个DiskLruCache.Editor对象,  
  4. DiskLruCache.Editor.newSink(int);获得一个sink流  
  5. DiskLruCache.Snapshot.getSource(int);获取一个Source对象。  
  6. DiskLruCache.Snapshot.edit();获得一个DiskLruCache.Editor对象,  
DiskLruCache.get(String)获取DiskLruCache.SnapshotDiskLruCache.remove(String)移除请求DiskLruCache.edit(String);获得一个DiskLruCache.Editor对象,DiskLruCache.Editor.newSink(int);获得一个sink流DiskLruCache.Snapshot.getSource(int);获取一个Source对象。DiskLruCache.Snapshot.edit();获得一个DiskLruCache.Editor对象,
逐一进行介绍。

DiskLruCache.class

private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true); LinkedHashMap自带Lru算法的光环属性,详情请看LinkedHashMap源码说明
该对象有一个线程池,不过该池最多有一个线程工作,用于清理,维护缓存数据。创建一个DiskLruCache对象的方法是调用该方法,而不是直接调用构造器。
create()@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,  
  2.       int valueCount, long maxSize) {  
  3.     if (maxSize <= 0) {  
  4.       throw new IllegalArgumentException(“maxSize <= 0”);  
  5.     }  
  6.     if (valueCount <= 0) {  
  7.       throw new IllegalArgumentException(“valueCount <= 0”);  
  8.     }  
  9.     // Use a single background thread to evict entries.  
  10.     Executor executor = new ThreadPoolExecutor(01, 60L, TimeUnit.SECONDS,  
  11.         new LinkedBlockingQueue<Runnable>(), Util.threadFactory(“OkHttp DiskLruCache”true)); //创建一个最多容纳一条线程的线程池  
  12.     return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);  
  13.   }  
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);  }
OkHttpClient通过该方法获取到DiskLruCache的一个实例。DiskLruCache的构造器,只能被包内中类调用,因此一般都是通过该方法获取一个DiskLruCache实例。


DiskLruCache()@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. static final String JOURNAL_FILE = “journal”;  
  2. static final String JOURNAL_FILE_TEMP = “journal.tmp”;  
  3. static final String JOURNAL_FILE_BACKUP = “journal.bkp”  
  4. DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize, Executor executor) {  
  5.     this.fileSystem = fileSystem;  
  6.     this.directory = directory;  
  7.     this.appVersion = appVersion;  
  8.     this.journalFile = new File(directory, JOURNAL_FILE);  
  9.     this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);  
  10.     this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);  
  11.     this.valueCount = valueCount;  
  12.     this.maxSize = maxSize;  
  13.     this.executor = executor;  
  14.   }  
static final String JOURNAL_FILE = "journal";static final String JOURNAL_FILE_TEMP = "journal.tmp";static final String JOURNAL_FILE_BACKUP = "journal.bkp"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;  }
该构造器会在指定的目录下创建三个文件,这三个文件是DiskLruCache的工作日志文件。在执行DiskLruCache的任何方法之前都会执行下面的方法完成DiskLruCache的初始化,对于为何不在DiskLruCache的构造器中完成对该方法的调用,目的估计是为了延迟初始化,因为该初始化会创建一系列的文件和对象,所以做延迟初始化处理。 
initialize()@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public synchronized void initialize() throws IOException {  
  2.     assert Thread.holdsLock(this); //note1  
  3.     if (initialized) {  
  4.       return// note2  
  5.     }  
  6.     //note3  
  7.     if (fileSystem.exists(journalFileBackup)) {  
  8.       // If journal file also exists just delete backup file.  
  9.       if (fileSystem.exists(journalFile)) {  
  10.         fileSystem.delete(journalFileBackup);  
  11.       } else {  
  12.         fileSystem.rename(journalFileBackup, journalFile);  
  13.       }  
  14.     }  
  15.     //note4  
  16.     if (fileSystem.exists(journalFile)) {  
  17.       try {  
  18.         readJournal();  
  19.         processJournal();  
  20.         initialized = true;  
  21.         return;  
  22.       } catch (IOException journalIsCorrupt) {  
  23.         Platform.get().logW(”DiskLruCache ” + directory + “ is corrupt: ”  
  24.             + journalIsCorrupt.getMessage() + ”, removing”);  
  25.         delete();  
  26.         closed = false;  
  27.       }  
  28.     }  
  29.     rebuildJournal(); //note5  
  30.     initialized = true//note6  
  31.   }  
public synchronized void initialize() throws IOException {    assert Thread.holdsLock(this); //note1    if (initialized) {      return; // note2    }    //note3    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);      }    }    //note4    if (fileSystem.exists(journalFile)) {      try {        readJournal();        processJournal();        initialized = true;        return;      } catch (IOException journalIsCorrupt) {        Platform.get().logW("DiskLruCache " + directory + " is corrupt: "            + journalIsCorrupt.getMessage() + ", removing");        delete();        closed = false;      }    }    rebuildJournal(); //note5    initialized = true; //note6  }
1、这是个断言语句,当后面的Thread.holdsLock(this)为真,则往下执行否则抛出异常
2、如果之前已经执行过该方法,那么这里就会从这里返回
3、如果有journalFile则删除journalFileBackup,没有journalFile但是有journalFileBackUp则将后者更名为journalFile
4、如果有journalFile文件则对该文件进行处理,分别调用readJournal方法和processJournal()方法;
  • readJournal():
    • BufferedSource source = Okio.buffer(fileSystem.source(journalFile))获取journalFile的读流
    • 对文件中的内容头进行验证判断日志是否被破坏;
    • 调用readJournalLine(source.readUtf8LineStrict())方法;
      • 方法参数是从source中取出一行一行的数据,String的格式类似如下CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 即第一个是操作名,第二个是对url进行md5编码后得到的key,后面则针对操作不同有不同的值,具体内容就是该Entry对应的缓存文件大小(bytes)。
      • 方法对读取到的String进行解析,通过解析结果对LruEntries进行初始化.所以系统重启,通过日志文件可以恢复上次缓存的数据。
      • 对每次解析的非REMOVE信息,利用该数据的key创建一个Entry;如果判断信息为CLEAN则设置entry.readable = true;表明该entry可读,设置entry.currentEditor = null表明当前Entry不是处于可编辑状态,调用entry.setLengths(String[]),设置该entry.lengths的初始值。如果判断为Dirty则设置entry.currentEditor = new Editor(entry);表明当前Entry处于被编辑状态。
    • 随后记录redundantOpCount的值,该值的含义就是判断当前日志中记录的行数与lruEntries集合容量的差值。即日志中多出来的”冗余”记录
  • processJournal():
    • 删除存在的journalFileTmp文件
    • lruEntries中的Entry数据处理:如果entry.currentEditor != null则表明上次异常关闭,因此该Entry的数据是脏的,不能读,进而删除该Entry下的缓存文件,将该Entry从lruEntries中移出;如果entry.currentEditor == null证明该Entry下的缓存文件可用,记录它所有缓存文件中存储的缓存数。结果赋值给size
5、如果没有journalFile文件则调用rebuildJournal()方法创建一个journalFile文件。
6、initialize()当退出这个方法无论何种情况最终initialized值都将变成true,该值将不会再被设置为false,除非DiskLruCache对象被销毁。这表明initialize()方法在DiskLruCache对象的整个生命周期中只会被执行一次,该动作完成日志文件的写入和LruEntries集合的初始化。

下面我们看看方法rebuildJournal();是如何工作的。
rebuildJournal()@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private synchronized void rebuildJournal() throws IOException {  
  2.     if (journalWriter != null) { //note1  
  3.       journalWriter.close();    
  4.     }  
  5.     BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp)); //note2  
  6.     try {  
  7.       //note3  
  8.       writer.writeUtf8(MAGIC).writeByte(’\n’);  
  9.       writer.writeUtf8(VERSION_1).writeByte(’\n’);  
  10.       writer.writeDecimalLong(appVersion).writeByte(’\n’);  
  11.       writer.writeDecimalLong(valueCount).writeByte(’\n’);  
  12.       writer.writeByte(’\n’);  
  13.      //note4  
  14.       for (Entry entry : lruEntries.values()) {  
  15.         if (entry.currentEditor != null) {  
  16.           writer.writeUtf8(DIRTY).writeByte(’ ’);  
  17.           writer.writeUtf8(entry.key);  
  18.           writer.writeByte(’\n’);  
  19.         } else {  
  20.           writer.writeUtf8(CLEAN).writeByte(’ ’);  
  21.           writer.writeUtf8(entry.key);  
  22.           entry.writeLengths(writer);  
  23.           writer.writeByte(’\n’);  
  24.         }  
  25.       }  
  26.     } finally {  
  27.       writer.close();  
  28.     }  
  29.    //note 5  
  30.     if (fileSystem.exists(journalFile)) {  
  31.       fileSystem.rename(journalFile, journalFileBackup);  
  32.     }  
  33.     fileSystem.rename(journalFileTmp, journalFile);  
  34.     fileSystem.delete(journalFileBackup);  
  35.     journalWriter = newJournalWriter();  
  36.     hasJournalErrors = false;  
  37.   }  
private synchronized void rebuildJournal() throws IOException {    if (journalWriter != null) { //note1      journalWriter.close();      }    BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp)); //note2    try {      //note3      writer.writeUtf8(MAGIC).writeByte('\n');      writer.writeUtf8(VERSION_1).writeByte('\n');      writer.writeDecimalLong(appVersion).writeByte('\n');      writer.writeDecimalLong(valueCount).writeByte('\n');      writer.writeByte('\n');     //note4      for (Entry entry : lruEntries.values()) {        if (entry.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();    }   //note 5    if (fileSystem.exists(journalFile)) {      fileSystem.rename(journalFile, journalFileBackup);    }    fileSystem.rename(journalFileTmp, journalFile);    fileSystem.delete(journalFileBackup);    journalWriter = newJournalWriter();    hasJournalErrors = false;  }
1、对于journalWriter我们只需要知道它是一个跟journalFile绑定的BufferedSink对象即可
2、获取对journalFileTmp文件的Sink流并对该流用buffer进行包装,提高I/O写入效率
3、写入日志头
4、将lruEntries集合中的Entry对象写入到文件中;根据Entry的currentEditor值判断是CLEN还是DIRTY,随后写入该Entry的key,如果是CLEN还会写入该Entry的每个缓存文件的大小bytes
5、这一段代码就是把前面的journalFileTmp更名为journalFile, 然后journalWriter跟该文件绑定,通过它来向journalWriter写入数据,设置hasJournalErrors = false;

上面我们把initialize()方法解析完了,终于可以看看之前一直提到的下列方法了
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. DiskLruCache.get(String)获取DiskLruCache.Snapshot  
  2. DiskLruCache.remove(String)移除请求  
  3. DiskLruCache.edit(String);获得一个DiskLruCache.Editor对象,  
  4. DiskLruCache.Editor.newSink(int);获得一个sink流  
  5. DiskLruCache.Snapshot.getSource(int);获取一个Source对象。  
  6. DiskLruCache.Snapshot.edit();获得一个DiskLruCache.Editor对象,  
DiskLruCache.get(String)获取DiskLruCache.SnapshotDiskLruCache.remove(String)移除请求DiskLruCache.edit(String);获得一个DiskLruCache.Editor对象,DiskLruCache.Editor.newSink(int);获得一个sink流DiskLruCache.Snapshot.getSource(int);获取一个Source对象。DiskLruCache.Snapshot.edit();获得一个DiskLruCache.Editor对象,

get(key)@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public synchronized Snapshot get(String key) throws IOException {  
  2.     initialize(); // note1  
  3.     checkNotClosed(); //note2  
  4.     validateKey(key); //note3  
  5.     Entry entry = lruEntries.get(key);  
  6.     if (entry == null || !entry.readable) return null;  
  7.     Snapshot snapshot = entry.snapshot(); //note 4  
  8.     if (snapshot == nullreturn null;  
  9.     redundantOpCount++;  
  10.     journalWriter.writeUtf8(READ).writeByte(’ ’).writeUtf8(key).writeByte(‘\n’); //note3  
  11.     if (journalRebuildRequired()) { //note4  
  12.       executor.execute(cleanupRunnable);    
  13.     }  
  14.     return snapshot;  
  15.   }  
public synchronized Snapshot get(String key) throws IOException {    initialize(); // note1    checkNotClosed(); //note2    validateKey(key); //note3    Entry entry = lruEntries.get(key);    if (entry == null || !entry.readable) return null;    Snapshot snapshot = entry.snapshot(); //note 4    if (snapshot == null) return null;    redundantOpCount++;    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n'); //note3    if (journalRebuildRequired()) { //note4      executor.execute(cleanupRunnable);      }    return snapshot;  }
1、完成初始化工作,这部分之前已经讲过就不再说了。
2、该方法其实是对closed进行判断,如果值为真抛出异常,为假继续执行。
3、判断key是否有效,Pattern规则是 Pattern.compile(“[a-z0-9_-]{1,120}”);
4、获取entry.snapshot()
5、向日志文件中写入读取日志
4、redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();简单说就是当前redundantOpCount值大于2000,而且该值大于等于存储的缓存键值对集合的容量。目的是判断日志中的数据是不是太多了?太多则开启线程执行清理工作

先来分析一下它是如何维护缓存数据的,先找到类中的cleanupRunnable对象,查看其run方法得知,其主要调用了trimToSize()rebuildJournal()两个方法对缓存数据进行维护的。
trimToSize()@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private void trimToSize() throws IOException {  
  2.     while (size > maxSize) {  
  3.       Entry toEvict = lruEntries.values().iterator().next();  
  4.       removeEntry(toEvict);  
  5.     }  
  6.     mostRecentTrimFailed = false;  
  7. }  
private void trimToSize() throws IOException {    while (size > maxSize) {      Entry toEvict = lruEntries.values().iterator().next();      removeEntry(toEvict);    }    mostRecentTrimFailed = false;}
方法逻辑很简单,如果lruEntries的容量大于门限,则把lruEntries中第一个Entry移出集合,一直循环该操作,直到lruEntries的容量小于门限。 maxSize是在创建Cache是得到的。rebuildJournal()方法前面已经讲过了这里就不讲了。

remove(String)@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public synchronized boolean remove(String key) throws IOException {  
  2.     initialize();  
  3.     checkNotClosed();  
  4.     validateKey(key);  
  5.     Entry entry = lruEntries.get(key);  
  6.     if (entry == nullreturn false;  
  7.     boolean removed = removeEntry(entry); //note1  
  8.     if (removed && size <= maxSize) mostRecentTrimFailed = false;  
  9.     return removed;  
  10. }  
public synchronized boolean remove(String key) throws IOException {    initialize();    checkNotClosed();    validateKey(key);    Entry entry = lruEntries.get(key);    if (entry == null) return false;    boolean removed = removeEntry(entry); //note1    if (removed && size <= maxSize) mostRecentTrimFailed = false;    return removed;}
该方法大部分内容之前已经讲解过了,这里只对其中调用的removeEntry(entry)方法进行下说明
removeEntry()@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private boolean removeEntry(Entry entry) throws IOException {  
  2.     if (entry.currentEditor != null) { //note1  
  3.       entry.currentEditor.hasErrors = true// Prevent the edit from completing normally.  
  4.     }  
  5.    //note2  
  6.     for (int i = 0; i < valueCount; i++) {  
  7.       fileSystem.delete(entry.cleanFiles[i]);  
  8.       size -= entry.lengths[i];  
  9.       entry.lengths[i] = 0;  
  10.     }  
  11.    //note3  
  12.     redundantOpCount++;  
  13.     journalWriter.writeUtf8(REMOVE).writeByte(’ ’).writeUtf8(entry.key).writeByte(‘\n’);  
  14.     lruEntries.remove(entry.key);  
  15.     if (journalRebuildRequired()) {  
  16.       executor.execute(cleanupRunnable);  
  17.     }  
  18.     return true;  
  19. }  
private boolean removeEntry(Entry entry) throws IOException {    if (entry.currentEditor != null) { //note1      entry.currentEditor.hasErrors = true; // Prevent the edit from completing normally.    }   //note2    for (int i = 0; i < valueCount; i++) {      fileSystem.delete(entry.cleanFiles[i]);      size -= entry.lengths[i];      entry.lengths[i] = 0;    }   //note3    redundantOpCount++;    journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');    lruEntries.remove(entry.key);    if (journalRebuildRequired()) {      executor.execute(cleanupRunnable);    }    return true;}
1、设置该entry对应的editor告诉它我就要挂了,你可以下班了
2、删除entry中的cleanFiles,不过为啥不删除dirty文件呢?然后改变DiskLruCach.size的大小
3、向日志中写入一条REMOVE消息
4、检查是否有必要维护一下缓存数据。 

edit()@DiskLruCache.class
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {  
  2.     initialize();  
  3.     checkNotClosed();  
  4.     validateKey(key);  
  5.     Entry entry = lruEntries.get(key); //note1  
  6.     ……  
  7.     journalWriter.writeUtf8(DIRTY).writeByte(’ ’).writeUtf8(key).writeByte(‘\n’); //note2  
  8.     journalWriter.flush();  
  9.     if (hasJournalErrors) {  
  10.       return null// Don’t edit; the journal can’t be written.  
  11.     }  
  12.     if (entry == null) {  
  13.       entry = new Entry(key);  
  14.       lruEntries.put(key, entry);  
  15.     }  
  16.     Editor editor = new Editor(entry); //note3  
  17.     entry.currentEditor = editor;  
  18.     return editor;  
  19.   }  
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {    initialize();    checkNotClosed();    validateKey(key);    Entry entry = lruEntries.get(key); //note1    ......    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n'); //note2    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); //note3    entry.currentEditor = editor;    return editor;  }
1、根据key获取到entry
2、写日志,诶跟雷锋一样啊,做一件事都得写个日志
3、创建Editor

至此我们对okhttp的缓存机制理解的差不多了,下面我们对上面的分析做一下小节:
构建一个Cache时需要我们指定一个缓存文件的存放目录,缓存文件的最大值(单位byte)。
DiskLruCache有一个线程池,该线程池最多只有一条线程执行,执行的任务也简单,主要完成两个任务,其一移除lruEntries集合中多余的Entry,使其小于maxSize,并删除相关的缓存文件;其二如有必要重建工作日志。
DiskLruCache的lruEntries采用LinkedHashMap实现,该集合自带Lru光环属性,无需任何额外编程,集合内部采用lru算法实现。
DiskLruCache会在缓存目录下创建日志文件,用于对每次的获取、删除、编辑等操作都会进行相应的记录,该日志也用于应用重启后恢复缓存信息,初始化lruEntries缓存集合。
DiskLruCache具体的缓存信息存放对象是DiskLruCache.Entry.class,该对象存放valueCount个文件的引用,默认是两个分别存储Response的headers和body,一个url对应一个Entry对象,对于Snapshot和Editor都是从Entry获取到的,Snapshot主要是读取Entry内容,Editor主要是向Entry写入数据。Entry对象引用的文件其命名格式为key.i。

对于okhttp的Cache的理解暂时就到这里了。下一节会对okhttp的最后一个内容okio进行深入的学习,详情请看《OkHttp深入学习(四)——0kio》




0 0
原创粉丝点击