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方法添加)。一个完整的拦截器结构大概如下图所示:

扫描二维码关注公众号,回复: 10881465 查看本文章

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,否则程序会发生异常。

 

 

 

发布了106 篇原创文章 · 获赞 37 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/102962262