精华内容
下载资源
问答
  • okHttp缓存机制

    2018-01-08 16:54:12
    先大概看一下缓存的实现机制再梳理一下流程 DiskLruCache实现方案 /* * This cache uses a journal file named "journal". A typical journal file * looks like this: * libcore.io.DiskLruCache * 1...

    基本流程

    CacheInterceptor,主要的步骤已经在下面代码中注释了出来,最关键的就是第二步,单独分析一下这一步

    @Override public Response intercept(Chain chain) throws IOException {
      // 1、get cache
      Response cacheCandidate = cache != null
          ? cache.get(chain.request())
          : null;
    
      long now = System.currentTimeMillis();
    
      // 2、根据cache头获取缓存策略,ref:https://tools.ietf.org/html/rfc7234
      CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
      Request networkRequest = strategy.networkRequest;
      Response cacheResponse = strategy.cacheResponse;
    
      if (cache != null) {
        cache.trackResponse(strategy);
      }
    
      // 清理cache
      if (cacheCandidate != null && cacheResponse == null) {
        closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
      }
    
      // 3、策略中的request和response都是空,是异常情况,构造504 Gateway Timeout错误
      // If we're forbidden from using the network and the cache is insufficient, fail.
      if (networkRequest == null && cacheResponse == null) {
        return new Response.Builder()
            .request(chain.request())
            .protocol(Protocol.HTTP_1_1)
            .code(504)
            .message("Unsatisfiable Request (only-if-cached)")
            .body(Util.EMPTY_RESPONSE)
            .sentRequestAtMillis(-1L)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
      }
    
      // 4、不需要进行请求,直接返回缓存
      // If we don't need the network, we're done.
      if (networkRequest == null) {
        return cacheResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse))
            .build();
      }
    
      // 5、发起真正的请求
      Response networkResponse = null;
      try {
        networkResponse = chain.proceed(networkRequest);
      } finally {
        // If we're crashing on I/O or otherwise, don't leak the cache body.
        if (networkResponse == null && cacheCandidate != null) {
          closeQuietly(cacheCandidate.body());
        }
      }
    
    // 6、网络请求返回304 not-modified,直接返回并更新缓存
      // If we have a cache response too, then we're doing a conditional get.
      if (cacheResponse != null) {
        if (networkResponse.code() == HTTP_NOT_MODIFIED) {
          Response response = cacheResponse.newBuilder()
              .headers(combine(cacheResponse.headers(), networkResponse.headers()))
              .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
              .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
              .cacheResponse(stripBody(cacheResponse))
              .networkResponse(stripBody(networkResponse))
              .build();
          networkResponse.body().close();
    
          // Update the cache after combining headers but before stripping the
          // Content-Encoding header (as performed by initContentStream()).
          cache.trackConditionalCacheHit();
          cache.update(cacheResponse, response);
          return response;
        } else {
          closeQuietly(cacheResponse.body());
        }
      }
    
      // 7、构造response
      Response response = networkResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .networkResponse(stripBody(networkResponse))
          .build();
    
      // 8、更新缓存
      if (cache != null) {
        if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
          // Offer this request to the cache.
          CacheRequest cacheRequest = cache.put(response);
          // 9、返回真正的response
          return cacheWritingResponse(cacheRequest, response);
        }
    
        if (HttpMethod.invalidatesCache(networkRequest.method())) {
          try {
            cache.remove(networkRequest);
          } catch (IOException ignored) {
            // The cache cannot be written.
          }
        }
      }
    
      return response;
    }

    缓存策略

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    
    /**
     * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
     */
    public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();
    
      // 疑问,这里为什么的把cache response也置为null返回
      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;
    }
    
    /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      // No cached response.
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }
    
      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }
    
      // 根据response code和no-store判断是否允许缓存
      // 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);
      }
    
      // requst不需要使用cache,或者没有If-Modified-Since/If-None-Match无法使用缓存
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
    
      // 下面这一段,计算缓存是否在有效期内,ref:https://tools.ietf.org/html/rfc7234
      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());
      }
    
      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());
      }
    
      // 有缓存同时需要再发起请求的,在请求头中添加If-Modified-Since/If-None-Match
      // 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 {
        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();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

    构造缓存策略,就是根据http协议头的规定来决定是否使用缓存,可以参考https://tools.ietf.org/html/rfc7234。这里还有一个疑问,在candidate.networkRequest != null && request.cacheControl().onlyIfCached()时候把cache response置空然后返回了,不理解为什么这么做。

    缓存类的实现

    缓存实现类主要有Cache和DiskLruCache。Cache相当于一个wrapper,主要的实现还是在DiskLruCache。先大概看一下缓存的实现机制再梳理一下流程

    DiskLruCache实现方案

    /*
     * This cache uses a journal file named "journal". A typical journal file
     * looks like this:
     *     libcore.io.DiskLruCache
     *     1
     *     100
     *     2
     *
     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
     *     READ 335c4c6028171cfddfbaae1a9c313c52
     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
     *
     * The first five lines of the journal form its header. They are the
     * constant string "libcore.io.DiskLruCache", the disk cache's version,
     * the application's version, the value count, and a blank line.
     *
     * Each of the subsequent lines in the file is a record of the state of a
     * cache entry. Each line contains space-separated values: a state, a key,
     * and optional state-specific values.
     *   o DIRTY lines track that an entry is actively being created or updated.
     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
     *     temporary files may need to be deleted.
     *   o CLEAN lines track a cache entry that has been successfully published
     *     and may be read. A publish line is followed by the lengths of each of
     *     its values.
     *   o READ lines track accesses for LRU.
     *   o REMOVE lines track entries that have been deleted.
     *
     * The journal file is appended to as cache operations occur. The journal may
     * occasionally be compacted by dropping redundant lines. A temporary file named
     * "journal.tmp" will be used during compaction; that file should be deleted if
     * it exists when the cache is opened.
     */

    这是DiskLruCache的注释。首先有一个记录文件journal,记录了每个缓存每次的==状态变化==,每个缓存的状态和大小;状态分为4中:DIRTY - 临时缓存,通常后续会变为CLEAN/READ状态,在初始化读取文件时,不会读取到内存中;CLEAN - 可用的缓存;READ - 缓存已经被读取过;REMOVE - 缓存需要删除。也就是说,对一个缓存会有多条记录,每条记录对应了一次状态的变更。

    保存缓存DIRTY(这时候只生成了一条记录和一个记录response head的文件) -> 调用方通过read方法读取response,在返回response的同时,生成记录response body的文件 -> 下次请求读取了该缓存,记录状态改为READ;过程中有异常或者缓存的size超过了max size,状态改为REMOVE

    缓存的存取流程

    初始化

    在put和get的过程中,如果还没有进行过初始化,就会进行初始化。主要的方法是readJournal和processJournal。

    流程就是遍历journal文件的内容,最终的结果,如果是CLEAN,就会在LinkedHashMap里生成一个节点

    private void readJournal() throws IOException {
      BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
      try {
        ...
        int lineCount = 0;
        while (true) {
          try {
            // 读取每一行并处理
            readJournalLine(source.readUtf8LineStrict());
            lineCount++;
          } catch (EOFException endOfJournal) {
            break;
          }
        }
        ...
    }
    
    private void readJournalLine(String line) throws IOException {
      ...
      // 如果是REMOVE的话,在链表中删除该节点
      if (secondSpace == -1) {
        key = line.substring(keyBegin);
        if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
          lruEntries.remove(key);
          return;
        }
      } else {
        key = line.substring(keyBegin, secondSpace);
      }
    
      // 如果不是REMOVE,先把该节点加入链表,后续会进一步处理
      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);
      }
    }
    
    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) {
          // 如果最终状态是CLEAN或者READ,对缓存大小进行累加
          for (int t = 0; t < valueCount; t++) {
            size += entry.lengths[t];
          }
        } else {
          // 如果状态是DIRTY(在readJournalLine中赋值的),就在链表中删除该节点
          entry.currentEditor = null;
          for (int t = 0; t < valueCount; t++) {
            fileSystem.delete(entry.cleanFiles[t]);
            fileSystem.delete(entry.dirtyFiles[t]);
          }
          i.remove();
        }
      }
    }

    缓存读取 - get

    缓存的读取比较简单,就是返回链接中对应的节点,然后在journal文件中增加READ状态。==注意,缓存里面存储的都是数据的source,而不是真正的数据,数据是在调用者需要读取的的时候才读到内存里的,下同==

    缓存存储 - update/put

    先看看基本流程

    @Nullable 
    CacheRequest put(Response response) {
      // 确定是否可以缓存,只有GET才能够缓存
    
      Entry entry = new Entry(response);
      DiskLruCache.Editor editor = null;
      try {
        // 1、生成editor
        editor = cache.edit(key(response.request().url()));
        if (editor == null) {
          return null;
        }
        // 2、向文件写入header数据
        entry.writeTo(editor);
        // 3、构造cache request,很重要!
        return new CacheRequestImpl(editor);
      } catch (IOException e) {
        abortQuietly(editor);
        return null;
      }
    }

    第一步生成editor比较简单,这一步主要做3件事:写入DIRTY记录;生成Entry;生成Editor,主要代码如下,比较清晰

    synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
      ...
      // 向journal文件写入一条DIRTY记录
      // Flush the journal before creating files to prevent file leaks.
      journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
      journalWriter.flush();
    
      ...
    
      // 实例化Entry存入链表,最后返回editor
      if (entry == null) {
        entry = new Entry(key);
        lruEntries.put(key, entry);
      }
      Editor editor = new Editor(entry);
      entry.currentEditor = editor;
      return editor;
    }

    第二步将response头写入文件,需要注意的是,header和body是要分别写入不同的文件的,而且这时候只写了header,是没有写body的,写body是在第三步

    第三步,Cache.put方法的返回值是CacheRequest,通过如下代码构造的,主要的部分就是生成了body文件对应的sink

    CacheRequestImpl(final DiskLruCache.Editor editor) {
      this.editor = editor;
      // 生成body对应的文件的sink
      this.cacheOut = editor.newSink(ENTRY_BODY);
      this.body = new ForwardingSink(cacheOut) {
        @Override public void close() throws IOException {
          synchronized (Cache.this) {
            if (done) {
              return;
            }
            done = true;
            writeSuccessCount++;
          }
          super.close();
          editor.commit();
        }
      };
    }

    CacheRequest返回给CacheInterceptor之后,Interceptor会通过cacheWritingResponse方法构造response,这里会构造body对应的缓存文件的source,方法如下

    private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
        throws IOException {
      // Some apps return a null body; for compatibility we treat that like a null cache request.
      if (cacheRequest == null) return response;
      Sink cacheBodyUnbuffered = cacheRequest.body();
      if (cacheBodyUnbuffered == null) return response;
    
      final BufferedSource source = response.body().source();
      final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
    
      Source cacheWritingSource = new Source() {
        boolean cacheRequestClosed;
    
        @Override public long read(Buffer sink, long byteCount) throws IOException {
          long bytesRead;
          try {
            bytesRead = source.read(sink, byteCount);
          } catch (IOException e) {
            ...
          }
          ...
          sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
          cacheBody.emitCompleteSegments();
          return bytesRead;
        }
        ...
      };
    
      return response.newBuilder()
          .body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
          .build();
    }

    之后,如果调用者通过response.body().soruce().readUtf8() - 或者其他read方法读取数据,就会调用到read方法,其中两行

    sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
    cacheBody.emitCompleteSegments();

    就会将缓存写入文件。在全部数据读取完成后,会调到source.close(),进一步调用到sink.close(),这个方法在CacheRequestImpl的构造方法中,这个close会再调到editor.commit(),最后执行的方法是completeEdit(),completeEdit的处理比较清晰:1、DIRTY状态下写入的文件是tmp文件,最后写入成功后,重命名;2、成功的话,修改状态为CLEAN,失败的话修改状态为REMOVE。

    到这里,缓存文件写入就完成了。

    还有点疑问:在用户读取的时候才写入body可以理解为节省空间,那为什么要先写入header呢,如果是DIRTY状态,header应该也没用才对。设计这么多状态并且记录每次状态的变更有什么好处?

    另外,cache是跟network response一起返回的,对于GET请求,在无网络的情况下应该可以很快返回,但是对于有网络但是请求处理比较慢的情况下,UI想先显示cache后显示再根据network response更新界面的需求无法用默认缓存实现,需要自己实现前置Interceptor实现。

    展开全文
  • okhttp缓存机制

    2017-10-26 08:57:40
    使用方法很简单: 1.首先进行先进行设置: ...private static final OkHttpClient client;... private static final ...// 缓存文件最大限制大小20M   private static String cacheDirectory = E

    使用方法很简单:

    1.首先进行先进行设置:

    1. private static final OkHttpClient client;  
    2.     private static final long cacheSize = 1024 * 1024 * 20;// 缓存文件最大限制大小20M  
    3.     private static String cacheDirectory = Environment.getExternalStorageDirectory() + "/okttpcaches"// 设置缓存文件路径  
    4.     private static Cache cache = new Cache(new File(cacheDirectory), cacheSize);  //  
    5.   
    6.     static {  
    7.         //如果无法生存缓存文件目录,检测权限使用已经加上,检测手机是否把文件读写权限禁止了  
    8.         OkHttpClient.Builder builder = new OkHttpClient.Builder();  
    9.         builder.connectTimeout(8, TimeUnit.SECONDS); // 设置连接超时时间  
    10.         builder.writeTimeout(8, TimeUnit.SECONDS);// 设置写入超时时间  
    11.         builder.readTimeout(8, TimeUnit.SECONDS);// 设置读取数据超时时间  
    12.         builder.retryOnConnectionFailure(true);// 设置进行连接失败重试  
    13.         builder.cache(cache);// 设置缓存  
    14.         client = builder.build();  

    15.     }

    1. /**  
    2.      * 缓存拦截器  
    3.      */    
    4.     private static class CacheInterceptor implements Interceptor {    
    5.     
    6.         @Override    
    7.         public Response intercept(Chain chain) throws IOException {    
    8.     
    9.             Response originResponse = chain.proceed(chain.request());    
    10.     
    11.             //设置缓存时间为,并移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性    
    12.             return originResponse.newBuilder().removeHeader("pragma")    
    13.                     .header("Cache-Control""max-age=10")//设置10秒    
    14.                     .header("Cache-Control""max-stale=30").build();    
    15.         }    
    16.     } 

    使用方法:
         创建okhttp builder对象添加
         具体请参考 




    展开全文
  • OkHttp缓存机制部分源码查看1.1. CacheInterceptor缓存拦截器1.2. CacheStrategy缓存策略类1.3. DiskLruCache磁盘缓存类2. 缓存案例2.1 场景:服务器响应首部没有HTTP缓存控制信息 1. OkHttp缓存机制部分源码查看 ...

    1. OkHttp缓存机制部分源码查看

    为了方便在AndroidStudio中查看源码,这里更换4.9.0版本为:

    implementation 'com.squareup.okhttp3:okhttp:3.12.0'
    

    如果需要其他的版本,可以在mvnrepository中搜索。

    切换AndroidStudio的为Project视图,然后找到OkHttp,可以看到:
    在这里插入图片描述

    简单看下这几个类:

    1.1. CacheInterceptor缓存拦截器

    CacheInterceptor:缓存拦截器,负责拦截处理网络请求。注意到下面的注释:处理来自缓存的请求并将响应写入缓存。

    /** Serves requests from the cache and writes responses to the cache. */
    public final class CacheInterceptor implements Interceptor {
    	final @Nullable InternalCache cache;
    	@Override
    	public Response intercept(Chain chain) throws IOException {
    		// 如果本次请求缓存有数据,就直接取出
    		Response cacheCandidate = cache != null
    	        ? cache.get(chain.request())
    	        : null;
    	        
    		long now = System.currentTimeMillis();
    
    		// 将请求传入到缓存策略,得到cacheResponse
        	CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        	Request networkRequest = strategy.networkRequest;
        	Response cacheResponse = strategy.cacheResponse;
    		
    		if (cache != null) {
    	      cache.trackResponse(strategy);
    	    }
    	
    	    if (cacheCandidate != null && cacheResponse == null) {
    	      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    	    }
    		
    		// If we're forbidden from using the network and the cache is insufficient, fail.
    		// 请求失败,504 网关错误
        	if (networkRequest == null && cacheResponse == null) {
    	      return new Response.Builder()
    	          .request(chain.request())
    	          .protocol(Protocol.HTTP_1_1)
    	          .code(504) 
    	          .message("Unsatisfiable Request (only-if-cached)")
    	          .body(Util.EMPTY_RESPONSE)
    	          .sentRequestAtMillis(-1L)
    	          .receivedResponseAtMillis(System.currentTimeMillis())
    	          .build();
        	}
    		
    		// If we don't need the network, we're done.
    	    if (networkRequest == null) {
    	      return cacheResponse.newBuilder()
    	          .cacheResponse(stripBody(cacheResponse))
    	          .build();
    	    }
    		// 缓存无效,继续执行网络请求
    		Response networkResponse = null;
    	    try {
    	      networkResponse = chain.proceed(networkRequest);
    	    } finally {
    	      // If we're crashing on I/O or otherwise, don't leak the cache body.
    	      if (networkResponse == null && cacheCandidate != null) {
    	        closeQuietly(cacheCandidate.body());
    	      }
    	    }
    		
    		// If we have a cache response too, then we're doing a conditional get.
    		// 协商缓存,如果本地存在,且资源为修改,就直接返回response
       		 if (cacheResponse != null) {
    	      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
    	        Response response = cacheResponse.newBuilder()
    	            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
    	            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
    	            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
    	            .cacheResponse(stripBody(cacheResponse))
    	            .networkResponse(stripBody(networkResponse))
    	            .build();
    	        networkResponse.body().close();
    	
    	        // Update the cache after combining headers but before stripping the
    	        // Content-Encoding header (as performed by initContentStream()).
    	        cache.trackConditionalCacheHit();
    	        cache.update(cacheResponse, response);
    	        return response;
    	      } else {
    	        closeQuietly(cacheResponse.body());
    	      }
    	    }
    		
    		// 请求数据,写入cache
    		...
    		return response;
    	}
    }
    

    1.2. CacheStrategy缓存策略类

    CacheStrategy:缓存策略类。主要用来判断当前缓存时候可用。在这个类的内部类Factoryget方法中,调用getCandidate方法,这个方法会将请求进行包装,也即是在请求头部添加和缓存控制相关的头部信息,比如这里我截取部分:

    // CacheStrategy.Factory->getCandidate方法
    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 {
      return new CacheStrategy(request, null); // No condition! Make a regular request.
    }
    
    // 包装,添加头部信息
    Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
    Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
    
    // 关联到请求的header
    Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
    return new CacheStrategy(conditionalRequest, cacheResponse);
    

    1.3. DiskLruCache磁盘缓存类

    Google提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但获得官方认证)。可以参考郭大佬的Android DiskLruCache完全解析,硬盘缓存的最佳方案来了解这个类。

    在前面我们在OkHttpClient创建的时候指定了cache,如:

    OkHttpClient client = new OkHttpClient.Builder()
            .cache(new Cache(file, cacheSize))
            // .addInterceptor(new MyInteceptor())
            .build();
    

    这里看下Cache这个类:

    public final class Cache implements Closeable, Flushable {
    	final DiskLruCache cache;
    	final InternalCache internalCache = new InternalCache() {
    		@Override 
    		public @Nullable Response get(Request request) throws IOException {
          		return Cache.this.get(request);
        	}
    
    	    @Override 
    	    public @Nullable CacheRequest put(Response response) throws IOException {
    	      return Cache.this.put(response);
    	    }
    	    ...
    	}
    }
    

    其实Cache类底层也就是调用DiskLruCache

    2. 缓存案例

    在上文OkHttp的HTTP缓存使用中做了简单的使用,但是不难发现在上篇的案例中虽然能够使用HTTP缓存,但是具有一定的局限性。

    因为使用OkHttpHTTP缓存时候需要确保服务端的响应是设置了对应的HTTP缓存首部控制信息。这种双方编码人员需要协商。显然这种方式不够简洁。

    所以可以尝试在OkHttp客户端这边加入首部信息,而拦截器可以做这件事情。

    2.1 场景:服务器响应首部没有HTTP缓存控制信息

    自定义一个拦截器,添加对应请求缓存控制,也响应

    class MyInteceptor implements Interceptor{
    
        @Override
        public Response intercept( Chain chain) throws IOException {
            // 1. 请求头部设置缓存信息
            Request request = chain.request().newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)  // 强制从缓存中读取
                    .build();
    
            // todo
            Log.e("TAG", "intercept: ");
            // 2. 响应请求头添加
            // 移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
            return chain.proceed(request).newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .addHeader("Cache-Control", "max-age=1800")
                    .build();
        }
    }
    

    这里如果不适用强制从缓存读取,会导致虽然OkHttp每次都写入了缓存数据,但是数据却没有从本地缓存中读取。

    此时的缓存文件头部:
    在这里插入图片描述
    和上篇OkHttp的HTTP缓存使用中的截图做对比可以发现这里写入缓存的首部信息少了Cache-ControlLast-Modified

    但至于怎么让他可以有这两个字段,暂时我还不知道。

    也就是暂时还不知道怎么做到理想情况:有缓存且没过期就从本地缓存中读取数据,没有缓存或者是已经过期就从服务器进行联网加载数据。

    找到了解决办法。还是使用拦截器,只是在使用的时候使用的是addNetworkInterceptor,而不是使用addInterceptor

    File file = new File(Environment.getExternalStorageDirectory(), "DCIM");
    long cacheSize = 10 * 1024 * 1024L; // 10MB
    OkHttpClient client = new OkHttpClient.Builder()
            .addNetworkInterceptor(new MyInteceptor())  // 这里添加拦截器使用的是addNetworkInterceptor
            .cache(new Cache(file, cacheSize))
            .build();
    
    CacheControl cacheControl = new CacheControl.Builder()
            .maxAge(1, TimeUnit.MINUTES)  // 缓存一分钟内有效,直接加载缓存
            .build();
    
    Request request = new Request.Builder()
            .cacheControl(cacheControl)
            .url("http://192.168.1.110:90/test/1.0/users/1")
            .build();
    
    button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Call call = client.newCall(request);
                call.enqueue(new Callback() {
    
                    @Override
                    public void onFailure(Call call, IOException e) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                textView.setText("数据请求失败。");
                            }
                        });
                    }
    
                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        int code = response.code();
                        String string = response.body().string();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                textView.setText(code+"\n"+string);
                            }
                        });
                    }
                });
            }
        }
    );
    
    // MyInteceptor.java
    class MyInteceptor implements Interceptor{
    
        @Override
        public Response intercept( Chain chain) throws IOException {
            Request request = chain.request().newBuilder()
                    .build();
    
            // todo
            Log.e("TAG", "intercept: ");
            // 响应请求头添加
            //设置缓存时间为60秒,并移除了pragma消息头
            return chain.proceed(request).newBuilder()
                    .removeHeader("Pragma")
                    .addHeader("Cache-Control", "max-age=1800")
                    .addHeader("Last-Modified", getTime())
                    .build();
        }
    
        private String getTime(){
            ZonedDateTime zonedDateTime = ZonedDateTime.now().with(LocalTime.MAX);
            return zonedDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);
        }
    }
    

    就可以做到在一分钟内的请求,会直接从缓存中读取,而不会请求后台服务器数据。此时再次来看下缓存的数据头部信息:
    在这里插入图片描述
    可以发现确实将Cache-ControlLast-Modified这两个字段添加到了本地缓存头部文件中。也证实了前面的分析确实是正确的。所以这里从缓存中拿取数据才会成功。

    对于addNetworkInterceptor和addInterceptor这两个方法做一个区分:

    // OkHttpClient.java
    public static final class Builder {
    	final List<Interceptor> interceptors = new ArrayList<>();
    	final List<Interceptor> networkInterceptors = new ArrayList<>();
    	...
    }
    

    这两者都是List列表存储的拦截器对象。这里看不出有什么区别。不妨来看下官网的API文档:here

    但是其实也没有得到解释。不过网上有很多解释,这里做一个笔记:
    addInterceptor是添加应用拦截器,而addNetworkInterceptor网络拦截器
    应用拦截器不关心OkHttp注入的头信息,所以在这里注册的拦截器并不会添加自己添加的响应头部信息到磁盘缓存中;
    网络拦截器能够操作中间过程的响应,根据上面的案例我们知道使用这种方式可以使用本地未过期的磁盘缓存。

    2.2 场景:断网强制使用缓存(服务器响应首部没有HTTP缓存控制信息)

    这里就直接设置请求头的缓存控制为强制从缓存读取数据。

    ps:这么做有个问题,那就是如果将缓存目录文件删除了,或者数据更新了,这里是不知道的。因为除了第一次获取数据后,强制缓存就根本不联网。

    所以可以将强制读取用在断网的时候,强制从缓存中加载。其余的时候就不适用强制缓存。

    File file = new File(Environment.getExternalStorageDirectory(), "DCIM");
    long cacheSize = 10 * 1024 * 1024L; // 10MB
    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new MyInteceptor())  // 添加请求拦截器,以添加响应首部缓存信息
            .cache(new Cache(file, cacheSize))
            .build();
    
    CacheControl cacheControl = new CacheControl.Builder()
            .maxAge(1, TimeUnit.MINUTES)
            .build();
    
    Request request = new Request.Builder()
            .cacheControl(cacheControl)
            .url("http://192.168.1.102:90/test/1.0/users/1")
            .build();
    
    button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Call call = client.newCall(request);
                call.enqueue(new Callback() {
    
                    @Override
                    public void onFailure(Call call, IOException e) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                textView.setText("数据请求失败。");
                            }
                        });
                    }
    
                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        int code = response.code();
                        String string = response.body().string();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                textView.setText(code+"\n"+string);
                            }
                        });
                    }
                });
            }
        }
    );
    
    // MyInteceptor.java
    class MyInteceptor implements Interceptor{
    
        @Override
        public Response intercept( Chain chain) throws IOException {
            // 1. 请求头部设置缓存信息
            Request request = chain.request().newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)  // 强制从缓存中读取
                    .build();
    
            // todo
            Log.e("TAG", "intercept: ");
            // 2. 响应请求头添加
            // 移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
            return chain.proceed(request).newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .addHeader("Cache-Control", "max-age=1800")
                    .build();
        }
    }
    

    注意添加权限。


    References

    展开全文
  • OkHttp实现缓存 需求 在有网的情况下,正常进行网络请求,然后把响应缓存到本地;在无网的情况下,从本地拿到...OkHttp请求过程:OkHttp缓存机制(CacheInterceptor)会自动判断我们提交的Request中的Cache-Co...

    OkHttp实现缓存

    需求

    在有网的情况下,正常进行网络请求,然后把响应缓存到本地;在无网的情况下,从本地拿到缓存,返回给调用方。

    限制

    不能改变服务器的API,服务器的API没有Cache-Control字段。

    思路

    利用OkHttp的拦截器实现。

    OkHttp请求过程:OkHttp的缓存机制(CacheInterceptor)会自动判断我们提交的Request中的Cache-Control头:如果是only-if-cache(FORCE_CACHE),则只能从缓存中获取,不能进行网络请求,如果获取缓存失败,则返回一个504的错误响应码;如果是no-cache则只从网络中获取。

    OkHttp响应过程:当正常的网络请求返回之后,CacheInterceptor会自动判断Response的Cache-Control头,如果是only-if-cache,则会缓存到本地;如果是no-cache,则不缓存。

    所以,我们可以在响应返回到CacheInterceptor之前拦截Response,强制加上Cache-Control: only-if-cache,保存缓存;然后在请求发出到CacheInterceptor之前拦截Resquest,判断当前网络状态,如果无网,则强制加上Cache-Control: only-if-cache,让请求从缓存中直接获取。

    这里写图片描述

    这里写图片描述

    关于拦截器的具体源码分析可以看我的另一篇博客:OkHttp源码解析

    实现

    拦截器

    我们先来看看拦截器的责任链工作模式: 
    这里写图片描述

    我们提交的请求首先会经过我们自定义的Interceptors,然后经过缓存(CacheInterceptor)处理,接着经过也是我们自定义的NetworkInterceptors,最后才交给CallServerInterceptor传输到TCP流。

    如何实现一个拦截器?

    //示例
    static class DemoInterceptor implements Interceptor{
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request req = chain.request(); //获得请求
                Response res = chain.process(req); //交给责任链下一环执行,最后回传一个响应
                return res; //返回响应给上一层
            }
        }

    编写请求拦截器

    先写一个请求拦截器,在请求发出去之前检查网络,如果无网,则要求使用缓存:

    public class Util {
        //判断网络连接状态
        public static boolean isNetworkConnected() {
            ConnectivityManager connectivityManager = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
            return networkInfo != null && networkInfo.isAvailable();
        }
    }
    static class RequestCacheInterceptor implements Interceptor {
    
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request.Builder builder = chain.request().newBuilder(); //在原来的request的基础上修改
                if (! Util.isNetworkConnected()) {
                    //无网下强制缓存
                    builder.cacheControl(CacheControl.FORCE_CACHE); //等同于添加only-if-cache
                }
                Request newRequest = builder.build();
                return chain.proceed(newRequest);
            }
        }

    编写响应拦截器

    再写一个响应拦截器,强制要求缓存:

        static class ResponseCacheInterceptor implements Interceptor{
            @Override
            public Response intercept(Chain chain) throws IOException {
                Response response = chain.proceed(chain.request()).newBuilder()
                        .removeHeader("Pragma") //移除影响
                        .removeHeader("Cache-Control") //移除影响
                        .addHeader("Cache-Control", CacheControl.FORCE_CACHE.toString()).build();
                return response;
            }
        }

    添加到OkHttpClient

    最后添加这些拦截器到OkHttpClient上:

    OkHttpClient client = new OkHttpClient.Builder()
        .cache(new Cache(Util.getContext().getExternalCacheDir(), 50 * 8 * 1024 * 1024)) //设置缓存目录大小50MB
        .addInterceptor(new RequestCacheInterceptor()) //在用户端添加请求拦截器
        .addNetworkInterceptor(new ResponseCacheInterceptor()) //在网络端添加响应拦截器(注意和用户端的区别)
        .build();
    展开全文
  • 剖析OkHttp缓存机制

    2016-04-27 11:03:00
    okHttp 最为人称道的一个特性就是它的缓存机制,而我将在本篇博文对其进行剖析。 每次我用 OkHttp 时我都需要一些时间想想我将怎么使用它,我该用哪一个 HTTP 报头,作为一个客户端 App 我有哪些职责,我期望从...
  • OKHTTP缓存机制的学习

    2017-11-06 11:19:41
    这几天在封装OKHttp框架,之前项目没用到数据缓存,所以了解一下,现在分享一波,肯定有不正确的地方,希望大家可以纠正。首先,一般有两种缓存:服务器端缓存、客户端缓存一、概念①服务器端缓存服务端缓存又分为...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 7,527
精华内容 3,010
关键字:

okhttp缓存机制