1. 简介
okhttp是我们在Android开发中十分常用的一个网络请求框架。
2. 常用API总结
下面将介绍okhttp3中的一些常用的api(okhttp3完整文档如下,可自行参阅:https://square.github.io/okhttp/3.x/okhttp/)。
2.1 OkHttpClient
OkHttpClient可以理解为是一个构造Call对象的“工厂”。OkHttpClient实例创建方式有两种:
(1)直接通过new的方式实例化:
// The singleton HTTP client.
public final OkHttpClient client = new OkHttpClient();
(2)通过OkHttpClient.Builder构造实例,此种方式支持自定义部分行为:
// The singleton HTTP client.
public final OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor())
.cache(new Cache(cacheDir, cacheSize))
.build();
我们在使用OkHttpClient实例时,最好确保它在整个应用中都是唯一的单例。client实例会持有它自己的连接池和线程池,确保client的全局唯一可以让我们在所有的请求中复用这些连接和线程,这有助于减少内存资源的消耗。
为此,官方还提供了newBuilder()的api来帮助我们创建支持自定义参数的可全局共享的OkHttpClient实例:
OkHttpClient eagerClient = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
Response response = eagerClient.newCall(request).execute();
之后,官方文档中还提到,OkHttp中被挂起的线程和连接都将会在保持空闲时自动回收。如果我们想要主动释放资源,可以使用如下方式,之后所有的Call对象执行请求都将被拒绝。
client.dispatcher().executorService().shutdown();
清理连接池可以使用如下方式(连接池的守护线程可能不会立即退出):
client.connectionPool().evictAll();
如果希望关闭缓存,可以使用如下方式:
client.cache().close();
2.2 Request
Request对象是http请求的抽象(包括请求地址,请求方法,请求头,请求体等内容)。通常情况下,我们可以通过Request.Builder来创建该对象:
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.build();
我们通过request对象的url,method,headers,body等方法能够获取到请求的对应信息。
2.3 Response
Response对象是http响应的抽象(包括响应头,响应体等内容)。
2.4 ResponseBody
ResponseBody抽象的是响应体(包括服务端响应的原始字节信息),我们可以通过response.body()来获取该对象。
需要注意的是,ResponseBody对象必须在我们使用完后关闭。因为每一个ResponseBody都是由有限的资源提供支持,例如套接字(实时网络响应)和打开的文件(用于缓存响应体)。如果没有关闭ResponseBody,将会导致资源的泄露,严重时可能会使得应用变得缓慢或者是崩溃。ResponseBody和Response均实现了closeable接口,我们可以通过如下方法来关闭:
Response.close();
Response.body().close();
Response.body().source().close();
Response.body().charStream().close();
Response.body().byteStream().close();
对于同步调用,我们可以使用try代码块,编译器会帮我们在隐含的finally代码块中调用close方法。例如:
Call call = client.newCall(request);
try (Response response = call.execute()) {
... // Use the response.
}
异步调用中也是类似的:
Call call = client.newCall(request);
call.enqueue(new Callback() {
public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
... // Use the response.
}
}
public void onFailure(Call call, IOException e) {
... // Handle the failure.
}
});
2.5 Call
Call对象抽象的是一个正在准备执行的请求。它代表着一次完整的请求和响应,可以被取消,但是不能被执行两次。
3. 使用案例
以下例子均来源于官网。
3.1 同步Get请求
通过Get请求去服务器上获取helloworld.txt的文本内容:
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
// client.newCall(request)会返回一个Call对象,通过调用Call对象的execute方法就会执行同步请求,并返回一个Response对象
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code: " + response);
}
// 获取响应头的集合
Headers responseHeaders = reponse.headers();
// 遍历并打印响应头的内容
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + " : " + responseHeaders.value(i));
}
// 打印响应体
System.out.println(response.body().string());
}
}
在上面的代码中,我们通过response.body().string()获取到了响应体的完整内容。但是如果在响应体的内容(字节数)比较大的情况下,就不建议采用这种方法来获取,因为response.body().string()会默认将整个响应体内容加载到内存中,此时我们最好使用字节流的形式来读取。
3.2 异步Get请求
在3.1中,我们学会了如何通过okhttp来发送同步请求,不过大家应该都知道,Android中的UI线程是不允许发送同步请求的,因为这会阻塞主线程,从而导致ANR。下面将介绍如何在okhttp中通过异步的方式来发送请求:
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
// 通过调用Call对象的enqueue方法就会执行异步请求,并且会根据请求成功与否执行相应的回调
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 非主线程
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// 非主线程,如果要执行UI操作,需要使用runOnUiThread
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
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(responseBody.string());
}
}
});
}
3.3 请求头和响应头
下面的例子展示了如何设置请求头以及获取响应头:
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
// 请求头的设置
// header()会覆盖之前已设置的同名请求头,而addHeader()不会
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
// response.header(headerName)获取的是响应头对应字段的最新值
System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
// 获取响应头集合
System.out.println("Vary: " + response.headers("Vary"));
}
}
3.4 Post请求(提交字符串)
下面的例子中通过Post请求提交了一串字符串到服务端:
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
需要注意的是,整个请求体对象Request都是在内存中的,所以要避免提交的字符串内容过多(不要超过1MB)。
3.5 Post请求(以字节流的形式提交请求体)
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
3.6 Post请求(提交文件)
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
3.7 Post请求(以表单形式提交参数)
通过FormBody.Builder我们可以构建表单参数形式的请求体(类似于HTML中的<form>标签),键值对将以兼容HTML表单的方式进行编码。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
此种方式Post的请求头中Content-Type字段的值是:application/x-www-form-urlencoded。
3.8 Post请求(以MultiPart的格式提交文件,分块请求)
/**
* The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
* these examples, please request your own client ID! https://api.imgur.com/oauth2
*/
private static final String IMGUR_CLIENT_ID = "...";
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
// Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
此种方式Post的请求头中Content-Type字段的值是:multipart/form-data,常用于以表单形式上传文件。详细区别可以参见这篇博客:深入解析 multipart/form-data。
3.9 取消一个请求
通过调用Call对象的cancel方法可以立刻停止一个进行中的请求,如果某个线程当前正在执行请求或读取响应,它将会收到一个IOException的异常。
无论是同步请求还是异步请求都能够被取消,在恰当的时候取消请求可以让我们节省网络资源。
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build();
final long startNanos = System.nanoTime();
final Call call = client.newCall(request);
// Schedule a job to cancel the call in 1 second.
executor.schedule(new Runnable() {
@Override public void run() {
System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
call.cancel();
System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
}
}, 1, TimeUnit.SECONDS);
System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
try (Response response = call.execute()) {
System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
(System.nanoTime() - startNanos) / 1e9f, response);
} catch (IOException e) {
System.out.printf("%.2f Call failed as expected: %s%n",
(System.nanoTime() - startNanos) / 1e9f, e);
}
}
3.10 超时设置
okhttp请求超时设置方法如下:
private final OkHttpClient client;
public ConfigureTimeouts() throws Exception {
client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build();
try (Response response = client.newCall(request).execute()) {
System.out.println("Response completed: " + response);
}
}
4. 参考文章
官方文档:https://square.github.io/okhttp/
OkHttp使用完全教程:https://www.jianshu.com/p/ca8a982a116b
Okhttp3基本使用:https://www.jianshu.com/p/da4a806e599b