徹底理解OkHttp - OkHttp 原始碼解析及OkHttp的設計思想

JakePrim發表於2018-12-20

OkHttp 現在統治了Android的網路請求領域,最常用的框架是:Retrofit+okhttp。OkHttp的實現原理和設計思想是必須要了解的,讀懂和理解流行的框架也是程式設計師進階的必經之路,程式碼和語言只是工具,重要的是思想。

在OKhttp 原始碼解析之前,我們必須先要了解http的相關基礎知識,任何的網路請求都離不開http。

概述

okhttp的原始碼分析,網上有好多部落格講解,但講解的都是一些原始碼可有可無的知識,並沒有將okhttp的核心設計思想講解到位,我們閱讀一些框架的原始碼,學習的其實就是其設計思想,瞭解了整體的框架設計,在深入瞭解細節的實現會更加容易。

OkHttp 原始碼解析

1、OkHttp 的整體框架設計

建議將okhttp的原始碼下載下來,用AndroidStudio 開啟,整篇文章是根據原始碼的分析來學習okhttp的設計技巧和思想,如果本篇文章有內容分析不到位的地方,歡迎大家和我一起討論。

下圖為okhttp請求網路的完整流程圖(大致看一遍)

image.png

okhttp的使用方法

OkHttpClient client = new OkHttpClient();
複製程式碼

我們第一步先看一下okhttp的建構函式OkHttpClient()和一些配置相關,大致瞭解一下。

public OkHttpClient() {
        this(new Builder());
    }
複製程式碼

原來OkHttpClient 內部已經實現了OkHttpClient(Builder builder),如果我們不需要配置client,okhttp已將幫我們預設實現了配置。

 public Builder() {
            dispatcher = new Dispatcher();
            protocols = DEFAULT_PROTOCOLS;
            connectionSpecs = DEFAULT_CONNECTION_SPECS;
            eventListenerFactory = EventListener.factory(EventListener.NONE);
            proxySelector = ProxySelector.getDefault();
            cookieJar = CookieJar.NO_COOKIES;
            socketFactory = SocketFactory.getDefault();
            hostnameVerifier = OkHostnameVerifier.INSTANCE;
            certificatePinner = CertificatePinner.DEFAULT;
            proxyAuthenticator = Authenticator.NONE;
            authenticator = Authenticator.NONE;
            connectionPool = new ConnectionPool();
            dns = Dns.SYSTEM;
            followSslRedirects = true;
            followRedirects = true;
            retryOnConnectionFailure = true;
            connectTimeout = 10_000;
            readTimeout = 10_000;
            writeTimeout = 10_000;
            pingInterval = 0;
        }
複製程式碼

如果需要一些配置如新增攔截器等,則需要這樣呼叫即可:

 mOkHttpClient = new OkHttpClient.Builder()
                            .addInterceptor(loggingInterceptor)
                            .retryOnConnectionFailure(true)
                            .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
                            .readTimeout(TIME_OUT, TimeUnit.SECONDS)
                            .build();
複製程式碼

我們看一下OkHttpClient 都有那些屬性,稍微瞭解一下即可。後面在分析

    final Dispatcher dispatcher;//排程器
    final @Nullable
    Proxy proxy;//代理
    final List<Protocol> protocols;//協議
    final List<ConnectionSpec> connectionSpecs;//傳輸層版本和連線協議
    final List<Interceptor> interceptors;//攔截器
    final List<Interceptor> networkInterceptors;//網路攔截器
    final EventListener.Factory eventListenerFactory;
    final ProxySelector proxySelector;//代理選擇器
    final CookieJar cookieJar;//cookie
    final @Nullable
    Cache cache;//cache 快取
    final @Nullable
    InternalCache internalCache;//內部快取
    final SocketFactory socketFactory;//socket 工廠
    final @Nullable
    SSLSocketFactory sslSocketFactory;//安全套層socket工廠 用於https
    final @Nullable
    CertificateChainCleaner certificateChainCleaner;//驗證確認響應書,適用HTTPS 請求連線的主機名
    final HostnameVerifier hostnameVerifier;//主機名字確認
    final CertificatePinner certificatePinner;//證照鏈
    final Authenticator proxyAuthenticator;//代理身份驗證
    final Authenticator authenticator;//本地省份驗證
    final ConnectionPool connectionPool;//連結池 複用連線
    final Dns dns; //域名
    final boolean followSslRedirects;//安全套接層重定向
    final boolean followRedirects;//本地重定向
    final boolean retryOnConnectionFailure;//重試連線失敗
    final int connectTimeout;//連線超時
    final int readTimeout;//讀取超時
    final int writeTimeout;//寫入超時
複製程式碼
請求網路
String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}
複製程式碼

從原始碼中可以看出 okhttp 實現了Call.Factory介面

interface Factory {
    Call newCall(Request request);
  }
複製程式碼

我們看一下okhttpClient 如何實現的Call介面,程式碼如下:

 @Override
    public Call newCall(Request request) {
        return RealCall.newRealCall(this, request, false /* for web socket */);
    }
複製程式碼

可以看出 真正的請求交給了 RealCall 類,並且RealCall 實現了Call方法,RealCall是真正的核心程式碼。

RealCall 主要方法:同步請求 :client.newCall(request).execute(); 和 非同步請求: client.newCall(request).enqueue();

下面我們著重分析一下非同步請求,因為在專案中所有的網路請求基本都是非同步的,同步很少用到,最後我們在分析一下同步請求即可。

非同步請求

跟著原始碼 走一遍 RealCall.enqueue() 的實現

RealCall.java

 @Override public void enqueue(Callback responseCallback) {
    //TODO 不能重複執行
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    //TODO 交給 dispatcher排程器 進行排程
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }
複製程式碼

可以看到上述程式碼做了幾件事:

  1. synchronized (this) 確保每個call只能被執行一次不能重複執行,如果想要完全相同的call,可以呼叫如下方法:進行克隆
@SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state.
  @Override public RealCall clone() {
    return RealCall.newRealCall(client, originalRequest, forWebSocket);
  }
複製程式碼
  1. 利用dispatcher排程器,來進行實際的執行client.dispatcher().enqueue(new AsyncCall(responseCallback));,在上面的OkHttpClient.Builder可以看出 已經初始化了Dispatcher。

下面我們著重看一下排程器的實現。

Dispatcher 排程器

Dispatcher#enqueue 的方法實現如下:

 //TODO 執行非同步請求
    synchronized void enqueue(AsyncCall call) {
        //TODO 同時請求不能超過併發數(64,可配置排程器調整)
        //TODO okhttp會使用共享主機即 地址相同的會共享socket
        //TODO 同一個host最多允許5條執行緒通知執行請求
        if (runningAsyncCalls.size() < maxRequests &&
                runningCallsForHost(call) < maxRequestsPerHost) {
            //TODO 加入執行佇列 並交給執行緒池執行
            runningAsyncCalls.add(call);
            //TODO AsyncCall 是一個runnable,放到執行緒池中去執行,檢視其execute實現
            executorService().execute(call);
        } else {
            //TODO 加入等候佇列
            readyAsyncCalls.add(call);
        }
    }
複製程式碼

從上述程式碼可以看到Dispatcher將call 加入到佇列中,然後通過執行緒池來執行call。

可能大家看了還是懵懵的,我們先了解一下Dispatcher幾個屬性和方法

    //TODO 同時能進行的最大請求數
    private int maxRequests = 64;
    //TODO 同時請求的相同HOST的最大個數 SCHEME :// HOST [ ":" PORT ] [ PATH [ "?" QUERY ]]
    //TODO 如 https://restapi.amap.com  restapi.amap.com - host
    private int maxRequestsPerHost = 5;
    /**
     * Ready async calls in the order they'll be run.
     * TODO 雙端佇列,支援首尾兩端 雙向開口可進可出,方便移除
     * 非同步等待佇列
     *
     */
    private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

    /**
     * Running asynchronous calls. Includes canceled calls that haven't finished yet.
     * TODO 正在進行的非同步佇列
     */
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
複製程式碼

很明顯,okhttp 可以進行多個併發網路請求,並且可以設定最大的請求數

executorService() 這個方法很簡單,只是建立了一個執行緒池

 public synchronized ExecutorService executorService() {
        if (executorService == null) {
            //TODO 執行緒池的相關概念 需要理解
            //TODO 核心執行緒 最大執行緒 非核心執行緒閒置60秒回收 任務佇列
            executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher",
                    false));
        }
        return executorService;
    }
複製程式碼

既然我們將 call放到了執行緒池中那麼它是如何執行的呢?注意這裡的call是AsyncCall。

我們看一下AsyncCall的實現:

final class AsyncCall extends NamedRunnable
複製程式碼

NamedRunnable 是什麼鬼?點進去看一下

public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}
複製程式碼

原來如此 AsyncCall其實就是一個 Runnable,執行緒池實際上就是執行了execute()。

我們在看一下AsyncCall的execute()

 final class AsyncCall extends NamedRunnable {
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        //TODO 責任鏈模式
        //TODO 攔截器鏈  執行請求
        Response response = getResponseWithInterceptorChain();
        //回撥結果
        if (retryAndFollowUpInterceptor.isCanceled()) {
          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!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        //TODO 移除佇列
        client.dispatcher().finished(this);
      }
    }
  }
複製程式碼

從上述程式碼可以看出真正執行請求的是getResponseWithInterceptorChain(); 然後通過回撥將Response返回給使用者。

值得注意的finally 執行了client.dispatcher().finished(this); 通過排程器移除佇列,並且判斷是否存在等待佇列,如果存在,檢查執行佇列是否達到最大值,如果沒有將等待佇列變為執行佇列。這樣也就確保了等待佇列被執行。

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
        int runningCallsCount;
        Runnable idleCallback;
        synchronized (this) {
            //TODO calls 移除佇列
            if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
            //TODO 檢查是否為非同步請求,檢查等候的佇列 readyAsyncCalls,如果存在等候佇列,則將等候佇列加入執行佇列
            if (promoteCalls) promoteCalls();
            //TODO 執行佇列的數量
            runningCallsCount = runningCallsCount();
            idleCallback = this.idleCallback;
        }
        //閒置呼叫
        if (runningCallsCount == 0 && idleCallback != null) {
            idleCallback.run();
        }
    }
    
    private void promoteCalls() {
        //TODO 檢查 執行佇列 與 等待佇列
        if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
        if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

        //TODO 將等待佇列加入到執行佇列中
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
            AsyncCall call = i.next();
            //TODO  相同host的請求沒有達到最大,加入執行佇列
            if (runningCallsForHost(call) < maxRequestsPerHost) {
                i.remove();
                runningAsyncCalls.add(call);
                executorService().execute(call);
            }

            if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
        }
    }
複製程式碼

真正的執行網路請求和返回響應結果:getResponseWithInterceptorChain(),下面我們著重分析一下這個方法:

每段程式碼我都加上了註釋。

//TODO 核心程式碼 開始真正的執行網路請求
  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    //TODO 責任鏈
    List<Interceptor> interceptors = new ArrayList<>();
    //TODO 在配置okhttpClient 時設定的intercept 由使用者自己設定
    interceptors.addAll(client.interceptors());
    //TODO 負責處理失敗後的重試與重定向
    interceptors.add(retryAndFollowUpInterceptor);
    //TODO 負責把使用者構造的請求轉換為傳送到伺服器的請求 、把伺服器返回的響應轉換為使用者友好的響應 處理 配置請求頭等資訊
    //TODO 從應用程式程式碼到網路程式碼的橋樑。首先,它根據使用者請求構建網路請求。然後它繼續呼叫網路。最後,它根據網路響應構建使用者響應。
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //TODO 處理 快取配置 根據條件(存在響應快取並被設定為不變的或者響應在有效期內)返回快取響應
    //TODO 設定請求頭(If-None-Match、If-Modified-Since等) 伺服器可能返回304(未修改)
    //TODO 可配置使用者自己設定的快取攔截器
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //TODO 連線伺服器 負責和伺服器建立連線 這裡才是真正的請求網路
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      //TODO 配置okhttpClient 時設定的networkInterceptors
      //TODO 返回觀察單個網路請求和響應的不可變攔截器列表。
      interceptors.addAll(client.networkInterceptors());
    }
    //TODO 執行流操作(寫出請求體、獲得響應資料) 負責向伺服器傳送請求資料、從伺服器讀取響應資料
    //TODO 進行http請求報文的封裝與請求報文的解析
    interceptors.add(new CallServerInterceptor(forWebSocket));

    //TODO 建立責任鏈
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    //TODO 執行責任鏈
    return chain.proceed(originalRequest);
  }
複製程式碼

從上述程式碼中,可以看出都實現了Interceptor介面,這是okhttp最核心的部分,採用責任鏈的模式來使每個功能分開,每個Interceptor自行完成自己的任務,並且將不屬於自己的任務交給下一個,簡化了各自的責任和邏輯。

責任鏈模式是設計模式中的一種也相當簡單參考連結,這裡不在複述。

我們著重分析一下,okhttp的設計實現,如何通過責任鏈來進行傳遞返回資料的。

上述程式碼中可以看出interceptors,是傳遞到了RealInterceptorChain該類實現了Interceptor.Chain,並且執行了chain.proceed(originalRequest)。

其實核心程式碼就是chain.proceed() 通過該方法進行責任鏈的執行。

  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();
    calls++;
    //TODO 建立新的攔截鏈,鏈中的攔截器集合index+1
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    //TODO 執行當前的攔截器-如果在配置okhttpClient,時沒有設定intercept預設是先執行:retryAndFollowUpInterceptor 攔截器
    Interceptor interceptor = interceptors.get(index);
    //TODO 執行攔截器
    Response response = interceptor.intercept(next);
    return response;
  }
複製程式碼

從上述程式碼,我們可以知道,新建了一個RealInterceptorChain 責任鏈 並且 index+1,然後 執行interceptors.get(index); 返回Response。

其實就是按順序執行了攔截器,這裡我畫了一個簡圖:

image.png

攔截器的執行順序便是如上圖這樣執行的。

這樣設計的一個好處就是,責任鏈中每個攔截器都會執行chain.proceed()方法之前的程式碼,等責任鏈最後一個攔截器執行完畢後會返回最終的響應資料,而chain.proceed() 方法會得到最終的響應資料,這時就會執行每個攔截器的chain.proceed()方法之後的程式碼,其實就是對響應資料的一些操作。

CacheInterceptor 快取攔截器就是很好的證明,我們來通過CacheInterceptor 快取攔截器來進行分析,大家就會明白了。

CacheInterceptor 的實現如下:

程式碼比較長,我們一步一步的來進行分析。

首先我們先分析上部分程式碼當沒有網路的情況下是如何處理獲取快取的。

  @Override public Response intercept(Chain chain) throws IOException
  {
//TODO 獲取request對應快取的Response 如果使用者沒有配置快取攔截器 cacheCandidate == null
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    //TODO 執行響應快取策略
    long now = System.currentTimeMillis();
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //TODO 如果networkRequest == null 則說明不使用網路請求
    Request networkRequest = strategy.networkRequest;
    //TODO 獲取快取中(CacheStrategy)的Response
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }
    //TODO 快取無效 關閉資源
    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.
    //TODO networkRequest == null 不實用網路請求 且沒有快取 cacheResponse == null  返回失敗
    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();
    }

    //TODO 不使用網路請求 且存在快取 直接返回響應
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    }
複製程式碼

上述的程式碼,主要做了幾件事:

  1. 如果使用者自己配置了快取攔截器,cacheCandidate = cache.Response 獲取使用者自己儲存的Response,否則 cacheCandidate = null;同時從CacheStrategy 獲取cacheResponse 和 networkRequest
  2. 如果cacheCandidate != null 而 cacheResponse == null 說明快取無效清楚cacheCandidate快取。
  3. 如果networkRequest == null 說明沒有網路,cacheResponse == null 沒有快取,返回失敗的資訊,責任鏈此時也就終止,不會在往下繼續執行。
  4. 如果networkRequest == null 說明沒有網路,cacheResponse != null 有快取,返回快取的資訊,責任鏈此時也就終止,不會在往下繼續執行。

上部分程式碼,其實就是沒有網路的時候的處理。

那麼下部分程式碼肯定是,有網路的時候處理

    //TODO 執行下一個攔截器
    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());
      }
    }

    //TODO 網路請求 回來 更新快取
    // If we have a cache response too, then we're doing a conditional get.
    //TODO 如果存在快取 更新
    if (cacheResponse != null) {
      //TODO 304響應碼 自從上次請求後,請求需要響應的內容未發生改變
      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());
      }
    }
    //TODO 快取Response
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }
複製程式碼

下部分程式碼主要做了這幾件事:

  1. 執行下一個攔截器,也就是請求網路
  2. 責任鏈執行完畢後,會返回最終響應資料,如果快取存在更新快取,如果快取不存在加入到快取中去。

這樣就體現出了,責任鏈這樣實現的好處了,當責任鏈執行完畢,如果攔截器想要拿到最終的資料做其他的邏輯處理等,這樣就不用在做其他的呼叫方法邏輯了,直接在當前的攔截器就可以拿到最終的資料。

這也是okhttp設計的最優雅最核心的功能。

當然我們可以通過一個小例子來進行驗證,實踐才最重要。

首先我們模擬一個 攔截器的介面

/**
 * @author prim
 * @version 1.0.0
 * @desc 模擬okhttp攔截器
 * @time 2018/8/3 - 下午4:29
 */
public interface Interceptor {
    String interceptor(Chain chain);

    interface Chain {
        String request();

        String proceed(String request);
    }
}
複製程式碼

然後在實現幾個攔截器

public class BridgeInterceptor implements Interceptor {
    @Override
    public String interceptor(Chain chain) {
        System.out.println("執行 BridgeInterceptor 攔截器之前程式碼");
        String proceed = chain.proceed(chain.request());
        System.out.println("執行 BridgeInterceptor 攔截器之後程式碼 得到最終資料:"+proceed);
        return proceed;
    }
}

public class RetryAndFollowInterceptor implements Interceptor {
    @Override
    public String interceptor(Chain chain) {
        System.out.println("執行 RetryAndFollowInterceptor 攔截器之前程式碼");
        String proceed = chain.proceed(chain.request());
        System.out.println("執行 RetryAndFollowInterceptor 攔截器之後程式碼 得到最終資料:" + proceed);
        return proceed;
    }
}

public class CacheInterceptor implements Interceptor {
    @Override
    public String interceptor(Chain chain) {
        System.out.println("執行 CacheInterceptor 最後一個攔截器 返回最終資料");
        return "success";
    }
}
複製程式碼

然後實現Chain 介面

public class RealInterceptorChain implements Interceptor.Chain {

    private List<Interceptor> interceptors;

    private int index;

    private String request;

    public RealInterceptorChain(List<Interceptor> interceptors, int index, String request) {
        this.interceptors = interceptors;
        this.index = index;
        this.request = request;
    }

    @Override
    public String request() {
        return request;
    }

    @Override
    public String proceed(String request) {
        if (index >= interceptors.size()) return null;

        //獲取下一個責任鏈
        RealInterceptorChain next = new RealInterceptorChain(interceptors, index+1, request);
        // 執行當前的攔截器
        Interceptor interceptor = interceptors.get(index);

        return interceptor.interceptor(next);
    }
}
複製程式碼

然後進行測試,看看我們是否分析的正確

List<Interceptor> interceptors = new ArrayList<>();
        interceptors.add(new BridgeInterceptor());
        interceptors.add(new RetryAndFollowInterceptor());
        interceptors.add(new CacheInterceptor());

        RealInterceptorChain request = new RealInterceptorChain(interceptors, 0, "request");

        request.proceed("request");
複製程式碼

列印的log日誌如下:

執行 BridgeInterceptor 攔截器之前程式碼
執行 RetryAndFollowInterceptor 攔截器之前程式碼
執行 CacheInterceptor 最後一個攔截器 返回最終資料
執行 RetryAndFollowInterceptor 攔截器之後程式碼 得到最終資料:success
執行 BridgeInterceptor 攔截器之後程式碼 得到最終資料:success
複製程式碼

OK 完美,驗證沒有問題,我想至此大家都應該懂了 okhttp的核心設計思想了。

okhttp的其他攔截器的具體實現大家可以自己研究一下即可,okhttp的這種設計思想我們完全可以應用到專案中去,解決一些問題。

同步請求

這裡在稍微講一下,okhttp的同步請求,程式碼很簡單 同樣是在RealCall 類中實現的

//TODO 同步執行請求 直接返回一個請求的結果
  @Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    //TODO 呼叫監聽的開始方法
    eventListener.callStart(this);
    try {
      //TODO 交給排程器去執行
      client.dispatcher().executed(this);
      //TODO 獲取請求的返回資料
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      //TODO 執行排程器的完成方法 移除佇列
      client.dispatcher().finished(this);
    }
  }
複製程式碼

主要做了幾件事:

  1. synchronized (this) 避免重複執行,上面的文章部分有講。
  2. client.dispatcher().executed(this); 實際上排程器只是將call 加入到了同步執行佇列中。程式碼如下:
//TODO 排程器執行同步請求
    synchronized void executed(RealCall call) {
        runningSyncCalls.add(call);
    }
複製程式碼
  1. getResponseWithInterceptorChain()最核心的程式碼,上面已經分析過了,請求網路得到響應資料,返回給使用者。
  2. client.dispatcher().finished(this); 執行排程器的完成方法 移除佇列

可以看出,在同步請求的方法中,涉及到dispatcher 只是告知了執行狀態,開始執行了(呼叫 executed),執行完畢了(呼叫 finished)其他的並沒有涉及到。dispatcher 更多的是服務非同步請求。

總結

okhttp還有很多細節在本文中並沒有涉及到,例如:okhttp是如何利用DiskLruCache實現快取的、HTTP2/HTTPS 的支援等,本文主要講解okhttp的核心設計思想,對整體有了清晰的認識之後,在深入細節,更容易理解。

簡述okhttp的執行流程:

  1. OkhttpClient 實現了Call.Fctory,負責為Request 建立 Call;
  2. RealCall 為Call的具體實現,其enqueue() 非同步請求介面通過Dispatcher()排程器利用ExcutorService實現,而最終進行網路請求時和同步的execute()介面一致,都是通過 getResponseWithInterceptorChain() 函式實現
  3. getResponseWithInterceptorChain() 中利用 Interceptor 鏈條,責任鏈模式 分層實現快取、透明壓縮、網路 IO 等功能;最終將響應資料返回給使用者。

拆輪子系列:拆 OkHttp

OkHttp

相關文章