Android-OkHttp

  从Android4.4开始HttpURLConnection的底层实现采用OkHttp

  同步请求:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。同步通信方式要求通信双方以相同的时钟频率进行,而且准确协调,通过共享一个单个时钟或定时脉冲源保证发送方和接收方的准确同步,效率较高。

  异步请求:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。异步通信方式不要求双方同步,收发方可采用各自的时钟源,双方遵循异步的通信协议,以字符为数据传输单位,发送方传送字符的时间间隔不确定,发送效率比同步传送效率低。

  测试URL: https://www.httpbin.org/

添加依赖

1
implementation("com.squareup.okhttp3:okhttp:4.9.0")

相关权限

1
2
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

同步请求

get请求

创建一个OkHttpClient对象作为请求器

1
OkHttpClient okHttpClient = new OkHttpClient();

创建http请求对象、设置请求地址、得到Request的实例对象、把request实例对象交给请求器、调用call的execute方法实现同步请求

1
2
3
4
5
6
7
8
9
10
//连缀方式一行带过
//Request request = new Request.Builder().url("https://www.httpbin.org/get").build();
// http请求的建造器
Request.Builder builder = new Request.Builder();
// 通过建造器的url方法设置请求地址,并通过build方法返回一个Request请求对象
Request request = builder.url("https://www.httpbin.org/get").build();
// 把request请求对象交给请求工具okHttpClient
Call call = okHttpClient.newCall(request);
// 同步请求就调用call的execute方法,execute返回一个Response对象
Response response = call.execute();

Response报错,需要进行异常捕获处理

1
2
3
4
5
6
  try {
// 同步请求就调用call的execute方法,execute返回一个Response对象
Response response = call.execute();
} catch (IOException e) {
e.printStackTrace();
}

执行如下代码,会闪退并报错,因为安卓中完成网络请求必须请求一个子线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//  http请求的建造器
Request.Builder builder = new Request.Builder();
// 通过建造器的url方法设置请求地址,并通过build方法返回一个Request请求对象
Request request = builder.url("https://www.httpbin.org/get").build();
// 把request请求对象交给请求工具okHttpClient
Call call = okHttpClient.newCall(request);
try {
// 同步请求就调用call的execute方法,execute返回一个Response对象
// 完成请求后得到的响应封装在Response对象中
Response response = call.execute();
// 得到response的请求体,并得到其中的字符串数据
String str = response.body().string();
// 设置显示
code.setText(str);
} catch (IOException e) {
e.printStackTrace();
}

创建一个子线程

1
2
3
4
5
6
7
//  创建一个新线程
new Thread(){
@Override
public void run() {
super.run();
}
}.start();

在线程中完成get请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//  创建一个新线程
new Thread(){
@Override
// 继承run方法,填入线程中执行的事件
public void run() {
// http请求的建造器
Request.Builder builder = new Request.Builder();
// 通过建造器的url方法设置请求地址,并通过build方法返回一个Request请求对象
Request request = builder.url("https://www.httpbin.org/get").build();
// 把request请求对象交给请求工具okHttpClient
Call call = okHttpClient.newCall(request);
try {
// 同步请求就调用call的execute方法,execute返回一个Response对象
// 完成请求后得到的响应封装在Response对象中
Response response = call.execute();
// 得到response的请求体,并得到其中的字符串数据
String str = response.body().string();
// 子线程中更新UI
runOnUiThread(new Runnable() {
@Override
public void run() {
// 设置请求的数据显示在text
code.setText(str);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

post请求

同步post请求与同步get请求几乎一样,多了post数据而已

1
2
3
4
//  创建FormBody表单提交对象
FormBody formBody = new FormBody.Builder().add("a","1").add("b","2").build();
// 创建post方式的请求对象,并把表单对象提交
Request request1 = new Request.Builder().url("https://www.lxx6.com").post(formBody).build();

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  new Thread(){
@Override
public void run() {
// 创建FormBody表单提交对象
FormBody formBody = new FormBody.Builder().add("a","1").add("b","2").build();
// 创建post方式的请求对象,并把表单对象提交
Request request1 = new Request.Builder().url("https://www.lxx6.com").post(formBody).build();
// 其余同get请求
Call call1 = okHttpClient.newCall(request1);
try {
Response response = call1.execute();
String str = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

异步请求

get请求

创建一个OkHttpClient对象作为请求器

1
OkHttpClient okHttpClient = new OkHttpClient();

创建请求对象

1
2
3
4
//  创建请求对象
Request request = new Request.Builder().url("https://blog.xxin.xyz").build();
// 把请求对象提交给请求器发送请求
Call call = okHttpClient.newCall(request);

这里call调用enqueue()方法,不再是execute()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//  这里call调用enqueue()方法,不再是execute()
// enqueue方法需要传入一个CallBack回调函数
call.enqueue(new Callback() {
@Override
// 当请求失败时调用onFailure
public void onFailure(@NotNull Call call, @NotNull IOException e) {
// 创建一个ui修改线程
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText("请求失败");
}
});
}

@Override
// 当请求结束时调用onResponse,请求结束并不代表请求成功,完成请求后得到的响应封装在Response对象中
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
// 调用Response类的isSuccessful()方法判断是否请求成功
// 打开isSuccessful()源码可知当请求码在200-299之间时请求成功
if (response.isSuccessful()){
// 得到response的请求体,并得到其中的字符串数据
String str = response.body().string();
// 创建一个ui修改线程
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
}
}
});

注意:

  1. 异步请求不会阻塞后续代码的执行。
  2. 异步请求不需要创建新线程,创建enqueue时已经创建了新的线程

post请求

异步post与异步get几乎相同,唯多了post数据而已

1
2
3
4
//  创建FormBody表单提交对象
FormBody formBody = new FormBody.Builder().add("a","1").add("b","2").build();
// 创建post方式的请求对象,并把表单对象提交
Request request1 = new Request.Builder().url("https://www.amyxiaoxin.top").post(formBody).build();

这里call调用enqueue()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  Call call1 = okHttpClient.newCall(request1);
call1.enqueue(new Callback() {
@Override
// 请求失败时
public void onFailure(@NotNull Call call, @NotNull IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText("请求失败");
}
});
}
@Override
// 请求结束时
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
if (response.isSuccessful()){
String str = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
}
}
});

post上传文件

协议规定POST提交的数据必须放在请求体中,但协议并没有规定数据必须使用什么编码方式

常用的编码方式有

  1. Content-Type: application/x-www-form-urlencoded;数据被编码为“名称/键值对”,默认类型(FormBody就是这种类型)
  2. Content-Type: multipart/from-date;数据编码为一条消息,一般用于文件上传
  3. Content-Type: application/octet-stream;提交二进制数据,如果用于文件上传,只能上传一个文件
  4. Content-Type: application/json;提交json数据

  数据编码方式对照表

多文件上传

用multipart/from-date编码方式实现多文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  new Thread(){
@Override
public void run() {
// 设置文件路径
String File1Path = "/storage/emulated/0/MT2/apks/temp/file1.txt";
String File2Path = "/storage/emulated/0/MT2/apks/temp/file2.txt";
// 获取路径下文件的对象
File file1 = new File(File1Path);
File file2 = new File(File2Path);
// 创建请求体
RequestBody requestBody1 = RequestBody.create(file1, MediaType.parse("text/plain"));//文件,文件类型,文件对象
RequestBody requestBody2 = RequestBody.create(file2, MediaType.parse("text/plain"));//文件,文件类型,文件对象
// 通过multipart方式进行多文件上传,所以创建MultipartBody
MultipartBody multipartBody = new MultipartBody.Builder()
.addFormDataPart("file1",file1.getName(),requestBody1)//文件key值,文件名
.addFormDataPart("file2",file2.getName(),requestBody2)//文件key值,文件名
.build();
// 创建请求对象
Request request = new Request.Builder().url("https://www.httpbin.org/post").post(multipartBody).build();
// 用请求器发起请求
Call call = okHttpClient.newCall(request);
try {
Response execute = call.execute();
String str = execute.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

二进制流上传文件

不知道文件类型,使用application/octet-stream二进制流上传文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  new Thread(){
@Override
public void run() {
// 设置文件路径
String FilePath = "/storage/emulated/0/MT2/apks/temp/img.png";
// 得到文件对象
File file = new File(FilePath);
// 创建请求体
RequestBody requestBody = RequestBody.create(file,MediaType.parse("application/x-png"));
// 创建请求对象
Request request = new Request.Builder().url("https://www.httpbin.org/post").post(requestBody).build();
Call call = okHttpClient.newCall(request);
try {
Response execute = call.execute();
String str = execute.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

提交json数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  new Thread(){
@Override
public void run() {
// 创建json数据
String json = "{\"a\":2,\"b\":2}";
// 创建请求体
RequestBody requestBody = RequestBody.create(json,MediaType.parse("application/json"));
// 创建请求对象
Request request = new Request.Builder().url("https://www.httpbin.org/post").post(requestBody).build();
// 用请求器发起请求
Call call = okHttpClient.newCall(request);
try {
Response execute = call.execute();
String str = execute.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

get下载文件

OkHttp简单的文件下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  Request request = new Request.Builder().url("https://www.httpbin.org/image/png").build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText("下载失败");
}
});
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
if (response.isSuccessful()) {
// 用输入流接收response中的字节流并解码
InputStream inputStream = response.body().byteStream();
// 用输出流,把接收的数据写入本地文件
FileOutputStream fileOutputStream = null;
// 文件储存地址
String filePath = "/storage/emulated/0/MT2/apks/temp/download.png";
File file = new File(filePath);//创建文件对象
// 写入到本地
fileOutputStream = new FileOutputStream(file);
byte[] bytes = new byte[1024];//字节长度
int len = 0;
// 获取下载文件大小
long fileSize = response.body().contentLength();
long sum = 0;//计算下载进度值
while ((len = inputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes,0,len);
// 得到进度值
sum += len;
Log.d(TAG, "进度: " + (int) ((sum * 1.0f / fileSize) * 100) + "%");
}
}
}
});

拦截器

addInterceptor(应用拦截器,先执行):

  1. 不需要担心中间过程的响应,如重定向和重试
  2. 总是只调用一次,即使HTTP响应是从缓存中获取
  3. 观察应用程序的初衷. 不关心OkHttp注入的头信息如: If-None-Match
  4. 允许短路而不调用 Chain.proceed(),即中止调用
  5. 允许重试,使 Chain.proceed()调用多

addNetworkInterceptor(网络拦截器,后执行):

  1. 能够操作中间过程的响应,如重定向和重试
  2. 当网络短路而返回缓存响应时不被调用
  3. 只观察在网络上传输的数据
  4. 携带请求来访问连接

addInterceptor(应用拦截器)

可以添加很多个,执行顺序与添加顺序相同(但总会比addNetworkInterceptor先执行)

  在请求器发送请求前会执行一次Interceptor拦截器的intercept方法,intercept方法内可以通过chain请求对象request,对请求对象进行发送请求前置操作,例如添加响应头等,然后通过chain.proceed(request)获得到处理之后的最终的响应信息并返回出去,这样就省下了在拦截器外重复添加响应头的繁琐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@NotNull
@Override
// 在请求器发送请求前会执行一次Interceptor拦截器的intercept方法
public Response intercept(@NotNull Chain chain) throws IOException {
// 通过chain可以得到请求对象request
Request request = chain.request();
// 对请求对象处理
Request request1 = request.newBuilder()
.addHeader("os","andrroid")//添加响应头
.addHeader("version","1.0")//添加响应头
.build();
// 获得到处理之后的最终的响应信息并返回出去
Response response = chain.proceed(request1);
return response;
}
})//添加addInterceptor拦截器
.build();
// 创建请求对象发起请求
new Thread(){
@Override
public void run() {
Request request = new Request.Builder().url("https://www.httpbin.org/get").build();
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
String str = response.body().string();
Log.d(TAG, "addInterceptor: " + str);
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

可见应用拦截器添加的响应头

addNetworkInterceptor(网络拦截器)

addNetworkInterceptor总会比addInterceptor后执行,所以它可以读取addNetworkInterceptor中添加的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addNetworkInterceptor(new Interceptor() {
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
// 通过chain可以得到请求对象request
Request request = chain.request();
// 对请求对象处理
Request request1 = request.newBuilder()
.addHeader("os","android")//添加响应头
.addHeader("version","1.0")//添加响应头
.build();
Response response = chain.proceed(request1);
return response;
}
})//添加addNetwork拦截器
.build();
// 创建请求对象发起请求
new Thread(){
@Override
public void run() {
Request request = new Request.Builder().url("https://www.httpbin.org/get").build();
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
String str = response.body().string();
Log.d(TAG, "addInterceptor: " + str);
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

OkHttp的缓存

添加缓存后okHttpClient会自动判断是否可以使用缓存并且自动保存缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
// 添加请求缓存,参数:缓存保存路径,缓存文件最大占用(超过大小新缓存会自动替换旧缓存)
.cache(new Cache(new File("/storage/emulated/0/MT2/apks/temp/temp"),1024*1024))
.build();
// 创建请求对象发起请求
new Thread(){
@Override
public void run() {
Request request = new Request.Builder().url("https://www.httpbin.org/get").build();
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
String str = response.body().string();
Log.d(TAG, "addInterceptor: " + str);
runOnUiThread(new Runnable() {
@Override
public void run() {
code.setText(str);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();

拓展:try/catch异常抛出与捕获

1
2
3
4
5
6
7
8
9
try {
// 可能出现异常的代码
}
catch(异常类名A e){
// 如果出现了异常类A类型的异常,那么执行该代码
}//...(catch可以有多个)
finally {
// 最终肯定必须要执行的代码(例如释放资源的代码)
}