重識OkHttp——更深入了解如何使用

本文的分析基于OkHttp3.4,不展示完整的代碼示例,具體可以查看這個官方例子或者項(xiàng)目中的samples。

OkHttp作為square公司出品的一個網(wǎng)絡(luò)請求框架,應(yīng)該算是目前Android端最火爆的網(wǎng)絡(luò)框架了。我公司目前的項(xiàng)目中采用的都是Rxjava結(jié)合Retrofit進(jìn)行網(wǎng)絡(luò)請求的處理,對于底層真正實(shí)現(xiàn)網(wǎng)絡(luò)請求的OkHttp關(guān)注的不是很多。最近探究了一下OkHttp的源碼,對OkHttp的使用有了一些新的認(rèn)識,在此做一下總結(jié)。

1 OkHttp的優(yōu)點(diǎn)

OkHttp作為當(dāng)前Android端最火熱的網(wǎng)絡(luò)請求框架,必然有很多的優(yōu)點(diǎn)。

  • 支持HTTP/2 協(xié)議,允許連接到同一個主機(jī)地址的所有請求共享Socket。這必然會提高請求效率。
  • 在HTTP/2協(xié)議不可用的情況下,通過連接池減少請求的延遲。
  • GZip透明壓縮減少傳輸?shù)臄?shù)據(jù)包大小。
  • 響應(yīng)緩存,避免同一個重復(fù)的網(wǎng)絡(luò)請求。

2 網(wǎng)絡(luò)處理3要素

對于客戶端來講,我們關(guān)注的就是把正確的請求發(fā)送到服務(wù)端并拿到結(jié)果來進(jìn)行處理。在OkHttp中,我認(rèn)為可以分為3個部分:

  • Request類封裝客戶端發(fā)送的請求,包括請求的url,請求方法method(主要是GET和POST方法)、請求頭header以及請求體requestBody;
  • Response類封裝了服務(wù)器響應(yīng)的數(shù)據(jù),包括code、message、body、header等。
  • OkHttpClient負(fù)責(zé)發(fā)送請求Request并通過同步或者異步的方式返回服務(wù)器的響應(yīng)Response,就好比是一個瀏覽器。

OkHttp中通過建造者模式來構(gòu)建OkHttpClient、Request和Response。對于客戶端來講,我們不需要過多關(guān)注Response是如何構(gòu)建的,因?yàn)檫@個是OkHttp對響應(yīng)結(jié)果進(jìn)行了封裝處理。我們只關(guān)注請求Request和客戶端OkHttpClient如何構(gòu)建即可。

2.1 請求Request

Request采用建造者模式來配置url,請求方法method、header、tag和cacheControl。

  • 設(shè)置url??梢允荢tring類型、URL類型和HttpUrl類型。最終都是用到HttpUrl類型。
  • 設(shè)置method,包含get、post方法等。默認(rèn)的是get方法。post方法要傳RequestBody,類似的還有delete、put、patch。
  • 設(shè)置header,方法有addHeader(String name, String value)、 removeHeader(String name)、header(String name, String value)、headers(Headers headers)。headers(Headers headers)調(diào)用之后其它的header都會被移除,只添加這一個header。而header(String name, String value)方法調(diào)用之后,其它與這個name同名的header都會被移除,只保留這一個header。
  • 設(shè)置tag,設(shè)置tag可以用來取消這一請求。如果未指定tag或者tag為null,那么這個request本身就會當(dāng)做是一個tag用來被取消請求。
  • 設(shè)置cacheControl,這個是設(shè)置到請求頭中。用來替換其它name是"Cache-Control"的header。如果cacheControl是空的話就會移除請求頭中name是"Cache-Control"的header。
Request.png

OkHttp采用POST方法向服務(wù)器發(fā)送一個請求體,在OkHttp中這個請求體是RequestBody。這個請求體可以是:

  • String類型
  • Stream流類型
  • File文件類型
  • Form表單形式的key-value類型
  • 類似Html文件上傳表單的復(fù)雜請求體類型(多塊請求)。

RequestBody有幾個靜態(tài)方法用于創(chuàng)建不同類型的請求體:

//創(chuàng)建String類型的請求體
public static RequestBody create(MediaType contentType, String content)

//創(chuàng)建文件類型的請求體
 public static RequestBody create(final MediaType contentType, final File file) 

最終都是相當(dāng)于重寫了RequestBody的兩個抽象方法來寫入流,如果傳遞流類型的參數(shù),只要重寫這兩個抽象方法即可。

  //對應(yīng)的是name為Content-Type的header
  public abstract MediaType contentType();

  //這個BufferedSink位于Okio包下,提供高效的寫入。
  public abstract void writeTo(BufferedSink sink) throws IOException;

  //在寫入的時(shí)候可以傳遞內(nèi)容的大小,如果不知道就返回-1即可。
  public long contentLength() throws IOException {  return -1;}

例如,我們提交一個String:

String postBody = ""
        + "Releases\n"
        + "--------\n"
        + "\n"
        + " * _1.0_ May 6, 2013\n"
        + " * _1.1_ June 15, 2013\n"
        + " * _1.2_ August 11, 2013\n";

Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
        .build();

提交File:

File file = new File("README.md");

Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
        .build();

提交流:

RequestBody requestBody = new RequestBody() {
       @Override
        public MediaType contentType() {
             return MEDIA_TYPE_MARKDOWN;
        }

        @Override
         public void writeTo(BufferedSink sink) throws IOException {
              sink.writeUtf8("Numbers\n");
              sink.writeUtf8("-------\n");
              for (int i = 2; i <= 997; i++) {
                     sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
               }
           }

          private String factor(int n) {
                for (int i = 2; i < n; i++) {
                     int x = n / i;
                     if (x * i == n) return factor(x) + " × " + i;
                 }
                return Integer.toString(n);
                }
};

Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(requestBody)
        .build();

對于提交表單和分塊請求,OkHttp提供了兩個RequestBody的子類,FormBodyMultipartBody

2.1.1 表單FormBody

FormBody也是采用建造者模式, 這個很簡單,添加key-value形式的鍵值對即可。
添加鍵值對有兩個方法:

//采用OkHttp默認(rèn)的編碼
public Builder add(String name, String value) 

//采用用戶要求的編碼
public Builder addEncoded(String name, String value)

例如:

  RequestBody formBody = new FormBody.Builder()
                        .add("search", "Jurassic Park")
                        .build();
  Request request = new Request.Builder()
                        .url("https://en.wikipedia.org/w/index.php")
                        .post(formBody)
                        .build();

2.1.2 分塊MultipartBody

MultipartBody也是采用建造者模式,MultipartBody.Builder可以構(gòu)建兼容Html文件上傳表單的復(fù)雜請求體。每一部分的多塊請求體都是它自身的請求體,并且可以定義它自己的請求頭。如果存在的話,這些請求頭用來描述這部分的請求體。例如Content-Disposition、Content-Length 和 Content-Type如果可用就會被自動添加到頭。

MIME類型有:

  public static final MediaType MIXED = MediaType.parse("multipart/mixed");

  public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative");

  public static final MediaType DIGEST = MediaType.parse("multipart/digest");

  public static final MediaType PARALLEL = MediaType.parse("multipart/parallel");

有幾個主要的方法:


  //設(shè)置MIME類型,如MIXED(默認(rèn)的)
    public Builder setType(MediaType type) {}

  //添加請求體
    public Builder addPart(RequestBody body) {
      return addPart(Part.create(body));
    }

  //添加包含header的請求體
    public Builder addPart(Headers headers, RequestBody body) {
      return addPart(Part.create(headers, body));
    }

    //請求體添加表單
    public Builder addFormDataPart(String name, String value) {
      return addPart(Part.createFormData(name, value));
    }

    //請求體中包含文件
    public Builder addFormDataPart(String name, String filename, RequestBody body) {
      return addPart(Part.createFormData(name, filename, body));
    }

    //添加自己定義的part
    public Builder addPart(Part part) {
      if (part == null) throw new NullPointerException("part == null");
      parts.add(part);
      return this;
    }

例如提交一個圖片文件:

RequestBody requestBody = new MultipartBody.Builder()
         .setType(MultipartBody.FORM)
        .addFormDataPart("title", "Square Logo")
        .addFormDataPart("image", "logo-square.png",
         RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
        .build();

 Request request = new Request.Builder()
         .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
         .url("https://api.imgur.com/3/image")
         .post(requestBody)
         .build();

2.2 客戶端OkHttpClient

OkHttpClient采用建造者模式,通過Builder可以配置連接超時(shí)時(shí)間、讀寫時(shí)間,是否緩存、是否重連,還可以設(shè)置各種攔截器interceptor等。
建議在一個App中,OkHttpClient保持一個實(shí)例。一個OkHttpClient支持一定數(shù)量的并發(fā),請求同一個主機(jī)最大并發(fā)是5,所有的并發(fā)最大是64。這個與OkHttp中的調(diào)度器Dispatcher有關(guān),可以設(shè)置并發(fā)數(shù)。本文不對Dispatcher進(jìn)行討論。

OkHttpClient okHttpClient=new OkHttpClient.Builder().build();

//如果不需要我們額外配置,可以使用默認(rèn)的配置
OkHttpClient okHttpClient1 = new OkHttpClient();

一個例子:

int cacheSize = 10 * 1024 * 1024; // 10 MiB
File cacheDirectory = new File(getCacheDir(), "OkHttpCache");
Cache cache = new Cache(cacheDirectory, cacheSize);

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(60, TimeUnit.SECONDS)//連接超時(shí)時(shí)間
        .readTimeout(60, TimeUnit.SECONDS)//讀的時(shí)間
        .writeTimeout(60, TimeUnit.SECONDS)//寫的時(shí)間
        .cache(cache)//配置緩存
        .build();

OkHttpClient支持單獨(dú)配置,例如原來設(shè)置不同的請求時(shí)間,可以通過OkHttpClient的newBuilder()方法來重新構(gòu)造一個OkHttpClient。例如:

OkHttpClient client = new OkHttpClient();

//讀的時(shí)間設(shè)置為500ms
OkHttpClient copy = client.newBuilder()
                          .readTimeout(500, TimeUnit.MILLISECONDS)
                          .build();

//讀的時(shí)間設(shè)置為3000ms
OkHttpClient copy = client.newBuilder()
                          .readTimeout(3000, TimeUnit.MILLISECONDS)
                          .build();

3 同步請求和異步請求

上面已經(jīng)講了如何創(chuàng)建Request和OkHttpClient,剩下的就是發(fā)送請求并得到服務(wù)器的響應(yīng)了。OkHttp發(fā)送請求可分為同步和異步。OkHttpClient首先通過Request構(gòu)建一個Call,通過這個Call去執(zhí)行同步或者異步請求。

#OkHttpClient
public Call newCall(Request request)

同步方式,調(diào)用Call的execute()方法,返回Response,會阻塞當(dāng)前線程:

response = client.newCall(request).execute();

異步方式,調(diào)用Call的enqueue(CallBack callBack)方法,會在另一個線程中返回結(jié)果。

client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
              //處理錯誤的回調(diào)
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
              //處理正確的回調(diào)
            }
        });

4 其他

4.1 配置響應(yīng)緩存

為了緩存響應(yīng),需要一個可讀寫并且設(shè)置大小Size的緩存目錄。緩存目錄需要私有,其它不信任的應(yīng)用不能訪問這個文件。
如果同時(shí)有多個緩存訪問同一個緩存目錄會報(bào)錯。所以最好只在App中初始化一次OkHttpClient,給這個實(shí)例配置緩存,在整個App生命周期內(nèi)都用這一個緩存。否則幾個緩存會相互影響,導(dǎo)致緩存出錯,引起程序崩潰。
響應(yīng)緩存采用Http頭來配置,你可以添加這樣的請求頭Cache-Control: max-stale=3600max-age指的是客戶端可以接收生存期不大于指定時(shí)間(以為單位)的響應(yīng)。
為了防止響應(yīng)使用緩存,可以用CacheControl.FORCE_NETWORK。為了防止使用網(wǎng)絡(luò),采用 CacheControl.FORCE_CACHE。

注意:如果使用FORCE_CACHE禁止使用網(wǎng)絡(luò),而響應(yīng)又沒有緩存存在,OkHttp會報(bào)**504 Unsatisfiable Request **響應(yīng)錯誤。

4.2 取消請求

調(diào)用Call.cancel()方法可以立即取消一個網(wǎng)絡(luò)請求。如果當(dāng)前線程正在寫request或者讀response會報(bào)IO異常。如果不再需要網(wǎng)絡(luò)請求,采用這種方法是比較方便的。例如在App中返回了上一頁。無論是同步還是異步的請求都可以被取消。

4.3 Response讀取響應(yīng)結(jié)果

可以通過Response的code來判斷請求是否成功,如果服務(wù)器返回的有數(shù)據(jù),可以通過Response的body得到一個ResponseBody讀取。
如果采用ResponseBody的string()方法會一次性把數(shù)據(jù)讀取到內(nèi)存中,如果數(shù)據(jù)超過1MB可能會報(bào)內(nèi)存溢出,所以對于超過1MB的數(shù)據(jù),建議采用流的方式去讀取,如ResponseBody的byteStream()方法。

需要說明的是:

  • 如果ResponseBody的內(nèi)容不讀取的話,不會觸發(fā)IO流的讀取操作
  • 內(nèi)容讀取之后,這個body需要關(guān)閉。

5 總結(jié)

OkHttp中的很多類都用到了建造者模式,可以根據(jù)需要靈活配置。采用建造者模式的有:

  • OkHttpClient.Builder
  • Request.Builder
  • FormBody.Builder
  • MultipartBody.Builder
  • Response.Builder

如果單獨(dú)使用OkHttp進(jìn)行網(wǎng)絡(luò)請求,通常需要開發(fā)者自己再封裝一下,如果不想重復(fù)造輪子,Github上面的有一些優(yōu)秀開源庫可以拿來使用(本文只列出star較多的幾個):

參考

OkHttp官方Wiki文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,973評論 19 139
  • 參考Android網(wǎng)絡(luò)請求心路歷程Android Http接地氣網(wǎng)絡(luò)請求(HttpURLConnection) 一...
    合肥黑閱讀 21,352評論 7 63
  • OkHttp使用完全教程 標(biāo)簽 : Http請求, 類庫blog : http://blog.csdn.net/o...
    oncealong閱讀 170,836評論 27 460
  • 1.okhttp介紹: 目前最新android 網(wǎng)絡(luò)請求底部封裝就是okhttp,github鏈接 :https:...
    小夢想家北冥有魚閱讀 1,599評論 0 3
  • MVP+okHttp+Retrofit+RxJava+Glide+Dagger 是現(xiàn)在最流行的一套技術(shù)框架, MV...
    EmanLu閱讀 1,856評論 0 3