美文网首页
OkHttp讲解(三)

OkHttp讲解(三)

作者: 涛涛123759 | 来源:发表于2020-06-12 16:23 被阅读0次

OkHttp讲解(一)
OkHttp讲解(二)
OkHttp讲解(三)

一、HTTP缓存机制

1.1、分类

1 强制缓存
已存在缓存数据时,仅基于强制缓存,请求数据流程如下:



2 对比缓存
已存在缓存数据时,仅基于对比缓存,请求数据的流程如下:


1.2、HTTP报文

HTTP报文就是客户端和服务器之间通信时发送及其响应的数据块。客户端向服务器请求数据,发送请求(request)报文;服务器向客户端下发返回数据,返回响应(response)报文,报文信息主要分为两部分

  • 1 包含属性的头部(header)-------------附加信息(cookie,缓存信息等),与缓存相关的规则信息,均包含在header中
  • 2 包含数据的主体部分(body)--------------HTTP请求真正想要传输的部分

二、CacheStrategy 缓存策略类

根据输出的networkRequest和cacheResponse的值是否为null给出不同的策略,如下:



CacheStrategy使用Factory模式进行构造,通过Factory的get()方法获取CacheStrategy的对象,参数如下:

  public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        //获取cacheReposne中的header中值
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

    public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

      return candidate;
    }

    public CacheStrategy get() {
      //获取当前的缓存策略
      CacheStrategy candidate = getCandidate();
     //如果是网络请求不为null并且请求里面的cacheControl是只用缓存
      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        //使用只用缓存的策略
        return new CacheStrategy(null, null);
      }
      return candidate;
    }

    private CacheStrategy getCandidate() {
      // No cached response.
      //如果没有缓存响应,返回一个没有响应的策略
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }
       //如果是https,丢失了握手,返回一个没有响应的策略
      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }
     
      // 响应不能被缓存
      // If this response shouldn't have been stored, it should never be used
      // as a response source. This check should be redundant as long as the
      // persistence store is well-behaved and the rules are constant.
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }
     
      //获取请求头里面的CacheControl
      CacheControl requestCaching = request.cacheControl();
      //如果请求里面设置了不缓存,则不缓存
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
      //获取响应的年龄
      long ageMillis = cacheResponseAge();
      //获取上次响应刷新的时间
      long freshMillis = computeFreshnessLifetime();
      //如果请求里面有最大持久时间要求,则两者选择最短时间的要求
      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      long minFreshMillis = 0;
      //如果请求里面有最小刷新时间的限制
      if (requestCaching.minFreshSeconds() != -1) {
         //用请求中的最小更新时间来更新最小时间限制
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }
      //最大验证时间
      long maxStaleMillis = 0;
      //响应缓存控制器
      CacheControl responseCaching = cacheResponse.cacheControl();
      //如果响应(服务器)那边不是必须验证并且存在最大验证秒数
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        //更新最大验证时间
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
     //响应支持缓存
       //持续时间+最短刷新时间<上次刷新时间+最大验证时间 则可以缓存
      //现在时间(now)-已经过去的时间(sent)+可以存活的时间<最大存活时间(max-age)
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
       //缓存响应
        return new CacheStrategy(null, builder.build());
      }
    
      //如果想缓存request,必须要满足一定的条件
      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      String conditionName;
      String conditionValue;
      if (etag != null) {
        conditionName = "If-None-Match";
        conditionValue = etag;
      } else if (lastModified != null) {
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
      } else if (servedDate != null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
      } else {
        //没有条件则返回一个定期的request
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }
      
      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      //返回有条件的缓存request策略
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

三、Cache.java类

  final DiskLruCache cache;
  int writeSuccessCount;
  int writeAbortCount;
  private int networkCount;
  private int hitCount;
  private int requestCount;
  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);
  }
  • 1、Cache对象拥有一个DiskLruCache引用。
  • 2、Cache构造器接受两个参数,意味着如果我们想要创建一个缓存必须指定缓存文件存储的目录和缓存文件的最大值

(1) ”增“操作——put()方法

CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    //判断请求如果是"POST"、"PATCH"、"PUT"、"DELETE"、"MOVE"中的任何一个则调用DiskLruCache.remove(urlToKey(request));将这个请求从缓存中移除出去。
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    //判断请求如果不是Get则不进行缓存,直接返回null。官方给的解释是缓存get方法得到的Response效率高,其它方法的Response没有缓存效率低。通常通过get方法获取到的数据都是固定不变的的,因此缓存效率自然就高了。其它方法会根据请求报文参数的不同得到不同的Response,因此缓存效率自然而然就低了。
    if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }
     //判断请求中的http数据包中headers是否有符号"*"的通配符,有则不缓存直接返回null
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    //由Response对象构建一个Entry对象,Entry是Cache的一个内部类
    Entry entry = new Entry(response);
    //通过调用DiskLruCache.edit();方法得到一个DiskLruCache.Editor对象。
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      //把这个entry写入
      //方法内部是通过Okio.buffer(editor.newSink(ENTRY_METADATA));获取到一个BufferedSink对象,随后将Entry中存储的Http报头数据写入到sink流中。
      entry.writeTo(editor);
      //构建一个CacheRequestImpl对象,构造器中通过editor.newSink(ENTRY_BODY)方法获得Sink对象
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

(2) ”删“操作——remove()方法

  void remove(Request request) throws IOException {
    cache.remove(key(request.url()));
  }
 //key()这个方法原来就说获取url的MD5和hex生成的key
  public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
  }

(3) ”改“操作——update()方法

void update(Response cached, Response network) {
    //用response构造一个Entry对象
    Entry entry = new Entry(network);
    //从命中缓存中获取到的DiskLruCache.Snapshot
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    //从DiskLruCache.Snapshot获取DiskLruCache.Editor()对象
    DiskLruCache.Editor editor = null;
    try {
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        //将entry写入editor中
        entry.writeTo(editor);
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }

(4) ”查“操作——get()方法

 Response get(Request request) {
    //获取url经过MD5和HEX的key
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
     //根据key来获取一个snapshot,由此可知我们的key-value里面的value对应的是snapshot
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }
    //利用前面的Snapshot创建一个Entry对象。存储的内容是响应的Http数据包Header部分的数据。snapshot.getSource得到的是一个Source对象 (source是okio里面的一个接口)
    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

    //利用entry和snapshot得到Response对象,该方法内部会利用前面的Entry和Snapshot得到响应的Http数据包Body(body的获取方式通过snapshot.getSource(ENTRY_BODY)得到)创建一个CacheResponseBody对象;再利用该CacheResponseBody对象和第三步得到的Entry对象构建一个Response的对象,这样该对象就包含了一个网络响应的全部数据了。
    Response response = entry.response(snapshot);
    //对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。
    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

四、 DiskLruCache

1、Entry.class(DiskLruCache的内部类)

Entry内部类是实际用于存储的缓存数据的实体类,每一个url对应一个Entry实体

    final String key;
    /** 实体对应的缓存文件 */ 
    /** Lengths of this entry's files. */
    final long[] lengths; //文件比特数 
    final File[] cleanFiles;
    final File[] dirtyFiles;
    /** 实体是否可读,可读为true,不可读为false*/  
    /** True if this entry has ever been published. */
    boolean readable;

     /** 编辑器,如果实体没有被编辑过,则为null*/  
    /** The ongoing edit or null if this entry is not being edited. */
    Editor currentEditor;
    /** 最近提交的Entry的序列号 */  
    /** The sequence number of the most recently committed edit to this entry. */
    long sequenceNumber;
    //构造器 就一个入参 key,而key又是url,所以,一个url对应一个Entry
    Entry(String key) {
     
      this.key = key;
      //valueCount在构造DiskLruCache时传入的参数默认大小为2
      //具体请看Cache类的构造函数,里面通过DiskLruCache.create()方法创建了DiskLruCache,并且传入一个值为2的ENTRY_COUNT常量
      lengths = new long[valueCount];
      cleanFiles = new File[valueCount];
      dirtyFiles = new File[valueCount];

      // The names are repetitive so re-use the same builder to avoid allocations.
      StringBuilder fileBuilder = new StringBuilder(key).append('.');
      int truncateTo = fileBuilder.length();
      //由于valueCount为2,所以循环了2次,一共创建了4份文件
      //分别为key.1文件和key.1.tmp文件
      //           key.2文件和key.2.tmp文件
      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);
      }
    }

通过上述代码咱们知道了,一个url对应一个Entry对象,同时,每个Entry对应两个文件,key.1存储的是Response的headers,key.2文件存储的是Response的body

2、Snapshot (DiskLruCache的内部类)
  public final class Snapshot implements Closeable {
    private final String key;  //也有一个key
    private final long sequenceNumber; //序列号
    private final Source[] sources; //可以读入数据的流   这么多的流主要是从cleanFile中读取数据
    private final long[] lengths; //与上面的流一一对应  

    //构造器就是对上面这些属性进行赋值
    Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
      this.key = key;
      this.sequenceNumber = sequenceNumber;
      this.sources = sources;
      this.lengths = lengths;
    }

    public String key() {
      return key;
    }
   //edit方法主要就是调用DiskLruCache的edit方法了,入参是该Snapshot对象的两个属性key和sequenceNumber.
    public Editor edit() throws IOException {
      return DiskLruCache.this.edit(key, sequenceNumber);
    }
    public Source getSource(int index) {
      return sources[index];
    }

    public long getLength(int index) {
      return lengths[index];
    }
    public void close() {
      for (Source in : sources) {
        Util.closeQuietly(in);
      }
    }
  }

这时候再回来看下Entry里面的snapshot()方法

    Snapshot snapshot() {
      //首先判断 线程是否有DiskLruCache对象的锁
      if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
      //new了一个Souce类型数组,容量为2
      Source[] sources = new Source[valueCount];
      //clone一个long类型的数组,容量为2
      long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
       //获取cleanFile的Source,用于读取cleanFile中的数据,并用得到的souce、Entry.key、Entry.length、sequenceNumber数据构造一个Snapshot对象
      try {
        for (int i = 0; i < valueCount; i++) {
          sources[i] = fileSystem.source(cleanFiles[i]);
        }
        return new Snapshot(key, sequenceNumber, sources, lengths);
      } catch (FileNotFoundException e) {
        // A file must have been deleted manually!
        for (int i = 0; i < valueCount; i++) {
          if (sources[i] != null) {
            Util.closeQuietly(sources[i]);
          } else {
            break;
          }
        }
        try {
          removeEntry(this);
        } catch (IOException ignored) {
        }
        return null;
      }
    }

由上面代码可知Spapshot里面的key,sequenceNumber,sources,lenths都是一个entry,其实也就可以说一个Entry对象一一对应一个Snapshot对象

3、Editor.class(DiskLruCache的内部类)
  public final class Editor {
    final Entry entry;
    final boolean[] written;
    private boolean done;
    Editor(Entry entry) {
      this.entry = entry;
      this.written = (entry.readable) ? null : new boolean[valueCount];
    }
    /**
     *这里说一下detach方法,当编辑器(Editor)处于io操作的error的时候,或者editor正在被调用的时候而被清
     *除的,为了防止编辑器可以正常的完成。我们需要删除编辑器创建的文件,并防止创建新的文件。如果编
     *辑器被分离,其他的编辑器可以编辑这个Entry
     */
    void detach() {
      if (entry.currentEditor == this) {
        for (int i = 0; i < valueCount; i++) {
          try {
            fileSystem.delete(entry.dirtyFiles[i]);
          } catch (IOException e) {
            // This file is potentially leaked. Not much we can do about that.
          }
        }
        entry.currentEditor = null;
      }
    }

    /**
     * 获取cleanFile的输入流 在commit的时候把done设为true
     */
    public Source newSource(int index) {
      synchronized (DiskLruCache.this) {
       //如果已经commit了,不能读取了
        if (done) {
          throw new IllegalStateException();
        }
        //如果entry不可读,并且已经有编辑器了(其实就是dirty)
        if (!entry.readable || entry.currentEditor != this) {
          return null;
        }
        try {
         //通过filesystem获取cleanFile的输入流
          return fileSystem.source(entry.cleanFiles[index]);
        } catch (FileNotFoundException e) {
          return null;
        }
      }
    }

    /**
    * 获取dirty文件的输出流,如果在写入数据的时候出现错误,会立即停止。返回的输出流不会抛IO异常
     */
    public Sink newSink(int index) {
      synchronized (DiskLruCache.this) {
       //已经提交,不能操作
        if (done) {
          throw new IllegalStateException();
        }
       //如果编辑器是不自己的,不能操作
        if (entry.currentEditor != this) {
          return Okio.blackhole();
        }
       //如果entry不可读,把对应的written设为true
        if (!entry.readable) {
          written[index] = true;
        }
         //如果文件
        File dirtyFile = entry.dirtyFiles[index];
        Sink sink;
        try {
          //如果fileSystem获取文件的输出流
          sink = fileSystem.sink(dirtyFile);
        } catch (FileNotFoundException e) {
          return Okio.blackhole();
        }
        return new FaultHidingSink(sink) {
          @Override protected void onException(IOException e) {
            synchronized (DiskLruCache.this) {
              detach();
            }
          }
        };
      }
    }

    /**
     * 写好数据,一定不要忘记commit操作对数据进行提交,我们要把dirtyFiles里面的内容移动到cleanFiles里才能够让别的editor访问到
     */
    public void commit() throws IOException {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor == this) {
          completeEdit(this, true);
        }
        done = true;
      }
    }
    public void abort() throws IOException {
      synchronized (DiskLruCache.this) {
        if (done) {
          throw new IllegalStateException();
        }
        if (entry.currentEditor == this) {
         //这个方法是DiskLruCache的方法在后面讲解
          completeEdit(this, false);
        }
        done = true;
      }
    }

    public void abortUnlessCommitted() {
      synchronized (DiskLruCache.this) {
        if (!done && entry.currentEditor == this) {
          try {
            completeEdit(this, false);
          } catch (IOException ignored) {
          }
        }
      }
    }
  }
  • abort()和abortUnlessCommitted()最后都会执行completeEdit(Editor, boolean) 这个方法这里简单说下:
  • success情况提交:dirty文件会被更名为clean文件,entry.lengths[i]值会被更新,DiskLruCache,size会更新(DiskLruCache,size代表的是所有整个缓存文件加起来的总大小),redundantOpCount++,在日志中写入一条Clean信息
  • failed情况:dirty文件被删除,redundantOpCount++,日志中写入一条REMOVE信息

相关文章

网友评论

      本文标题:OkHttp讲解(三)

      本文链接:https://www.haomeiwen.com/subject/npzgtktx.html