網絡請求框架OkHttp3全解系列 - (一)OkHttp的基本使用

預備知識:
HTTP協議詳解
HTTP請求報文和響應報文

OkHttp3是由square公司開發,Android中公認最好用的網絡請求框架,在接口封裝上做的簡單易用,GitHub地址

它有以下默認特性:

  • 支持HTTP/2,允許所有同一個主機地址的請求共享同一個socket連接
  • 使用連接池減少請求延時
  • 透明的GZIP壓縮減少響應數據的大小
  • 緩存響應內容,避免一些完全重複的請求

當網絡出現問題的時候OkHttp 會自動恢復一般的連接問題,如果你的服務有多個IP地址,當第一個IP請求失敗時,OkHttp會交替嘗試你配置的其他IP。

一、引入

gradle引入依賴即可。

    implementation 'com.squareup.okhttp3:okhttp:3.14.7'
    implementation 'com.squareup.okio:okio:1.17.5'

3.14.x版本及以前的版本,採用Java語言編寫,4.0.0以後採用kotlin語言;本系列文章中源碼引自3.14.x版本,以Java語言講解。

其中Okio庫 是對Java.io和java.nio的補充,以便能夠更加方便,快速的訪問、存儲和處理你的數據。OkHttp的底層使用該庫作爲支持。

另外,別忘了申請網絡請求權限,如果還使用網絡請求的緩存功能,那麼還要申請讀寫外存的權限:

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

二、使用方式

基本使用步驟如下

  • 構建客戶端對象OkHttpClient
  • 構建請求Request
  • 生成Call對象
  • Call發起請求(同步/異步)

下面跟着具體使用實例,詳細介紹。

2.1 get請求

以百度主頁爲例,進行Get請求:

        OkHttpClient httpClient = new OkHttpClient();

        String url = "https://www.baidu.com/";
        Request getRequest = new Request.Builder()
                .url(url)
                .get()
                .build();

        Call call = httpClient.newCall(getRequest);

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //同步請求,要放到子線程執行
                    Response response = call.execute();
                    Log.i(TAG, "okHttpGet run: response:"+ response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

首先,創建了OkHttpClient實例,接着用Request.Builder構建了Request實例並傳入了百度主頁的url,然後httpClient.newCall方法傳入Request實例生成call,最後在子線程調用call.execute()執行請求獲得結果response。

所以,使用OkHttp進行get請求,是比較簡單的,只要在構建Request實例時更換url就可以了。

有個問題,你可能注意到了,這裏是放在子線程執行請求的,這是因爲call.execute()是同步方法。想要在主線程直接使用而不用手動創建子線程可以嘛?當然可以,使用call.enqueue(callback)即可:

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.i(TAG, "okHttpGet enqueue: onResponse:"+ response.body().string());
            }
        });

call.enqueue會異步執行,需要注意的是,兩個回調方法onFailure、onResponse是執行在子線程的,所以如果想要執行UI操作,需要使用Handler切換到UI線程。

另外,注意每一個Call只能執行一次(原因會在下篇流程分析中說明)。

執行後結果打印如下:

2020-05-04 21:52:56.446 32681-3631/com.hfy.androidlearning I/OkHttpTestActivity: okHttpGet run: response:<!DOCTYPE html>
    <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新聞</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地圖</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>視頻</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>貼吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登錄</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登錄</a>');
                    </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多產品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>關於百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必讀</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意見反饋</a>&nbsp;京ICP證030173號&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>

可見百度首頁的get請求成功響應了。

2.2 post請求

2.2.1 post請求提交String、文件

post請求與get請求的區別是 在構造Request對象時,需要多構造一個RequestBody對象,用它來攜帶我們要提交的數據。示例如下:

        OkHttpClient httpClient = new OkHttpClient();

        MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
        String content = "hello!";
        RequestBody body = RequestBody.create(contentType, content);

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

        Call call = httpClient.newCall(getRequest);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
            }
        });

對比get請求,把構建Request時的get()改成post(body),並傳入RequestBody實例。RequestBody實例是通過create方法創建,需要指定請求體內容類型、請求體內容。這裏是傳入了一個指定爲markdown格式的文本。

結果打印如下:

2020-05-05 13:18:26.445 13301-13542/com.hfy.androidlearning I/OkHttpTestActivity: okHttpPost enqueue: 
     onResponse:Response{protocol=http/1.1, code=200, message=OK, url=https://api.github.com/markdown/raw}
     body:<p>hello!</p>

請求成功並把請求體內容又返回來了。

傳入RequestBody的 MediaType 還可以是其他類型,如客戶端要給後臺發送json字符串、發送一張圖片,那麼可以定義爲:

// RequestBody:jsonBody,json字符串
String json = "jsonString";
RequestBody jsonBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);

//RequestBody:fileBody, 上傳文件
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
RequestBody fileBody = RequestBody.create(MediaType.parse("image/png"), file);

MediaType更多類型信息可以查看 RFC 2045

2.2.2 post請求提交表單

構建RequestBody除了上面的方式,還有它的子類FormBody,FormBody用於提交表單鍵值對,這種能滿足平常開發大部分的需求。

//RequestBody:FormBody,表單鍵值對
RequestBody formBody = new FormBody.Builder()
        .add("username", "hfy")
        .add("password", "qaz")
        .build();

FormBody是通過FormBody.Builder用構建者模式創建,add鍵值對即可。它的contentType在內部已經指定了。

  private static final MediaType CONTENT_TYPE = MediaType.get("application/x-www-form-urlencoded");

2.2.2 post請求提交複雜請求體

RequestBody另一個子類MultipartBody,用於post請求提交複雜類型的請求體。複雜請求體可以同時包含多種類型的的請求體數據。

上面介紹的 post請求 string、文件、表單,只有單一類型。考慮一種場景–註冊場景,用戶填寫完姓名、電話,同時要上傳頭像圖片,這時註冊接口的請求體就需要 接受 表單鍵值對 以及文件了,那麼前面講的的post就無法滿足了。那麼就要用到MultipartBody了。 完整代碼如下:

        OkHttpClient httpClient = new OkHttpClient();

//        MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
//        String content = "hello!";
//        RequestBody body = RequestBody.create(contentType, content);

        //RequestBody:fileBody,上傳文件
        File file = drawableToFile(this, R.mipmap.bigpic, new File("00.jpg"));
        RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpg"), file);


        //RequestBody:multipartBody, 多類型 (用戶名、密碼、頭像)
        MultipartBody multipartBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("username", "hufeiyang")
                .addFormDataPart("phone", "123456")
                .addFormDataPart("touxiang", "00.png", fileBody)
                .build();


        Request getRequest = new Request.Builder()
                .url("http://yun918.cn/study/public/file_upload.php")
                .post(multipartBody)
                .build();

        Call call = httpClient.newCall(getRequest);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

                Log.i(TAG, "okHttpPost enqueue: \n onFailure:"+ call.request().toString() +"\n body:" +call.request().body().contentType()
                +"\n IOException:"+e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
            }
        });

可見,在構建RequestBody時是使用MultipartBody.Builder構建了MultipartBody實例,通過addFormDataPart方法傳入了姓名、電話的鍵值對,也通過addFormDataPart(“touxiang”, “00.png”, fileBody)傳入了頭像圖片,其中"touxiang"是key值, "00.png"是文件名,fileBody是要以上傳的圖片創建的RequestBody。
因爲所有數據都是以鍵值對的表單形式提交,所以要設置setType(MultipartBody.FORM)。

請求抓包結果:
在這裏插入圖片描述
可見請求體重確實包含了姓名、電話、頭像,並且注意到Content-Type值是 multipart/form-data。響應是200,說明請求成功了。

其他請求方式像put、header、delete,主要在構建Request時把get()或post()換成put()、header()、delete()就可以了,但一般在Android端很少用到。

2.4 請求配置項

先看幾個問題:

  1. 如何全局設置超時時長?
  2. 緩存位置、最大緩存大小 呢?
  3. 考慮有這樣一個需求,我要監控App通過 OkHttp 發出的 所有 原始請求,以及整個請求所耗費的時間,如何做?

這些問題,在OkHttp這裏很簡單。把OkHttpClient實例的創建,換成以下方式即可:

        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(15, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .cache(new Cache(getExternalCacheDir(),500 * 1024 * 1024))
                .addInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {
                        Request request = chain.request();
                        String url = request.url().toString();
                        Log.i(TAG, "intercept: proceed start: url"+ url+ ", at "+System.currentTimeMillis());
                        Response response = chain.proceed(request);
                        ResponseBody body = response.body();
                        Log.i(TAG, "intercept: proceed end: url"+ url+ ", at "+System.currentTimeMillis());
                        return response;
                    }
                })
                .build();

這裏通過OkHttpClient.Builder通過構建者模式設置了連接、讀取、寫入的超時時長,用cache()方法傳入了由緩存目錄、緩存大小構成的Cache實例,這樣就解決了前兩個問題。

還注意到,使用addInterceptor()方法添加了Interceptor實例,且重寫了intercept方法。Interceptor意爲攔截器,intercept()方法會在開始執行請求時調用。其中chain.proceed(request)內部是真正請求的過程,是阻塞操作,執行完後會就會得到請求結果ResponseBody,所以chain.proceed(request)的前後取當前時間,那麼就知道整個請求所耗費的時間。上面chain.proceed(request)的前後分別打印的日誌和時間,這樣第三個問題也解決了。

具體Interceptor是如何工作,會在下一篇流程分析中介紹。

另外,通常OkHttpClient實例是全局唯一的,這樣這些基本配置就是統一,且內部維護的連接池也可以有效複用(會在下一篇流程分析中介紹)。

全局配置的有了,單個請求的也可以有一些單獨的配置。

        Request getRequest = new Request.Builder()
                .url("http://yun918.cn/study/public/file_upload.php")
                .post(multipartBody)
                .addHeader("key","value")
                .cacheControl(CacheControl.FORCE_NETWORK)
                .build();

這個Request實例,

  • 使用addHeader()方法添加了請求頭。
  • 使用cacheControl(CacheControl.FORCE_NETWORK)設置此次請求是能使用網絡,不用緩存。(還可以設置只用緩存FORCE_CACHE。)

感謝
Okhttp3基本使用
OkHttp使用詳解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章