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来实现。
开始使用前,请在你工程gradle中配置好OkHttp的依赖,文章写作时官网最新版本是4.2.1,配置代码如下:
implementation("com.squareup.okhttp3:okhttp:4.2.1")
以及在AndroidManifest.xml中添加访问网络权限
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());
}
}
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();
}
});
}
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());
}
}
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());
}
}
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());
}
}
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());
}
}
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());
}
}
我们只要在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());
}
}
使用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();
}
}
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());
}
}
OkHttp2.2以后加入了拦截器,其设计思想可谓是整个框架的精髓,它可以实现网络监听、请求重写、响应重写、请求失败重试等功能。从一个网络请求的发出到响应的过程中间会经历了数个拦截器。
拦截器本质上都是基于Interceptor接口,而我们开发者能够自定义的拦截器有两类,分别是:ApplicationInterceptor(应用拦截器,通过使用addInterceptor方法添加) 和 NetworkInterceptor(网络拦截器,通过使用addNetworkInterceptor方法添加)。一个完整的拦截器结构大概如下图所示:
下面我们以实例的方式来认识拦截的使用。首先定义一个日志拦截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。
拦截器的出现并不是为了如上述给我们提供日志的打印,拦截器还可以在请求前进行添加、移除或者替换请求头。甚至在有请求主体时候,可以改变请求主体。以及在响应后重写响应头并且可以改变它的响应主体(重写响应通常不建议,因为这种操作可能会改变服务端所要传递的响应内容的意图)。实现例如像以下代码,以下拦截器实现了如请求体中不存在"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);
}
}
建议在创建OkHttpClient 实例时,让其是一个单例。因为OkHttpClient 内部存在着相应的连接池和线程池,当多个请求发生时,重用这些资源可以减少延时和节省资源。还要注意的是,每一个Call对象只可能执行一次RealCall,否则程序会发生异常。