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

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

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

1 OkHttp的優(yōu)點

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

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

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

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

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

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

2.1 請求Request

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

  • 設(shè)置url。可以是String類型、URL類型和HttpUrl類型。最終都是用到HttpUrl類型。
  • 設(shè)置method,包含get、post方法等。默認的是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本身就會當做是一個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) 

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

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

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

  //在寫入的時候可以傳遞內(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默認的編碼
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(默認的)
    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è)置各種攔截器interceptor等。
建議在一個App中,OkHttpClient保持一個實例。一個OkHttpClient支持一定數(shù)量的并發(fā),請求同一個主機最大并發(fā)是5,所有的并發(fā)最大是64。這個與OkHttp中的調(diào)度器Dispatcher有關(guān),可以設(shè)置并發(fā)數(shù)。本文不對Dispatcher進行討論。

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

//如果不需要我們額外配置,可以使用默認的配置
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)//連接超時時間
        .readTimeout(60, TimeUnit.SECONDS)//讀的時間
        .writeTimeout(60, TimeUnit.SECONDS)//寫的時間
        .cache(cache)//配置緩存
        .build();

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

OkHttpClient client = new OkHttpClient();

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

//讀的時間設(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,會阻塞當前線程:

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

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

4.2 取消請求

調(diào)用Call.cancel()方法可以立即取消一個網(wǎng)絡(luò)請求。如果當前線程正在寫request或者讀response會報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可能會報內(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

如果單獨使用OkHttp進行網(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)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

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