Android源碼系列-解密OkHttp

OkHttp是什麼?

簡介

OkHttp是一款優秀的HTTP框架,它支持get請求和post請求,支持基於Http的文件上傳和下載,支持加載圖片,支持下載文件透明的GZIP壓縮,支持響應緩存避免重複的網絡請求,支持使用鏈接池來下降響應延遲問題。OkHttp由Square公司開發,是目前Android最熱門的網絡框架之一。java

官網網址:OKHttp官網git

Github地址:Githubgithub

特色

  1. 支持HTTP2/SPDY
  2. socket自動選擇最好路線,並支持自動重連
  3. 擁有自動維護的socket鏈接池,減小握手次數
  4. 擁有隊列線程池,輕鬆寫併發
  5. 擁有Interceptors輕鬆處理請求與響應(好比透明GZIP壓縮)基於Headers的緩存策略

OkHttp怎麼用?

一、gradle引入庫,implementation 'com.squareup.okhttp3:okhttp:3.11.0'算法

二、初始化OkHttpClient對象設計模式

client = new OkHttpClient.Builder()
                .connectTimeout(15, TimeUnit.SECONDS)
                .readTimeout(15, TimeUnit.SECONDS)
                .writeTimeout(15, TimeUnit.SECONDS)
                .build();
複製代碼

同步請求

public void okHttpSync() {
        Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .build();
        Call call = client.newCall(request);
        try {
            Response response = call.execute();
            if (response.isSuccessful()) {
                System.out.println("response.code()==" + response.code());
                System.out.println("response.heard()==" + response.headers());
                System.out.println("response.message()==" + response.message());
                System.out.println("res==" + response.body().string());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
複製代碼

異步請求

public void okHttpAsync() {
        Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .build();
        Call call = client.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                needCancelled.set(true);
                System.out.println("url==" + call.request().url());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()) {
                    System.out.println("response.code()==" + response.code());
                    System.out.println("response.heard()==" + response.headers());
                    System.out.println("response.message()==" + response.message());
                    System.out.println("res==" + response.body().string());
                    needCancelled.set(true);
                }
            }
        });
    }
複製代碼

詳細的OkHttp使用可參考OKHttp使用詳解緩存

OkHttp核心執行流程是怎樣?

關鍵類功能說明

功能說明
OKHttpClient 裏面包含了不少對象,OKhttp的不少功能模塊都包裝進這個類,讓這個類單獨提供對外的API,使用Builder模型構建
Request、Response 抽象的網絡輸入及響應模型
Call HTTP請求任務封裝,是一個接口
RealCall Call的實現,實現execute()同步方法、enqueue(Callback responseCallback)異步方法, getResponseWithInterceptorChain() 獲取攔截器響應
AsyncCall RealCall的內部類。繼承了Runnable接口,後續在異步的線程池中執行
Dispatcher 核心調度類,內部維護爲了readyAsyncCalls、runningAsyncCalls、runningSyncCalls隊列,實際RealCall後續也是調用該類進行同步、異步的具體實現。內部維護了一個線程池,限制了最大併發數maxRequests=64。
RealInterceptorChain 攔截器鏈,維護了一個interceptors隊列,每次proceed經過index + 1會執行下一攔截器的intercept方法
RetryAndFollowUpInterceptor 負責失敗重連以及重定向
BridgeInterceptor 負責對Request和Response報文進行加工
CacheInterceptor 負責緩存攔截器
ConnectInterceptor 負責維護鏈接攔截器
CallServerInterceptor 負責最後網絡IO的讀寫

代碼執行流程

image

一、經過Builder模式統一構建OkHttpClient對象bash

二、經過Call,實現類RealCall進行請求發送服務器

三、RealCall經過調用了Dispatcher的execute()及enqueue()方法進行同步及異步的請求微信

四、最終調用ReallCall的getResponseWithInterceptorChain()方法進行攔截鏈的攔截cookie

五、依次經過重定向攔截器、橋接攔截器、緩存攔截器、鏈接攔截器、網絡攔截器依次進行處理

六、最後經過intercept的return往回返回Response,最終返回給客戶端請求的結果

OkHttp如何進行線程調度控制?

線程調度

在Dispatcher中維護了一個線程池,異步的請求會將任務加入到線程池中。

public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }
複製代碼

默認的最大併發數爲maxRequests=64,若是超過限制會加入到等待隊列中,執行異步的方法以下

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

複製代碼

最後線程池執行AsyncCall中的execute()方法,以下

@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        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 {
        client.dispatcher().finished(this);
      }
    }
複製代碼

隊列機制

Dispathcer中維護了3個隊列,分別爲異步等待隊列、異步執行隊列、同步執行隊列。

/** Ready async calls in the order they'll be run. */ private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */ private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>(); 複製代碼

不論是同步仍是異步,最終在finally塊都會調用dispatcher的finished方法,會移除掉該隊列任務,最後實現以下

int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
複製代碼

在finish中會再調用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.
    }
  }
複製代碼

OkHttp的攔截器及調用鏈是怎麼執行?

調用鏈執行流程

經過上述的分析,咱們知道無論同步仍是異步,最終調用到的都是RealCall的getResponseWithInterceptorChain()方法,以下:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
複製代碼

其中定義了攔截器集合及RealInterceptorChain攔截鏈,具體執行了攔截鏈的proceed方法,以下:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null. if (response == null) { throw new NullPointerException("interceptor " + interceptor + " returned null"); } if (response.body() == null) { throw new IllegalStateException( "interceptor " + interceptor + " returned a response with no body"); } return response; } 複製代碼

一、先判斷是否超過list的size,若是超過則遍歷結束,若是沒有超過則繼續執行

二、calls+1

三、new了一個RealInterceptorChain,其中而後下標index+1

四、從list取出下一個interceptor對象

五、執行interceptor的intercept方法

總結一下就是每個RealInterceptorChain對應一個interceptor,而後每個interceptor再產生下一個RealInterceptorChain,直到List迭代完成。

攔截器

image

從上面的調用關係能夠看出除了紅色圈出的攔截器以外都是系統提供的攔截器,這整個過程是遞歸的執行過程,在 CallServerInterceptor 中獲得最終的 Response 以後,將 response 按遞歸逐級進行返回,期間會通過 NetworkInterceptor 最後到達 Application Interceptor 。

OkHttp是如何進行數據緩存?

緩存策略

OkHttp使用了CacheInterceptor攔截器進行數據緩存的控制使用了CacheStrategy實現了上面的流程圖,它根據以前緩存的結果與當前將要發送Request的header進行策略,並得出是否進行請求的結果。根據輸出的networkRequest和cacheResponse的值是否爲null給出不一樣的策略,以下:

networkRequest cacheResponse result 結果
null null only-if-cached (代表不進行網絡請求,且緩存不存在或者過時,必定會返回503錯誤)
null non-null 不進行網絡請求,直接返回緩存,不請求網絡
non-null null 須要進行網絡請求,並且緩存不存在或者過去,直接訪問網絡
non-null non-null Header中包含ETag/Last-Modified標籤,須要在知足條件下請求,仍是須要訪問網絡

緩存算法

經過分析CacheInterceptor攔截器的intercept方法,咱們能夠發現具體的緩存都是使用了Cache類進行,最後具體的實如今DiskLruCache類中。緩存其實是一個比較複雜的邏輯,單獨的功能塊,實際上不屬於OKhttp上的功能,其實是經過是http協議和DiskLruCache作了處理。LinkedHashMap能夠實現LRU算法,而且在這個case裏,它被用做對DiskCache的內存索引

有興趣能夠參考以下2篇文章的具體實現:

OKHttp源碼解析(六)--中階之緩存基礎

OKHttp源碼解析(七)--中階之緩存機制

OkHttp的鏈接池複用機制是怎麼樣?

鏈路

RealConnection是Connection的實現類,表明着連接socket的鏈路,若是擁有了一個RealConnection就表明了咱們已經跟服務器有了一條通訊鏈路,並且經過 RealConnection表明是鏈接socket鏈路,RealConnection對象意味着咱們已經跟服務端有了一條通訊鏈路。 另外StreamAllocation類爲流的橋樑,在RetryAndFollowUpInterceptor中進行初始化,在ConnectInterceptor中進行newStream操做,具體的鏈接攔截器代碼以下:

public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
複製代碼

newStream建立留最後會調用到findConnection方法,這裏面是鏈接複用的關鍵,若是再鏈接池中找到能複用的鏈接,則直接返回。 不然將RealConnection加入到連接池ConnectionPool中,具體代碼以下:

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    Connection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // Attempt to use an already-allocated connection. We need to be careful here because our
      // already-allocated connection may have been restricted from creating new streams.
      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
        // We had an already-allocated connection and it's good. result = this.connection; releasedConnection = null; } if (!reportedAcquired) { // If the connection was never reported acquired, don't report it as released!
        releasedConnection = null;
      }

      if (result == null) {
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    closeQuietly(toClose);

    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
      // If we found an already-allocated or pooled connection, we're done. return result; } // If we need a route selection, make one. This is a blocking operation. boolean newRouteSelection = false; if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) { newRouteSelection = true; routeSelection = routeSelector.next(); } synchronized (connectionPool) { if (canceled) throw new IOException("Canceled"); if (newRouteSelection) { // Now that we have a set of IP addresses, make another attempt at getting a connection from // the pool. This could match due to connection coalescing. List<Route> routes = routeSelection.getAll(); for (int i = 0, size = routes.size(); i < size; i++) { Route route = routes.get(i); Internal.instance.get(connectionPool, address, this, route); if (connection != null) { foundPooledConnection = true; result = connection; this.route = route; break; } } } if (!foundPooledConnection) { if (selectedRoute == null) { selectedRoute = routeSelection.next(); } // Create a connection and assign it to this allocation immediately. This makes it possible // for an asynchronous cancel() to interrupt the handshake we're about to do.
        route = selectedRoute;
        refusedStreamCount = 0;
        result = new RealConnection(connectionPool, selectedRoute);
        acquire(result, false);
      }
    }

    // If we found a pooled connection on the 2nd time around, we're done. if (foundPooledConnection) { eventListener.connectionAcquired(call, result); return result; } // Do TCP + TLS handshakes. This is a blocking operation. result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener); routeDatabase().connected(result.route()); Socket socket = null; synchronized (connectionPool) { reportedAcquired = true; // Pool the connection. Internal.instance.put(connectionPool, result); // If another multiplexed connection to the same address was created concurrently, then // release this connection and acquire that one. if (result.isMultiplexed()) { socket = Internal.instance.deduplicate(connectionPool, address, this); result = connection; } } closeQuietly(socket); eventListener.connectionAcquired(call, result); return result; } 複製代碼

鏈接池

OkHttp中使用ConnectionPool管理http和http/2的連接,以便減小網絡請求延遲。同一個address將共享同一個connection。該類實現了複用鏈接的目標。一個OkHttpClient只包含一個ConnectionPool,其實例化也是在OkHttpClient的過程。這裏說一下ConnectionPool各個方法的調用並無直接對外暴露,而是經過OkHttpClient的Internal接口統一對外暴露。

一、獲取鏈接使用get方法,或獲取是否有合適的連接,不然返回null,具體實現以下:

RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }
複製代碼

二、加入鏈接使用put方法,而且會是會觸發cleanupRunnable,清理鏈接。具體實現以下:

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
複製代碼

三、具體的鏈接回收機制,首先統計空閒鏈接數量,而後經過for循環查找最長空閒時間的鏈接以及對應空閒時長,而後判斷是否超出最大空閒鏈接數(maxIdleConnections)或者或者超過最大空閒時間(keepAliveDurationNs),知足其一則清除最長空閒時長的鏈接。若是不知足清理條件,則返回一個對應等待時間。具體的實現以下:

long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done. long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { // We've found a connection to evict. Remove it from the list, then close it below (outside
        // of the synchronized block).
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }
複製代碼

OkHttp的底層網絡實現是什麼?

一、OkHttp使用okio進行io的操做。okio是由square公司開發的,它補充了java.io和java.nio的不足,以便可以更加方便,快速的訪問、存儲和處理你的數據。OKHttp底層也是用該庫做爲支持。並且okio使用起來很簡單,減小了不少io操做的基本代碼,而且對內存和CPU使用作了優化。

二、沒有依賴其餘的關於Http實現的庫,底層使用了Socket,本身實現了Http1.X及2.X的協議。

OkHttp中代碼運用了那些設計模式,有什麼巧妙的設計?

一、建造者模式

不論是OkHttpClient對象的建立仍是Request對象、Respone對象,都使用了建造者模式,將複雜的對象建立統一在不一樣方法中,使得建立的過程更加簡單。

二、外觀模式 OkHttpClient對外提供了統一的調度,屏蔽了內部的實現,使得使用該網絡庫簡單便捷。

三、責任鏈模式 OkHttp中的攔截器使用了責任鏈模式,將不一樣的攔截器獨立實現,動態組成鏈的調用形式。責任清晰,可動態擴展。

爲何要用OkHttp?

目前Android開發中,主要的網絡框架有HttpClient、Volley、HttpURLConnection、OkHttp。

其中Android早就不推薦httpclient,5.0以後乾脆廢棄,6.0刪除了HttpClient。因此HttpClient不考慮。Volley框架如今也已經再也不升級了,故目前考慮使用的有、HttpURLConnection及OkHttp。

相對HttpURLConnection,OkHttp使用更加便捷及靈活,且第三方社區活躍,相關資料齊全,成熟穩定性高。OkHttp也獲得了官方的承認,並在不斷優化更新,因此建議應用優先選擇OkHttp做爲網絡框架。

總結

思考

在項目的開發過程當中,咱們常用到大量的第三方框架,但可能知其然不知其因此然,經過不斷的思考反問爲何,從而去研究源碼中的實現。能讓咱們對框架更加運用自如及解決一些底層疑難的問題。

參考資料

OKHttp使用詳解

OKHTTP結合官網示例分析兩種自定義攔截器的區別

OKHttp源碼解析(一)系列

推薦

Android源碼系列-解密OkHttp

Android源碼系列-解密Retrofit

Android源碼系列-解密Glide

Android源碼系列-解密EventBus

Android源碼系列-解密RxJava

Android源碼系列-解密LeakCanary

Android源碼系列-解密BlockCanary

關於

歡迎關注個人我的公衆號

微信搜索:一碼一浮生,或者搜索公衆號ID:life2code

image

相關文章
相關標籤/搜索