深入理解OkHttp源碼及設(shè)計(jì)思想

用OkHttp很久了,也看了很多人寫的源碼分析,在這里結(jié)合自己的感悟,記錄一下對(duì)OkHttp源碼理解的幾點(diǎn)心得。

整體結(jié)構(gòu)

網(wǎng)絡(luò)請(qǐng)求框架雖然都要做請(qǐng)求任務(wù)的封裝和管理,但是最大的難點(diǎn)在于網(wǎng)絡(luò)請(qǐng)求任務(wù)的多樣性,因?yàn)榫W(wǎng)絡(luò)層情況復(fù)雜,不僅要考慮功能性的建立Socket連接、文件流傳輸、TLS安全、多平臺(tái)等,還要考慮性能上的Cache復(fù)用、Cache過(guò)期、連接池復(fù)用等,這些功能如果交錯(cuò)在一起,實(shí)現(xiàn)和維護(hù)都會(huì)有很大的問(wèn)題。

為了解決這個(gè)問(wèn)題,OkHttp采用了分層設(shè)計(jì)的思想,使用多層攔截器,每個(gè)攔截器解決一個(gè)問(wèn)題,多層攔截器套在一起,就像設(shè)計(jì)模式中的裝飾者模式一樣,可以在保證每層功能高內(nèi)聚的情況下,解決多樣性的問(wèn)題。

OkHttp使用了外觀模式,開發(fā)者直接操作的主要就是OkHttpClient,其實(shí)如果粗略劃分的話,整個(gè)OkHttp框架從功能上可以分為三部分:
1.請(qǐng)求和回調(diào):具體的類就是Call、RealCall(及其內(nèi)部類AsyncCall)、Callback等。
2.分發(fā)器及線程池:具體的類就是Dispatcher、ThreadPoolExecutor等。
3.攔截器:實(shí)現(xiàn)了分層設(shè)計(jì)+鏈?zhǔn)秸{(diào)用,具體的類就是Interceptor+RealInterceptorChain。

至于更具體的操作,均由攔截器實(shí)現(xiàn),包括應(yīng)用層攔截器、網(wǎng)絡(luò)層攔截器等,開發(fā)者也可以自己擴(kuò)展新的攔截器。

請(qǐng)求

網(wǎng)絡(luò)請(qǐng)求其實(shí)可以分為數(shù)據(jù)和行為兩部分,數(shù)據(jù)即我們的請(qǐng)求數(shù)據(jù)和返回?cái)?shù)據(jù),行為則是發(fā)起網(wǎng)絡(luò)請(qǐng)求,以及得到處理結(jié)果。
數(shù)據(jù)(Request和Response)
在OkHttp中,用Request定義請(qǐng)求數(shù)據(jù),用Response定義返回?cái)?shù)據(jù),這兩個(gè)類都使用了建造者模式,把對(duì)象的創(chuàng)建和使用分離開,但這兩個(gè)類更接近于數(shù)據(jù)模型,主要用來(lái)讀寫數(shù)據(jù),不做請(qǐng)求動(dòng)作。
行為(Call/RealCall/AsyncCall和Callback)
在OkHttp中,用Call和Callback定義網(wǎng)絡(luò)請(qǐng)求,用Call去發(fā)起網(wǎng)絡(luò)請(qǐng)求,用Callback去接收異步返回,(如果是同步請(qǐng)求,就直接返回Response數(shù)據(jù))。
其中,Call是個(gè)接口,真正的實(shí)現(xiàn)類是RealCall,RealCall如果需要異步處理,還會(huì)先包裝為RealCall的內(nèi)部類AsyncCall,然后再把AsyncCall交給線程池。

在具體執(zhí)行過(guò)程中,把數(shù)據(jù)對(duì)象交給行為對(duì)象去操作:
在RealCall行為中調(diào)用enqueue去發(fā)起異步網(wǎng)絡(luò)請(qǐng)求,此時(shí)需要傳參Request數(shù)據(jù)對(duì)象;返回的Callback會(huì)傳遞Response數(shù)據(jù)對(duì)象。
如果RealCall行為中調(diào)用的是execute同步網(wǎng)絡(luò)請(qǐng)求,就直接返回Response數(shù)據(jù)對(duì)象。

RealCall只是對(duì)請(qǐng)求做了封裝,真正處理請(qǐng)求的是分發(fā)器Dispatcher。

分發(fā)器及線程池

對(duì)于網(wǎng)絡(luò)請(qǐng)求RealCall來(lái)說(shuō),需要可并行、可回調(diào)、可取消,因?yàn)镺kHttp統(tǒng)一使用Dispatcher分發(fā)器來(lái)分發(fā)所有的Call請(qǐng)求,分發(fā)給多個(gè)線程進(jìn)行執(zhí)行(所以Dispatcher也叫反向代理),所以,這幾個(gè)問(wèn)題就需要交給Dispatcher來(lái)處理,對(duì)于Dispatcher來(lái)說(shuō),可并行、可回調(diào)、可取消的問(wèn)題可以進(jìn)一步被分解為以下幾個(gè)問(wèn)題,并分別處理:

1.有沒(méi)有必要管理所有的請(qǐng)求

不論是同步請(qǐng)求還是異步請(qǐng)求,都是耗時(shí)操作,所以是個(gè)需要觀測(cè)的行為,比如請(qǐng)求結(jié)束需要處理,請(qǐng)求本身可能取消等,都需要管理起來(lái)。
而且,不論是正在運(yùn)行的,還是等待運(yùn)行的,都需要管理。

2.如何管理所有的請(qǐng)求

為了管理所有的請(qǐng)求,Dispatcher采用了隊(duì)列+生產(chǎn)+消費(fèi)的模式。
為同步執(zhí)行提供了runningSyncCalls來(lái)管理所有的同步請(qǐng)求;
為異步執(zhí)行提供了runningAsyncCalls和readyAsyncCalls來(lái)管理所有的異步請(qǐng)求。
其中readyAsyncCalls是在當(dāng)前可用資源不足時(shí),用于緩存請(qǐng)求的。

由于這三個(gè)隊(duì)列的使用場(chǎng)景類似于棧,偶爾需要?jiǎng)h除功能,所以O(shè)kHttp使用了ArrayDeque雙端隊(duì)列來(lái)管理,ArrayDeque的設(shè)計(jì)和實(shí)現(xiàn)非常精妙,感興趣的可以深入了解一下。

3.如何確保多個(gè)隊(duì)列之間能順暢地調(diào)度

對(duì)于多線程情況下的隊(duì)列調(diào)度,其實(shí)就是數(shù)據(jù)移動(dòng)和失敗阻塞的這兩個(gè)問(wèn)題。
對(duì)于數(shù)據(jù)移動(dòng)來(lái)說(shuō),就是要考慮多線程下隊(duì)列數(shù)據(jù)移動(dòng)的問(wèn)題。
對(duì)于同步請(qǐng)求來(lái)說(shuō),只有1個(gè)隊(duì)列,不存在數(shù)據(jù)移動(dòng),數(shù)據(jù)移動(dòng)的場(chǎng)景在兩個(gè)異步隊(duì)列,每當(dāng)有一個(gè)異步請(qǐng)求finish了,就需要從待處理readyAsyncCalls隊(duì)列移動(dòng)到runningAsyncCalls隊(duì)列,這在多線程場(chǎng)景下并不安全,需要加鎖:

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

在promoteCalls時(shí),會(huì)把call從ready隊(duì)列轉(zhuǎn)移到running隊(duì)列:

 private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    ...
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);//添加隊(duì)列
        executorService().execute(call);//交給線程池
      }

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

另外這個(gè)移動(dòng)的操作放在finish函數(shù)里,會(huì)存在另一個(gè)問(wèn)題,就是如何確保會(huì)執(zhí)行這個(gè)finish函數(shù),避免造成失敗阻塞。

對(duì)于失敗阻塞來(lái)說(shuō),因?yàn)榫W(wǎng)絡(luò)請(qǐng)求失敗是很常見的場(chǎng)景,必須能在失敗時(shí)避免阻塞隊(duì)列。
OkHttp的處理是為Call對(duì)象的execute函數(shù)寫try finally,在RealCall的execute函數(shù)里,在finally中調(diào)用client.dispatcher.finish(call),確保隊(duì)列不阻塞。
這其實(shí)類似AsyncTask的處理方式,AsyncTask也是使用了try finally,在finally中scheduleNext,確保隊(duì)列不阻塞。

4.如何實(shí)現(xiàn)多線程

io是個(gè)耗時(shí)但是不耗CPU的操作,是典型的需要并行處理的場(chǎng)景。
OkHttp不出意外地采用了線程池實(shí)現(xiàn)并行,這一點(diǎn)類似于AsyncTask,但不像AsyncTask使用了全局唯一的線程池,每個(gè)OkHttpClient都有自己的線程池。
不過(guò),與AsyncTask不同的是,OkHttp的同步執(zhí)行不進(jìn)線程池,在RealCall執(zhí)行同步execute任務(wù)時(shí),只是在Dispatcher的runningSyncCalls中記錄這個(gè)call,然后直接在當(dāng)前線程執(zhí)行了攔截器的操作。
至于異步執(zhí)行,就是在RealCall中enqueue時(shí)調(diào)用Dispatcher的enqueue,然后調(diào)用線程池executeService().execute(call),這里面的call是RealCall的內(nèi)部類AsyncCall,實(shí)現(xiàn)異步調(diào)用。

5.在這個(gè)過(guò)程中,用哪些方式提升效率

OkHttp主要針對(duì)隊(duì)列和線程池做了優(yōu)化:
循環(huán)數(shù)組
因?yàn)镈ispatcher中的三個(gè)隊(duì)列需要頻繁出棧和入棧,所以采用了性能良好的循環(huán)數(shù)組ArrayDeque管理隊(duì)列。

阻塞隊(duì)列
因?yàn)镈ispatcher自己用隊(duì)列管理了排隊(duì)的請(qǐng)求,所以Dispatcher中的線程池其實(shí)不需要緩存隊(duì)列,那么這個(gè)線程池的任務(wù)其實(shí)是盡快地把元素轉(zhuǎn)交給線程池中的io線程,所以采用了容量為0的阻塞隊(duì)列SynchronousQueue,SynchronousQueue與普通隊(duì)列不同,不是數(shù)據(jù)等線程,而是線程等數(shù)據(jù),這樣每次向SynchronousQueue里傳入數(shù)據(jù)時(shí),都會(huì)立即交給一個(gè)線程執(zhí)行,這樣可以提高數(shù)據(jù)得到處理的速度。

控制線程數(shù)量
因?yàn)榫€程本身也會(huì)消耗資源,所以每個(gè)線程池都需要控制線程數(shù)量,OkHttp的線程池更進(jìn)一步,會(huì)針對(duì)每個(gè)Host主機(jī)的請(qǐng)求(避免全都卡死在某個(gè)Host上),分別控制線程數(shù)上限(5個(gè)),具體方法就是遍歷所有runningAsyncCall隊(duì)列中的每個(gè)Call,查詢每個(gè)Call的Host,并做計(jì)數(shù)。

攔截器原理

在前面的步驟中,不管是同步請(qǐng)求還是異步請(qǐng)求,最終都會(huì)調(diào)用攔截器來(lái)處理網(wǎng)絡(luò)請(qǐng)求。

//RealCall源碼
Response result = getResponseWithInterceptorChain();

這就是OkHttp的核心,Interceptor攔截器。
在OkHttp中,Call、Callback和Dispatcher雖然很有用,但對(duì)于解決復(fù)雜的網(wǎng)絡(luò)請(qǐng)求沒(méi)有太多作用,使用了分層設(shè)計(jì)的攔截器Interceptor才是解決復(fù)雜網(wǎng)絡(luò)請(qǐng)求的核心,這也是OkHttp的核心設(shè)計(jì)。

分層設(shè)計(jì)

我們都知道,真實(shí)情況中的網(wǎng)絡(luò)行為其實(shí)非常復(fù)雜,縱跨軟件、協(xié)議、數(shù)據(jù)包、電信號(hào)、硬件等,所以網(wǎng)絡(luò)層的第一個(gè)基礎(chǔ)知識(shí)就是IOS七層模型,明確了各層的功能范圍,每一層各司其職,層與層依次依賴,實(shí)際上降低了開發(fā)和維護(hù)的難度與成本。

OkHttp也采用了分層設(shè)計(jì)思想,每層Interceptor的輸入都是Request,輸出都是Response,所以可以一層層地加工Request,再一層層地加工Response。

由于各個(gè)Interceptor之間不是組合關(guān)系,不能像ViewTree那樣遞歸調(diào)用,所以需要一個(gè)鏈把這些攔截器全部串起來(lái),為此,入口RealCall會(huì)執(zhí)行網(wǎng)絡(luò)請(qǐng)求的getResponseWithInterceptorChain函數(shù),主要就是一層層地組織Interceptor,組成一個(gè)鏈,然后用chain.proceed去調(diào)用它。

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());//自定義應(yīng)用攔截器
    interceptors.add(retryAndFollowUpInterceptor);//重試/重定向
    interceptors.add(new BridgeInterceptor(client.cookieJar()));//應(yīng)用請(qǐng)求轉(zhuǎn)網(wǎng)絡(luò)請(qǐng)求
    interceptors.add(new CacheInterceptor(client.internalCache()));//緩存
    interceptors.add(new ConnectInterceptor(client));//連接
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//自定義網(wǎng)絡(luò)攔截器
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//服務(wù)端連接

    Interceptor.Chain chain = new RealInterceptorChain(//組成鏈
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);//從RealCall的Request開始鏈?zhǔn)教幚?  }

如何實(shí)現(xiàn)鏈?zhǔn)教幚?/h4>

我們看到,鏈?zhǔn)教幚淼娜肟谑荝ealInterceptorChain的proceed函數(shù):

  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    ...
    RealInterceptorChain next = new RealInterceptorChain(//在chain中前進(jìn)一步
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);//調(diào)用攔截器
    ...
    return response;
  }

而攔截器在執(zhí)行過(guò)程中,會(huì)再調(diào)用chain

  @Override 
  public Response intercept(Chain chain) throws IOException {
  ...
  Response networkResponse = chain.proceed(requestBuilder.build());
  ...

這樣,就形成一個(gè)chain.process(intreceptor)-->interceptor.intercept(chain)-->chainprocess(intreceptor)-->interceptor.intercept(chain)的循環(huán),這個(gè)過(guò)程中,chain不斷消費(fèi),直至最后一個(gè)攔截器,最后這個(gè)攔截器一定是CallServerInterceptor,CallServerInterceptor不再調(diào)用chain.process,鏈?zhǔn)秸{(diào)用結(jié)束。

攔截器的層次設(shè)計(jì)

了解過(guò)攔截器和鏈?zhǔn)椒磻?yīng)的基本原理,我們?cè)賮?lái)看看各攔截器的層次設(shè)計(jì)和具體實(shí)現(xiàn),有很多可以借鑒的地方。
我們先回到RealCall中,看看攔截器的層次和分類:

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());//自定義應(yīng)用攔截器
    interceptors.add(retryAndFollowUpInterceptor);//重試/重定向
    interceptors.add(new BridgeInterceptor(client.cookieJar()));//應(yīng)用請(qǐng)求轉(zhuǎn)網(wǎng)絡(luò)請(qǐng)求
    interceptors.add(new CacheInterceptor(client.internalCache()));//緩存
    interceptors.add(new ConnectInterceptor(client));//連接
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//自定義網(wǎng)絡(luò)攔截器
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//實(shí)現(xiàn)在線網(wǎng)絡(luò)連接

    Interceptor.Chain chain = new RealInterceptorChain(//組成鏈
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);//從RealCall的Request開始鏈?zhǔn)教幚?  }

我們可以看到,OkHttp中攔截器的層次是這樣的:
1.自定義應(yīng)用攔截器
2.重試、重定向攔截器
3.應(yīng)用/網(wǎng)絡(luò)橋接攔截器
4.緩存攔截器
5.連接攔截器
6.自定義網(wǎng)絡(luò)攔截器
7.在線網(wǎng)絡(luò)請(qǐng)求攔截器

我們看到,我們開發(fā)者可以添加兩種自定義Interceptor,一種是client.interceptors()應(yīng)用層攔截器,一種是client.networkInterceptors()網(wǎng)絡(luò)層攔截器。
但其實(shí)這兩種都是Interceptor,為什么可以分成是應(yīng)用層和網(wǎng)絡(luò)層呢?
因?yàn)樵诰W(wǎng)絡(luò)層攔截器上方,是ConnectionInterceptor連接攔截器,這個(gè)攔截器里會(huì)提供Address、ConnectionPool等資源,可以用于處理網(wǎng)絡(luò)連接,networkInterceptors是添加在這之后的,可以參與真正的網(wǎng)絡(luò)層數(shù)據(jù)的處理。
接下來(lái),我們自頂向下,依次看看每層攔截器的實(shí)現(xiàn)

攔截器——自定義應(yīng)用攔截器

OkHttp在最外圍允許添加自定義的應(yīng)用攔截器,我們可以攔截Request和Response,分別進(jìn)行加工,例如在Request時(shí)統(tǒng)一添加Header和Url參數(shù):

Request.Builder builder = chain.request().newBuilder();
builder.addHeader("Accept-Charset", "UTF-8");
builder.addHeader("Accept", " application/json");
builder.addHeader("Content-type", "application/json");

HttpUrl url=builder.build().url().newBuilder()
                  .addQueryParameter("mac", EquipmentUtils.getMac())
                  .build();
Request request = builder.url(url).build();

還可以攔截Response內(nèi)容,打印返回?cái)?shù)據(jù)的日志:

long t1 = System.nanoTime();
Request request = chain.request();
Response response = chain.proceed(request);
long t2 = System.nanoTime();

//直接復(fù)制字節(jié)流,獲取response的數(shù)據(jù)內(nèi)容
BufferedSource sr = response.body().source();
sr.request(Long.MAX_VALUE);
Buffer buf = sr.buffer().clone();//copy副本讀取,不能讀取原文
String content = buf.readString(Charset.forName("UTF-8"));
buf.clear();

Log.i(TAG, "net layer received response of url: " + request.url().url().toString()
          + "\nresponse: " + content
          + "\nspent time: " + (t2 - t1) / 1e6d);

開發(fā)者可以擴(kuò)展針對(duì)請(qǐng)求數(shù)據(jù)和返回?cái)?shù)據(jù),自由開發(fā)功能。

攔截器——重試/重定向

雖然前面有開發(fā)者自定義的應(yīng)用攔截器,但是真正準(zhǔn)備處理網(wǎng)絡(luò)連接,是從OkHttp自己定義的RetryAndFollowUpInterceptor開始的,因?yàn)镺kHttp正是把這個(gè)攔截器作為真正的入口,創(chuàng)建StreamAllocation對(duì)象,在StreamAllocation對(duì)象中準(zhǔn)備了網(wǎng)絡(luò)連接的Address、連接池等資源,后續(xù)的攔截器,使用的都是這個(gè)StreamAllocation對(duì)象。

StreanAllocation

StreamAllocation是OkHttp中用來(lái)定義和傳遞網(wǎng)絡(luò)資源,并建立網(wǎng)絡(luò)連接的對(duì)象,內(nèi)部包含:
Address:規(guī)定如何連接服務(wù)器,包括DNS、協(xié)議、URL等。
Route:存儲(chǔ)建立連接的目標(biāo)IP和端口InetSocketAddress,以及代理服務(wù)器。
ConnectionPool:存儲(chǔ)和復(fù)用已存在的連接,復(fù)用時(shí)根據(jù)Address查找對(duì)應(yīng)的連接。
StreamAllocation會(huì)通過(guò)findConnection創(chuàng)建連接,或復(fù)用已存在的連接,期間會(huì)調(diào)用RealConnection,根據(jù)設(shè)置建立TLS連接、處理握手協(xié)議等,最底層是根據(jù)當(dāng)前運(yùn)行的平臺(tái),直接操作Socket。
每個(gè)Host不超過(guò)5個(gè)連接,每個(gè)連接不超過(guò)5分鐘。

重試/重定向

網(wǎng)絡(luò)環(huán)境本質(zhì)上是不穩(wěn)定的,已建立的連接可能突然不可用,或者連接可用但是服務(wù)器報(bào)錯(cuò),這就需要重試/重定向功能,這也是RetryAndFollowUpInterceptor攔截器的分層功能。
重試
如果整個(gè)鏈?zhǔn)秸{(diào)用出現(xiàn)了RouteException或IOException,就會(huì)調(diào)用recover函數(shù)重新建立連接;
重定向
如果服務(wù)器返回錯(cuò)誤碼如301,要求重定向,就會(huì)調(diào)用followUpRequest函數(shù),新建一個(gè)Request,然后重定向,再走一遍整個(gè)調(diào)用鏈。
while
intercept函數(shù)中的這些主要邏輯都在while(true)循環(huán)中,最大循環(huán)上限是20。

攔截器——應(yīng)用轉(zhuǎn)網(wǎng)絡(luò)的橋接功能

BridgeInterceptor是個(gè)橋梁,這主要是指他會(huì)自動(dòng)處理一些網(wǎng)絡(luò)層特有的Header信息,例如Host屬性,是HTTP1.1必須的,但應(yīng)用層并不關(guān)心這個(gè)屬性,這就是由BridgeInterceptor自動(dòng)處理的。
BridgeInterceptor中處理的Header屬性包括Host、Connection的Keep-Alive、gzip透明壓縮、User-Agent描述、Cookie策略等。
當(dāng)然,因?yàn)镺kHttp采用了外觀模式,所以很多屬性需要通過(guò)client設(shè)置和獲取。

攔截器——緩存功能

在網(wǎng)絡(luò)請(qǐng)求中使用緩存是非常必要提速手段,OkHttp專門用了CacheInterceptor攔截器來(lái)處理這個(gè)功能。
緩存的使用注意包括存儲(chǔ)、查詢和有效性檢查,在OkHttp中:
存儲(chǔ),使用client外觀模式來(lái)設(shè)置存儲(chǔ)Cache數(shù)據(jù)的InternalCache實(shí)現(xiàn)類,在走請(qǐng)求鏈獲取Response時(shí)記錄cache。
查詢,在存儲(chǔ)Cache數(shù)據(jù)的InternalCache實(shí)現(xiàn)類中,根據(jù)Request過(guò)濾,來(lái)查找Cache。
有效性檢查,利用工具類CacheStrategy的getCandidate函數(shù),來(lái)判斷Cache數(shù)據(jù)的各項(xiàng)指標(biāo)是否達(dá)到條件。

攔截器——連接功能

在RetryAndFollowUpInterceptor入口處,我們已經(jīng)分析過(guò),在OkHttp中,連接功能由StreamAlloc實(shí)現(xiàn),提供Address地址、Route路由、RealConnection連接、ConnectionPool線程池復(fù)用、身份驗(yàn)證、協(xié)議、握手、平臺(tái)、安全等功能。

在ConnectionInterceptor這一層,其實(shí)還沒(méi)有真正連接網(wǎng)絡(luò),它的具體功能很簡(jiǎn)單,就是準(zhǔn)備好request請(qǐng)求、streamAllocation連接資源、httpCodec傳輸工具、connection連接,為最底層的網(wǎng)絡(luò)連接服務(wù)。

其中,httpCodec通過(guò)sink提供了OKio封裝過(guò)的基于socket的OutputStream,通過(guò)source提供了OKio封裝的基于socket的InputStream,最終就是通過(guò)這個(gè)sink提交Request,用這個(gè)source獲取Response。

攔截器——自定義網(wǎng)絡(luò)攔截器

主要區(qū)別

自定義的網(wǎng)絡(luò)層攔截器相比應(yīng)用層攔截器,能直接監(jiān)測(cè)到在線網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)交換過(guò)程。
例如,Http有url重定向機(jī)制,如果Http返回碼為301,就需要根據(jù)Header中Location字段的新url,重新發(fā)起一次請(qǐng)求,這樣的話,總共會(huì)有兩次請(qǐng)求。

在應(yīng)用層的攔截器看來(lái),第一次請(qǐng)求并沒(méi)有返回有效數(shù)據(jù),它只會(huì)抓到一次請(qǐng)求,也就是第二次的請(qǐng)求。
但是在網(wǎng)絡(luò)層的攔截器看來(lái),兩次都是網(wǎng)絡(luò)請(qǐng)求,所以它會(huì)抓到兩次請(qǐng)求。

用途擴(kuò)展

根據(jù)網(wǎng)絡(luò)層攔截器的特點(diǎn),我們可以擴(kuò)展如下功能:
1.模擬各種網(wǎng)絡(luò)情況
網(wǎng)絡(luò)接口不只是可用不可用的問(wèn)題,還存在速度波動(dòng)的問(wèn)題,一個(gè)穩(wěn)健的App應(yīng)該能hold住波動(dòng)的甚至是斷斷續(xù)續(xù)的網(wǎng)絡(luò),但是這樣的網(wǎng)絡(luò)非常不好模擬,我們可以在網(wǎng)絡(luò)攔截器層自由設(shè)定網(wǎng)絡(luò)返回值和返回時(shí)間,輔助我們檢查App在處理網(wǎng)絡(luò)數(shù)據(jù)時(shí)的健壯性。
2.模擬多個(gè)備用地址切換
無(wú)論是為了災(zāi)備,還是為了節(jié)省DNS解析時(shí)間,App都會(huì)有多個(gè)備用地址,有些就是ip地址,當(dāng)網(wǎng)絡(luò)出現(xiàn)問(wèn)題時(shí),要自動(dòng)切換到備用地址,就可以在網(wǎng)絡(luò)層模擬出301返回,直接重定向到備用地址。
3.模擬數(shù)據(jù)輔助開發(fā)/測(cè)試
在開發(fā)過(guò)程中,我們可以用gradle多環(huán)境的方法,增加一個(gè)mock的productFlavor,在這個(gè)環(huán)境下添加一個(gè)mockInterceptor,把指向官網(wǎng)的地址重定向?yàn)橹赶蜷_發(fā)測(cè)試網(wǎng)址,甚至直接mock返回?cái)?shù)據(jù),換掉在線數(shù)據(jù),這樣可以檢測(cè)整個(gè)網(wǎng)絡(luò)層的全部功能(編碼、緩存、切換、報(bào)錯(cuò)等),把mock數(shù)據(jù)的內(nèi)容和App的反饋結(jié)合的話,還可以做到針對(duì)網(wǎng)絡(luò)數(shù)據(jù)的半自動(dòng)/自動(dòng)化的測(cè)試驗(yàn)證。

攔截器——在線網(wǎng)絡(luò)請(qǐng)求功能

前面所有的攔截器,都是在準(zhǔn)備或處理網(wǎng)絡(luò)連接前后的數(shù)據(jù),只有CallServerInterceptor這個(gè)攔截器,是真正連接在線服務(wù)的。
它使用ConnectionInterceptor提供的HttpCodec傳輸工具來(lái)發(fā)出Request,獲取Response,然后用ResponseBuilder生成最終的Response,再層層傳遞給外層的攔截器。
HttpCodec本身是一個(gè)接口,實(shí)例是StreamAllocation利用RealConnection生產(chǎn)的,RealConnection根據(jù)連接池中的可用連接,利用Okio生產(chǎn)source和sink:

  private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    rawSocket.setSoTimeout(readTimeout);
    ...
      //用Okio生產(chǎn)
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    ...
  }

Okio的source是socket.inputStream,sink是socket.outputStream。
所以,真正在傳輸數(shù)據(jù)時(shí),就是用Okio的sink去傳socket,用source去取socket,底層其實(shí)也是socket操作。

其他特性

以上是OkHttp的主要內(nèi)容,此外,OkHttp還有一些很有意思的特性。

1.返回?cái)?shù)據(jù)閱后即焚

在OkHttp中,如果要攔截ResponseBody的數(shù)據(jù)內(nèi)容(比如寫日志),會(huì)發(fā)現(xiàn)該數(shù)據(jù)讀過(guò)一次就會(huì)被情況,相當(dāng)于是“閱后即焚”:

  //ResponseBody源碼
  public final String string() throws IOException { //底層不能自己消化異常,應(yīng)該向上層拋出異常
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    //不做catch,異常全部拋出給上層
    } finally { //確保原始字節(jié)數(shù)據(jù)得到處理
      Util.closeQuietly(source); //閱后即焚,這樣可以迅速騰出內(nèi)存空間來(lái)
    }
  }

如果一定要攔截出數(shù)據(jù)內(nèi)容,我們就不能直接讀ResponseBody中的source,需要copy一個(gè)副本才行:

BufferedSource sr = response.body().source();
sr.request(Long.MAX_VALUE);
Buffer buf = sr.buffer().clone();//copy副本讀取,不能讀取原文
String content = buf.readString(Charset.forName("UTF-8"));
buf.clear();

Response也提供了專門獲取ResponsBody數(shù)據(jù)的函數(shù)peekBody,實(shí)現(xiàn)原理也是copy“:

  //Response源碼
  public ResponseBody peekBody(long byteCount) throws IOException {
    BufferedSource source = body.source();
    source.request(byteCount);
    Buffer copy = source.buffer().clone();
    ...
    return ResponseBody.create(body.contentType(), result.size(), result);
  }

參考

深入解析OkHttp3
OkHttp3源碼分析[綜述]
Okhttp-wiki 之 Interceptors 攔截器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容