转载注明出处:简书-十个雨点
上一篇中,我们学习了如何使用OkHttp进行Get和Post操作,本篇将教你如何对网络请求和应答进行更多定制。
Call call = new OkHttpClient().newCall(new Request.Builder().build());
Response response=call.execute();
上面一行代码中展示了使用OkHttp发送一次请求涉及的类,我们来一一解析。
OkHttpClient
OkHttpClient是OkHttp的核心功能的执行者,可以通过
OkHttpClient client=new OkHttpClient();
来创建默认的OkHttpClient对象,也可以使用:
OkHttpClient client=new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS) //设置连接超时
.readTimeout(60, TimeUnit.SECONDS) //设置读超时
.writeTimeout(60,TimeUnit.SECONDS) //设置写超时
.retryOnConnectionFailure(true) //是否自动重连
.build(); //构建OkHttpClient对象
来构建自定义的OkHttpClient 对象,上面代码只给出了常用的设置项,其他设置项比较复杂,我们下篇再说。
使用OkHttpClient 的时候需要注意以下几点:
- 最好只使用一个共享的OkHttpClient 实例,将所有的网络请求都通过这个实例处理。因为每个OkHttpClient 实例都有自己的连接池和线程池,重用这个实例能降低延时,减少内存消耗,而重复创建新实例则会浪费资源。
- OkHttpClient的线程池和连接池在空闲的时候会自动释放,所以一般情况下不需要手动关闭,但是如果出现极端内存不足的情况,可以使用以下代码释放内存:
client.dispatcher().executorService().shutdown(); //清除并关闭线程池
client.connectionPool().evictAll(); //清除并关闭连接池
client.cache().close(); //清除cache
- 如果对一些请求需要特殊定制,可以使用
OkHttpClient eagerClient = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
这样创建的实例与原实例共享线程池、连接池和其他设置项,只需进行少量配置就可以实现特殊需求。
Request
Request是网络请求的对象,其本身的构造函数是private的,只能通过Request.Builder来构建。下面代码展示了常用的设置项。
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues") //设置访问url
.get() //类似的有post、delete、patch、head、put等方法,对应不同的网络请求方法
.header("User-Agent", "OkHttp Headers.java") //设置header
.addHeader("Accept", "application/json; q=0.5") //添加header
.removeHeader("User-Agent") //移除header
.headers(new Headers.Builder().add("User-Agent", "OkHttp Headers.java").build()) //移除原有所有header,并设置新header
.addHeader("Accept", "application/vnd.github.v3+json")
.build(); //构建request
RequestBody
在Request中使用post、patch等方法时,需要传入一个RequestBody参数,除了上一节讲到的构造方法外,RequestBody还有两个子类:FormBody和MultipartBody。
FromBody用于提交表单键值对,其作用类似于HTML中的<form>标记。
RequestBody formBody = new FormBody.Builder() //提交表单键值对
.add("platform", "android") //添加键值对
.add("name", "XXX")
.add("subject", "Hello")
.addEncoded(URLEncoder.encode("详细","GBK"), //添加已编码的键值对
URLEncoder.encode("无","GBK"))
.add("描述","你好") //其实会自动编码,但是无法控制编码格式
.build();
使用MultipartBody.Builder可以构建与HTML文件上传格式兼容的复杂请求体。多块请求体中每块请求都是一个独立的请求体,都可以定义自己的请求头。这些请求头应该用于描述对应的请求体,例如Content-Disposition,Content-Length,和Content-Type会自动被添加到请求头中。
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"title\""),
RequestBody.create(null, "Logo"))
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"image\""),
RequestBody.create(MediaType.parse("image/png"), new File("website/static/logo.png")))
.addFormDataPart("discription","beautiful")
.build();
了上面这些现成的类和方法以外,还可以用继承RequestBody的方式自定义实现,比如下例所示是以流的方式POST提交RequestBody,其中BufferedSink是Okio的API,可以使用BufferedSink.outputStream()来得到OutputStream。
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();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
Call
Call对象表示一个已经准备好可以执行的请求,用这个对象可以查询请求的执行状态,或者取消当前请求。它具有以下方法:
Call call=client.newCall(request); //获取Call对象
Response response=call.execute(); //同步执行网络请求,不要在主线程执行
call.enqueue(new Callback()); //异步执行网络请求
call.cancel(); //取消请求
call.isCanceled(); //查询是否取消
call.isExecuted(); //查询是否被执行过
要注意的是,每个Call对象只能执行一次请求。如果想重复执行相同的请求,可以:
Call reCall=client.newCall(call.request()); //获取另一个相同配置的Call对象
Response
Response是网络请求的结果下面是一些常用方法:
Response response=call.execute(); //获取Response对象
response.code(); //请求的状态码
response.isSuccessful(); //如果状态码为[200..300),则表明请求成功
Headers headers=response.headers(); //获取响应头
List<String> names=response.headers("name"); //获取响应头中的某个字段
ResponseBody body=response.body(); //获取响应体
其中ResponseBody代表响应体,用于操作网络请求返回的内容。常用方法如下:
body.contentLength(); //body的长度
String content=body.string(); //以字符串形式解码body
byte[] byteContent=body.bytes(); //以字节数组形式解码body
InputStreamReader reader=body.charStream(); //将body以字符流的形式解码
InputStream inputStream=body.byteStream(); //将body以字节流的形式解码
ResponseBody还有一些注意事项:
- ResponseBody必须关闭,不然可能造成资源泄漏,你可以通过以下方法关闭ResponseBody,对同一个ResponseBody只要关闭一次就可以了。
Response.close();
Response.body().close();
Response.body().source().close();
Response.body().charStream().close();
Response.body().byteString().close();
Response.body().bytes();
Response.body().string();
ResponseBody只能被消费一次,也就是string(),bytes(),byteStream()或 charStream()方法只能调用其中一个。
如果ResponseBody中的数据很大,则不应该使用bytes() 或 string()方法,它们会将结果一次性读入内存,而应该使用byteStream()或 charStream(),以流的方式读取数据。