OkHttp 有以下特点:
- HTTP / 2支持允许同一主机的所有请求共享套接字。
- 连接池减少请求延迟(如果HTTP / 2不可用)。
- 透明GZIP缩小下载大小。
- 响应缓存可以避免重复请求的网络。
ok,我们开始来探究一下 OkHttp 的使用。
首先添加依赖
compile 'com.squareup.okhttp3:okhttp:3.8.0'
OkHttp 发起网络请求非常简单,具体步骤如下:
- 构建 OkHttpClient 客户端
- 构建 Request 请求
- 创建 Call 对象,发起网络请求
- 处理 Response 响应结果
这么看来,只要搞懂这几个关键类,跟调用请求的过程。对于 OkHttp 的基本使用就没问题了。
一、Request
Request 对应于 Http 的请求报文,查看源码可以看到,Request 封装了请求报文的一些字段。
发起网络请求必然要配置这些请求报文字段,Request
采用 Builder 模式,通过 Builder 内部类使我们灵活去配置 Request
对象,默认初始化Request request = new Request.Builder().build();
是配置了请求头,请求方法GET
,也可以在调用build()
方法之前调用对应方法进行配置。如果需要对 其中的某个字段做特殊配置,可以Request.Builder builder = request.newBuilder()
拿到 Builder 对象进行 "返厂重建"。
- Http 请求地址
url
- 请求方法,例如(GET,POST等等),配置请求方法时调用
get()
,post()
等,内部都是调用method()
方法传入请求方法与请求体,并对是否需要请求体进行判断。 Headers
:维护 Http 消息的头字段,内部也是通过 Builder 模式构建字符串键值对来维护消息头。Request
初始化时默认构造了Headers
的 Builder 实例,我们初始化Request
可以调用header()
方法设置请求头的name
,value
字段。其实内部是调用了Headers
的 Builder 实例的set()
方法设置的,这种方法设置的head
表示name
是唯一的。因为设置之前内部调用了removeAll()
方法先移除了相同的name
对应的value
。- 如果想添加多个
name
相同的请求头,可以调用addHeader()
方法进行设置,其实内部是调用了Headers
的 Builder 实例的add()
方法设置的 - 也可以调用
removeHeader()
方法移除同一个name
的所有请求头 - 可以通过
Request
的head()
方法获得指定请求头,·其实内部也是通过Headers
的get()
方法通过name
获取的value
,headers()
方法获得同一个name
的所有value
值,以list
形式返回
- 请求体
RequestBody
(一般用来向服务器提交数据请求时用到) tag
,设置请求标签,用于取消网络请求CacheControl
用于控制缓存使用。
二、构建 RequestBody
用于携带数据请求服务器,一般就是在特定的请求才会携带请求头,例如 POST
,PUT
等。查看 RequestBody
源码可以看到有五个 static 方法用于创建 RequestBody
实例,第一个参数都是 MediaType
表示数据的 MIME 类型,第二个参数分别可选为:String
,File
,byte[]
或者ByteString
,也就是提交的数据内容。
MediaType
类封装了三个字段:type
、subtype
、CharSet
。分别对应 MIME 类型中的数据类
,数据类型下的子类
,编码类型
。例如text/x-markdown; charset=utf-8
,text
代表文本大类(type),x-markdown
代表文本类下的子类(subtype),charset=utf-8
则表示采用UTF-8编码,内部封装了一个解析方法parse()
,只要传入这些信息就能获得MediaType
实例。可以参考链接Media Types和MIME 参考手册 平时比较常用的 MIME 类型如下:json :application/json
,xml:application/xml
,png:image/png
,jpg: image/jpeg
,gif:image/gif
除了直接使用静态方法create()
构建之外, RequestBody
还有两个直接子类FormBody
,MultipartBody
,分别用于提交 Form 表单中的键值对和构建分块表单请求体(既可以添加表单,又可以也可以添加文件等二进制数据)
FormBody表单创建
通过内部类 Builder
构建 FormBody
实例,同时调用 add()
传入需要传的键值对,例如:
RequestBody formBody = new FormBody.Builder()
.add("username", "张少林")
.build();
构建分块表单请求体 MultipartBody
MultipartBody
也是通过内部类 MultipartBody.Builder
动态构建实例,构建实例之前有以下几个方法:
setType()
:设置MultipartBody
的MediaType
类型,一般会设置为MultipartBody.FORM
,也就是multipart/form-data
.addFormDataPart(String name, String value)
:添加字符串键值对addFormDataPart(String name, @Nullable String filename, RequestBody body)
:添加表单文件- 其中三个重载
addPart()
方法,分别添加请求体,其中的addPart(@Nullable Headers headers, RequestBody body)
可以在添加RequestBody的时候,同时为其单独设置请求头
源码浅析:查看MultipartBody.Builder
源码发现,addFormDataPart(String name, String value)
内部其实都是调用了addPart(Part part)
,而addPart(Part part)
内部是往全局实例化好的List<Part>
中 add 实例化好的part
。而MultipartBody.Part
其实就是包含了RequestBody
,所以可以说 MultipartBody
是一个RequestBody
的集合。
三、Response
Response对应于 Http 的响应报文,查看源码可以看到,Request 封装了响应报文的一些字段。
封装的字段有 对应的请求报文Request
,响应码 code
,响应消息 message
,响应体ResponseBody
,网络请求响应结果以及缓存响应结果等字段。同样的,Response
的构建也是通过内部类 Response.Builder 进行动态灵活配置,对其中的一些常用方法做简单介绍:
isSuccessful()
:用来判断请求是否成功body()
:返回响应体ResponseBody
headers()
:获取响应头Headers
对象
四、ResponseBody
通过 Response
的body()
方法可以得到响应体ResponseBody
,响应体必须最终要被关闭,否则会导致资源泄露、App运行变慢甚至崩溃。
响应体中的数据有可能很大,应该只读取一次响应体的数据。调用ResponseBody
的bytes()
或string()
方法会将整个响应体数据写入到内存中,可以通过source()
、byteStream()
或charStream()
进行流式处理。
五、OkHttpClient
OkHttp 网络请求客户端,封装了 一些客户端请求的常用配置,例如请求超时时间、连接超时时间、读取超时时间、缓存目录、代理等等。
同样,OkHttpClient 也是通过内部类 Builder 模式,动态灵活的配置参数,从而构建 OkHttpClient 实例。默认初始化 OkHttpClient client = new OkHttpClient()
内部已经帮我们配置了一系列参数,我们需要自己配置参数的话,只需要通过内部类 Builder 实例,在调用 build()
方法之前,调用对应方法动态配置即可。OkHttpClient client = new OkHttpClient.Builder().build();
例如我们动态配置读取时间为30秒(默认配置为10秒):
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.build();
当配置完成之后,我们需要对特定的请求配置不同的参数,也可以OkHttpClient.Builder builder = okHttpClient.newBuilder();
拿到 Builder 类实例进行"返厂重建"
当然了,还有很多参数可以配置,具体可以查阅文档 OkHttpClient参数配置
注意:在创建 OkHttpClient 时,应当保持全局单例, 每一个 OkHttpClient 实例都保持着自己的连接池和线程池 ,重用连接池和线程池会节省一些内部资源,假如每次请求都去创建新的客户端实例,就会造成一定的资源浪费
六、创建 Call 对象,发起网络请求
查阅 OkHttpClient 源码,我们看到有个 newCall()
方法,传入 Request
实例,返回一个 Call
对象。我们暂且不用考虑它的原理,只知道可以用它来发起网络请求,返回 Response
响应报文。点击进去查看 Call 接口,可以看到有两个方法是用来请求网络的。
execute()
:同步的网络请求,返回Response
响应结果,由于同步请求会阻塞当前线程,我们使用过程还需要开启子线程,所以一般较少使用。enqueue(Callback responseCallback)
:加入队列,也就是异步网络请求,传入回调接口,发起网络请求时,请求成功会回掉Callback
对象的onResponse
方法,同时返回Response
响应报文,对结果进行处理。请求失败则回调Callback对象的onFailure
方法,对失败进行处理。由于异步请求不会阻塞当前线程,所以一般使用异步请求,注意:不管是成功还是失败的回调方法都是在子线程,不能直接进行 ui 操作。isCanceled()
方法用于判断网络请求是否取消isExecuted()
方法用于判断网络请求是否已经执行request()
返回发起该请求的Request
请求报文clone()
方法克隆一个Call
对象
七、实际使用
至此,已经理清了 OkHttp 的关键类以及网络请求步骤,现在对于 OkHttp 的基本使用已经没多大问题了,下面举例几个实际场景使用,滴滴滴,开车~~~
别忘了添加网络权限:
<uses-permission android:name="android.permission.INTERNET"/>
同步 GET 请求
private void sync_get() throws IOException {
//构建okHttp客户端
OkHttpClient okHttpClient = new OkHttpClient();
//构建请求报文
Request request = new Request.Builder()
.url("http://www.baidu.com")//配置请求百度首页地址
.build();
//发起同步请求,返回响应报文
Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
//获取Headers 请求头对象
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
//分别获取请求头的name,value值
String headName = headers.name(i);
String headValue = headers.value(i);
}
//将响应体中的数据转为 String ,读取到内存
final String string = response.body().string();
} else {
//失败处理
}
}
注意: 同步请求必须开启子线程,我这里省略部分代码
异步 GET 请求
private void async_get() {
//构建okHttp客户端
OkHttpClient okHttpClient = new OkHttpClient();
//构建请求报文
Request request = new Request.Builder()
.url("http://www.baidu.com")
.build();
//发起异步请求,返回响应报文
okHttpClient.newCall(request)
.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//失败处理
Log.e(TAG, "onFailure: " + Thread.currentThread().getName());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.e(TAG, "onResponse: " + Thread.currentThread().getName());
//获取Headers 请求头对象
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
//分别获取请求头的name,value值
String headName = headers.name(i);
String headValue = headers.value(i);
}
//将响应体中的数据转为 String ,读取到内存
final String string = response.body().string();
}
});
}
异步请求不需要自己开启子线程,打印回调方法的线程名发现,成功或者失败回调默认都是在子线程
//06-21 22:52:01.567 30162-30277/com.sunnada.okhttpdemo E/MainActivity: onResponse: OkHttp http://www.baidu.com/...
POST 上传 json 字符串
public void async_post() {
//解析数据类型为json
MediaType MEDIA_TYPE_JSON
= MediaType.parse("application/json; charset=utf-8");
//构建OkHttp 客户端
OkHttpClient okHttpClient = new OkHttpClient();
//构建json数据源
User user = new User("张少林", "123456");
String json = new Gson().toJson(user);
//初始化请求体
RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json);
//初始化请求报文
Request request = new Request.Builder()
.url("http://192.168.1.104:8080/users/uploadJson")//配置服务端请求地址
.post(requestBody)//配置请求方法,传入请求体
.build();
//发起异步请求,回调处理结果
okHttpClient.newCall(request)
.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//失败回调
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
Log.e(TAG, "onResponse: " + response.body().string());
}
});
}
POST 上传文件
public void postFile() {
//获取手机中的图片文件
List<File> files = FileUtils.listFilesInDir(SDCardUtils.getSDCardPath() + "/Pictures/Screenshots", false);
File file = files.get(1);
String fileName = file.getName();
//构建OkHttp 客户端
OkHttpClient okHttpClient = new OkHttpClient();
if (!file.exists()) {
ToastUtils.showLong("文件不存在");
} else {
//构建文件请求头
RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
Request request = new Request.Builder()
.url("http://192.168.1.104:8080/upload")
.post(requestBody)
.build();
okHttpClient.newCall(request)
.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
Log.e(TAG, "onResponse: " + response.body().string());
}
});
}
}
POST 提交表单
public void post_form() {
//构建okHttp客户端
OkHttpClient okHttpClient = new OkHttpClient();
RequestBody formBody = new FormBody.Builder()
.add("username", "张少林")
.add("password", "123456")
.build();
Request request = new Request.Builder()
.url("http://192.168.1.104:8080/users/register")
.post(formBody)
.build();
okHttpClient.newCall(request)
.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
Log.e(TAG, "onResponse: " + response.body().string());
}
});
}
POST 图文上传
public void post_img_title() {
//获取手机中的图片文件
List<File> files = FileUtils.listFilesInDir(SDCardUtils.getSDCardPath() + "/Pictures/Screenshots", false);
File file = files.get(1);
MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)//设置表单类型
.addFormDataPart("title", "Logo")//添加表单键值对
.addFormDataPart("image", "logo.png", RequestBody.create(MEDIA_TYPE_PNG, file))//添加表单文件
.build();
Request request = new Request.Builder()
.url("xxxxxxx")
.post(requestBody)
.build();
client.newCall(request)
.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//请求失败处理
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
Log.e(TAG, "onResponse: " + response.body().string());
}
});
}
文件下载,这里下载一张图片
public void downloadImg(){
OkHttpClient client = new OkHttpClient();
final Request request = new Request.Builder()
.get()
.url("https://www.baidu.com/img/bd_logo1.png")
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//拿到字节流
InputStream is = response.body().byteStream();
int len = 0;
File file = new File(Environment.getExternalStorageDirectory(), "image.png");
FileOutputStream fos = new FileOutputStream(file);
byte[] buf = new byte[128];
while ((len = is.read(buf)) != -1){
fos.write(buf, 0, len);
}
fos.flush();
//关闭流
fos.close();
is.close();
}
});
}