Android網絡編程(九) 之 OkHttp3框架的使用

1 簡介

OkHttp是一個用於Android網絡請求的第三方開源的輕量級框架。該框架由移動支付Square公司貢獻,其優勢有支持HTTP/2,允許連接到同一個主機地址的所有請求共享一個Socket連接;若HTTP/2不可用情況下,還可通過連接池的設計減少請求延遲;自動處理GZip壓縮節省響應數據大小;支持緩存響應請求數據避免重複請求等。

其實我們在上一篇文章《Android網絡編程(八) 之 HttpURLConnection原理分析》中就已經暴光過okhttp框架了。因爲HttpURLConnection裏針對http和https協議的處理底層就是通過OkHttp來完成的,當時提到其源碼下載地址可訪問:https://android.googlesource.com/platform/external/okhttp 進行下載。實際上,OkHttp也有自己的官網:https://square.github.io/okhttp。OkHttp第3個版本,也就是我們常提到的OkHttp3是一個里程碑版本,儘管目前最新版本已升級至4.X,但其內部還是保持着與OkHttp3.X的嚴格兼容,甚至包名仍然是okhttp3。還值得一提的,在OkHttp 4.0.0 RC 3版本後它的實現語言從Java變成了Kotlin來實現。

2 快速上手

開始使用前,請在你工程gradle中配置好OkHttp的依賴,文章寫作時官網最新版本是4.2.1,配置代碼如下:

implementation("com.squareup.okhttp3:okhttp:4.2.1")

以及在AndroidManifest.xml中添加訪問網絡權限

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

2.1 同步Get請求

private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void syncGet() throws Exception {
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .get()
            .build();

    Call call = mOkHttpClient.newCall(request);
    Response response = call.execute();
    if (response.isSuccessful()) {
        Headers responseHeaders = response.headers();
        for (int i = 0; i < responseHeaders.size(); i++) {
            System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
        }
        System.out.println(response.body().string());
    }
}

2.2 異步Get請求

private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void asyncGet() {
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .get()
            .build();

    Call call = mOkHttpClient.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            if (response.isSuccessful()) {
                Headers responseHeaders = response.headers();
                for (int i = 0, size = responseHeaders.size(); i < size; i++) {
                    System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
                }
                System.out.println(response.code());
                System.out.println(response.message());
                System.out.println(response.body().string());
            }
        }
        @Override
        public void onFailure(Call call, IOException e) {
            e.printStackTrace();
        }
    });
}

2.3 Post提交字符串

private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void postString() throws Exception {
    String postBody = "Hello world!";
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .post(RequestBody.create(MediaType.parse("text/x-markdown; charset=utf-8"), postBody))
            .build();

    Response response = mOkHttpClient.newCall(request).execute();
    if (response.isSuccessful()) {
        System.out.println(response.body().string());
    }
}

2.4 Post提交流

private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void PostStreaming() throws Exception {
    RequestBody requestBody = new RequestBody() {
        @Override public MediaType contentType() {
            return MediaType.parse("text/x-markdown; charset=utf-8");
        }
        @Override public void writeTo(BufferedSink sink) throws IOException {
            sink.writeUtf8("Hello \n");
            sink.writeUtf8("world");
        }
    };

    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .post(requestBody)
            .build();

    Response response = mOkHttpClient.newCall(request).execute();
    if (response.isSuccessful()) {
        System.out.println(response.body().string());
    }
}

2.5 Post提交文件

private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void PostFile() throws Exception {
    File file = new File("README.md");
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .post(RequestBody.create(MediaType.parse("text/x-markdown; charset=utf-8"), file))
            .build();
    Response response = mOkHttpClient.newCall(request).execute();
    if (response.isSuccessful()) {
        System.out.println(response.body().string());
    }
}

2.6 Post提交表單

private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void PostForm() throws Exception {
    RequestBody formBody = new FormBody.Builder()
            .add("name", "zyx")
            .build();
    Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .post(formBody)
            .build();
    Response response = mOkHttpClient.newCall(request).execute();
    if (response.isSuccessful()) {
        System.out.println(response.body().string());
    }
}

2.7 Post複雜請求體

MultipartBody就是可以構建與HTML文件上傳表單形式兼容的複雜的請求體。multipart請求體的每一部分本身就是請求體,並且可以定義自己的頭部。這些請求頭可以用來描述的部分請求體,如它的 Content-Disposition 。如果 Content-Length 和 Content-Type 可用的話,則會自動添加。

private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void postMultipart() throws Exception {
    RequestBody requestBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("title", "Square Logo")
            .addFormDataPart("image", "logo-square.png", RequestBody.create(MediaType.parse("image/png"), new File("website/static/logo-square.png")))
            .build();

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

    Response response = mOkHttpClient.newCall(request).execute();
    if (response.isSuccessful()) {
        System.out.println(response.body().string());
    }
}

2.8 緩存響應和超時時間

我們只要在OkHttpClient創建時通過cache方法傳入一個Cache類對象,便可使請求支持緩存。而Cache對象的創建就是要傳入可進行讀寫緩存目錄和(一般應該是私自有目錄)一個緩存大小限制值即可。而connectTimeout、writeTimeout和readTimeout方法可設置訪問的連接、寫、讀的超時時間。

public void responseCaching(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    OkHttpClient client = new OkHttpClient.Builder()
            .cache(cache)
            .connectTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build();

    Request request = new Request.Builder()
            .url("http://publicobject.com/helloworld.txt")
            .build();


    Response response1 = client.newCall(request).execute();
    if (response1.isSuccessful()) {
        System.out.println(response1.body().string());
        System.out.println("Response 1 cache response:    " + response1.cacheResponse());
        System.out.println("Response 1 network response:  " + response1.networkResponse());
    }

    Response response2 = client.newCall(request).execute();
    if (response2.isSuccessful()) {
        System.out.println(response2.body().string());
        System.out.println("Response 2 cache response:    " + response2.cacheResponse());
        System.out.println("Response 2 network response:  " + response2.networkResponse());
    }
}

2.9 取消請求

使用Callcancel()方法可立即停止正在進行的Call。如果一個線程目前正在寫請求或讀響應,它還會收到一個IOException異常,其異常信息如:java.io.IOException: Canceled

private final OkHttpClient mOkHttpClient = new OkHttpClient();
public void syncGet() throws Exception {
    Request request = new Request.Builder()
            .url("http://httpbin.org/delay/2")
            .get()
            .build();

    final Call call = mOkHttpClient.newCall(request);

    // 將其取消
    new Thread(new Runnable() {
        @Override
        public void run() {
            call.cancel();
        }
    }).start();

    try {
        Response response = call.execute();
        if (response.isSuccessful()) {
            System.out.println(response.body().string());
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2.10 認證處理

OkHttp會自動重試未驗證的請求. 當響應是401 Not Authorized時,Authenticator會被要求提供證書. Authenticator的實現中需要建立一個新的包含證書的請求. 如果沒有證書可用, 返回null來跳過嘗試.

使用Response.challenges()來獲得任何authentication challenges的 schemes 和 realms. 當完成一個Basic challenge, 使用Credentials.basic(username, password)來解碼請求頭.

public void syncGet() throws Exception {
    OkHttpClient client = new OkHttpClient.Builder()
            .authenticator(new Authenticator() {
                @Override
                public Request authenticate(Route route, Response response) throws IOException {
                    if (response.request().header("Authorization") != null) {
                        return null; // Give up, we've already attempted to authenticate.
                    }

                    System.out.println("Authenticating for response: " + response);
                    System.out.println("Challenges: " + response.challenges());
                    String credential = Credentials.basic("jesse", "password1");
                    return response.request().newBuilder()
                            .header("Authorization", credential)
                            .build();
                }
            })
            .build();

    Request request = new Request.Builder()
            .url("http://publicobject.com/secrets/hellosecret.txt")
            .get()
            .build();

    Call call = client.newCall(request);
    Response response = call.execute();
    if (response.isSuccessful()) {
        System.out.println(response.body().string());
    }
}

3 攔截器

OkHttp2.2以後加入了攔截器,其設計思想可謂是整個框架的精髓,它可以實現網絡監聽、請求重寫、響應重寫、請求失敗重試等功能。從一個網絡請求的發出到響應的過程中間會經歷了數個攔截器。

攔截器本質上都是基於Interceptor接口,而我們開發者能夠自定義的攔截器有兩類,分別是:ApplicationInterceptor(應用攔截器,通過使用addInterceptor方法添加) 和 NetworkInterceptor(網絡攔截器,通過使用addNetworkInterceptor方法添加)。一個完整的攔截器結構大概如下圖所示:

3.1 攔截器的選擇

下面我們以實例的方式來認識攔截的使用。首先定義一個日誌攔截LoggingInterceptor,然後通過在創建OkHttpClient對象時,分別使用addInterceptor方法和addNetworkInterceptor方法將該日誌攔截器添加到不同的位置。

    class LoggingInterceptor implements Interceptor {
        @Override public Response intercept(Interceptor.Chain chain) throws IOException {
            // 請求
            Request request = chain.request();
            long t1 = System.nanoTime();
            System.out.println(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers()));

            // 響應
            Response response = chain.proceed(request);
            long t2 = System.nanoTime();
            System.out.println(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers()));

            return response;
        }
    }

    public void syncGet() throws Exception {
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor())
//                .addNetworkInterceptor(new LoggingInterceptor())
                .build();

        Request request = new Request.Builder()
                .url("http://www.publicobject.com/helloworld.txt")
                .header("User-Agent", "OkHttp Example")
                .get()
                .build();

        Call call = client.newCall(request);
        Response response = call.execute();
        if (response.isSuccessful()) {
            System.out.println(response.body().string());
        }
    }

來看使用addInterceptor輸出的結果:

Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example

Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

再來使用addNetworkInterceptor輸出的結果:

Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt

Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

因爲URL:http://www.publicobject.com/helloworld.txt 最終會重定向到 https://publicobject.com/helloworld.txt。能看出使用addInterceptor添加的ApplicationInterceptor輸出的信息只有初次請求和最終響應,而使用addNetworkInterceptor添加的NetworkInterceptor輸出的信息包含了初次的請求、初次的響應,重定向後的請求和重定向後的響應。

所以,我們總結ApplicationInterceptor和NetworkInterceptor在使用上的選擇,若不關心中間過程,只需最終結果的攔截使用ApplicationInterceptor即可;如果需要攔截請求過程中的中間響應,那麼就需要使用NetworkInterceptor。

3.2 重寫請求和重寫響應

攔截器的出現並不是爲了如上述給我們提供日誌的打印,攔截器還可以在請求前進行添加、移除或者替換請求頭。甚至在有請求主體時候,可以改變請求主體。以及在響應後重寫響應頭並且可以改變它的響應主體(重寫響應通常不建議,因爲這種操作可能會改變服務端所要傳遞的響應內容的意圖)。實現例如像以下代碼,以下攔截器實現瞭如請求體中不存在"Content-Encoding",則給它添加經過壓縮之後的請求主體。

final class GzipRequestInterceptor implements Interceptor {
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request originalRequest = chain.request();
        if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
            return chain.proceed(originalRequest);
        }

        Request compressedRequest = originalRequest.newBuilder()
                .header("Content-Encoding", "gzip")
                .method(originalRequest.method(), gzip(originalRequest.body()))
                .build();
        return chain.proceed(compressedRequest);
    }
}

4 使用建議

建議在創建OkHttpClient 實例時,讓其是一個單例。因爲OkHttpClient 內部存在着相應的連接池和線程池,當多個請求發生時,重用這些資源可以減少延時和節省資源。還要注意的是,每一個Call對象只可能執行一次RealCall,否則程序會發生異常。

 

 

 

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