1. 程式人生 > >OKHttp原始碼學習——快取篇

OKHttp原始碼學習——快取篇

OKHttp真實呼叫請求的類是RealCall

Dispatcher該類是作為請求分發

  //非同步請求最多的請求個數
  private int maxRequests = 64;
  //同一個host最多非同步請求的個數
  private int maxRequestsPerHost = 5;
  //執行緒池
  private ExecutorService executorService;
  //即將要請求的
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  //正在請求的
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  //同步請求
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

非同步請求時如果最多非同步請求超過64,並且同一主機請求個數超過了5就,放入到readyAsyncCalls

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

當網路請求完畢之後會呼叫finish方法,從readyAsyncCalls中把準備請求的拿出來

synchronized void finished(AsyncCall call) {
	//將同步請求移除
    if (!runningAsyncCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
    //將readyAsyncCalls中等待的realCall放入執行緒池中
    promoteCalls();
  }

  private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }

      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

同步請求時直接將RealCall放入到runningSyncCalls

synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

攔截器

@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain(forWebSocket);
        if (canceled) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

ApplicationInterceptorChain這是一個攔截器,使用者可以自己定義攔截器進行攔截
在這裡插入圖片描述

如果沒有攔截器,那麼就可以進行網路請求了,在getResponse方法中例項化了HttpEngine類

傳送請求sendRequest

networkRequest:只是設定了一些請求頭的操作

private Request networkRequest(Request request) throws IOException {
    Request.Builder result = request.newBuilder();

    if (request.header("Host") == null) {
      result.header("Host", hostHeader(request.url(), false));
    }

    if (request.header("Connection") == null) {
      result.header("Connection", "Keep-Alive");
    }

    if (request.header("Accept-Encoding") == null) {
      transparentGzip = true;
      result.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = client.cookieJar().loadForRequest(request.url());
    if (!cookies.isEmpty()) {
      result.header("Cookie", cookieHeader(cookies));
    }

    if (request.header("User-Agent") == null) {
      result.header("User-Agent", Version.userAgent());
    }

    return result.build();
  }

下面是快取的策略,重點啊~
如果你的程式碼設定

File cacheFile = new File(context.getExternalCacheDir().toString(),"cache");
        //快取大小為10M
        int cacheSize = 10 * 1024 * 1024;
        //建立快取物件
        final Cache cache = new Cache(cacheFile,cacheSize);
        
		OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(mConnectTimeout, TimeUnit.SECONDS)
                .readTimeout(mReadTimeOut, TimeUnit.SECONDS)
                .writeTimeout(mWriteTimeOut, TimeUnit.SECONDS)
                .cache(cache);

那麼將會在訪問網路之後

if (hasBody(userResponse)) {
      maybeCache();
      userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
    }

將響應體寫到Cache裡面,採用LRU演算法,Key是一個32位長度的MD5值,Value是檔案封裝的物件
當put存入之後,你下次responseCache.get就是上一次的響應

InternalCache responseCache = Internal.instance.internalCache(client);
    Response cacheCandidate = responseCache != null
        ? responseCache.get(request)
        : null;

但是光拿到上一次的響應頭還不算完,還得進行一些判斷,目的是判斷是否需要重新進行網路請求,因為可能響應頭的資源已經過期,超出了規定的時間,就不能在用了,要重新進行網路請求

public Factory(long nowMillis, Request request, Response cacheResponse) {
	  //這個是當前系統的時間
      this.nowMillis = nowMillis;
      //請求
      this.request = request;
      //快取中的響應
      this.cacheResponse = cacheResponse;
		
      if (cacheResponse != null) {
        Headers headers = cacheResponse.headers();
        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)) {
            //首部欄位Age能告知客戶端,源伺服器在多久前建立了響應,欄位值的單位為秒。
            //若建立該響應的伺服器是快取伺服器,Age值是指快取後的響應再次發起認證到認證
            //完成的時間值
            ageSeconds = HeaderParser.parseSeconds(value, -1);
          } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
            //傳送請求的本地時間。
            sentRequestMillis = Long.parseLong(value);
          } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
            //收到響應的本地時間。
            receivedResponseMillis = Long.parseLong(value);
          }
        }
      }
    }

只是對響應頭進行了一些抽取,接下來

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

重點先看下get方法裡面的getCandidate方法,快取策略的重點

private CacheStrategy getCandidate() {
      // No cached response.
      if (cacheResponse == null) {
      	//如果響應頭是null,那麼就得請求網路了
        return new CacheStrategy(request, null);
      }
	
      //如果缺少必要的握手,則刪除快取的響應,也得去請求網路
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

	  //主要是對各種狀態進行檢查
	  //這裡面注意下這樣程式碼
	  //!response.cacheControl().noStore() && !request.cacheControl().noStore();
	  //no-store這個響應欄位含義:簡單來說就是不能有快取,應用於機密檔案
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }
	  //請求物件獲取cacheControl物件,Request可以通過cacheControl方法進行設定
      CacheControl requestCaching = request.cacheControl();
      //requestCaching.noCache()是否需要快取
      //hasConditions(request)
      //request.header("If-Modified-Since") != null || request.header("If-None-Match") != null
      //If-Modified-Since和If-None-Match你可以理解成資料庫的條件語句
      //你想啊,有條件,證明每次請求都會不一樣,那麼當然需要重新請求網路了
      //實在好奇就自己百度下吧
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
      //該方法裡面有這樣一句receivedAge + responseDuration + residentDuration
      //就是現在的我距離上次傳送請求的時間
      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;
      //我在自己的程式碼中加入了攔截器
      /* class CacheInterceptor implements Interceptor{

        @Override
        public Response intercept(Chain chain) throws IOException {

            Response originResponse = chain.proceed(chain.request());

            //設定快取時間為60秒,並移除了pragma訊息頭,移除它的原因是因為pragma也是控制快取的一個訊息頭屬性
            return originResponse.newBuilder().removeHeader("pragma")
                    .header("Cache-Control","max-age=60").build();
        }
    }
*/
 	  //這句話就是得到我設定快取過期時間
      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());
      }

      Request.Builder conditionalRequestBuilder = request.newBuilder();
      //為請求增加條件
      if (etag != null) {
        conditionalRequestBuilder.header("If-None-Match", etag);
      } else if (lastModified != null) {
        conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
      } else if (servedDate != null) {
        conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
      }
      //如果已經有條件了,那麼就不能走快取了,需要重新進行網路請求
      Request conditionalRequest = conditionalRequestBuilder.build();
      return hasConditions(conditionalRequest)
          ? new CacheStrategy(conditionalRequest, cacheResponse)
          : new CacheStrategy(conditionalRequest, null);
    }

以上就是快取的策略

//假如你現在沒有快取,並且你還設定了CacheControl.FORCE_CACHE這個引數,那麼你就會收
//到504的異常
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

回到上面接著說

if (responseCache != null) {
	  //記錄當前請求是網路發起還是快取發起
      responseCache.trackResponse(cacheStrategy);
    }
    
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    //504錯誤
    if (networkRequest == null && cacheResponse == null) {
      userResponse = new Response.Builder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_BODY)
          .build();
      return;
    }

    //如果networkRequest為null那麼就可以走快取了,這種情況在前面講過
    if (networkRequest == null) {
      userResponse = cacheResponse.newBuilder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .cacheResponse(stripBody(cacheResponse))
          .build();
      userResponse = unzip(userResponse);
      return;
    }