Android开发之网络编程框架-OkHttp

OkHttp 简介

HTTP 是现代应用程序网络的方式。这就是我们交换数据和媒体的方式。有效地执行 HTTP 可以使您的内容加载更快并节省带宽。

OkHttp 是一个默认高效的 HTTP 客户端:

  • HTTP/2 支持允许对同一主机的所有请求共享一个套接字。
  • 连接池减少了请求延迟(如果 HTTP/2 不可用)。
  • 透明 GZIP 缩小了下载大小。
  • 响应缓存完全避免了网络重复请求。

当网络出现问题时,OkHttp 坚持不懈:它会默默地从常见的连接问题中恢复。如果您的服务有多个 IP 地址,如果第一次连接失败,OkHttp 将尝试备用地址。这对于 IPv4+IPv6 和冗余数据中心中托管的服务是必要的。OkHttp 支持现代 TLS 功能(TLS 1.3、ALPN、证书固定)。它可以配置为回退以实现广泛的连接。

使用 OkHttp 很容易。它的请求/响应 API 设计有流畅的构建器和不变性。它支持同步阻塞调用和带有回调的异步调用。

应用场景

  • 请求内容数据(目前来说,数据格式主要是 json,xml 比较少用了)
  • 加载图片(一般来说,图片地址以内容的形式返回到手机端,然后再通过图片地址进行加载到控件中)
  • 上传文件/数据(头像的上传,采集数据的上传,甚至是录像之类的大文件上传)

使用框架

Android 10 Http 访问配置(https 协议略过)

Android27 以上,默认是不支持 Http 访问的了,需要使用 https,如果你要使用 Http 明文访问,那么需要配置一下。

在清单文件,application 节点,添加

1
android:networkSecurityConfig="@xml/network_security_config"

添加 network_security_config.xml 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">sunofbeaches.com</domain>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">www.sunofbeach.net</domain>
<domain includeSubdomains="true">imgs.sunofbeaches.com</domain>
</domain-config>
</domain-config>
</network-security-config>

关于网络安全配置可参考谷歌官方文档:https://developer.android.google.cn/training/articles/security-config#manifest

声明权限

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

添加依赖

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

异步 GET 请求

步骤:

  1. 创建 OkHttpClient
  2. 创建请求内容
  3. 浏览器根据请求内容创建请求任务
  4. 执行请求任务

代码:

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
public void asyncGet(View view) {
//获取商城的分类信息
String url = "https://xxxx.com/api/json";
//1、创建client,理解为创建浏览器
OkHttpClient okHttpClient = new OkHttpClient();
//2、创建请求内容
Request request = new Request.Builder()
.url(url)
.get()
.build();
//3、用浏览器创建调用任务
Call call = okHttpClient.newCall(request);
//4、执行任务
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call,@NotNull IOException e) {
Log.d(TAG,"onFailure -- > " + e.toString());
}

@Override
public void onResponse(@NotNull Call call,@NotNull Response response) throws IOException {
Log.d(TAG,"response -- > " + response.body().string());
}
});
}

运行返回结果:

同步 GET 请求

同步请求需自行处理线程的问题,不可以在 UI 主线程中去执行任务。

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
public void syncGet(View view) {
//获取商城的分类信息
String url = "https://xxxx.com/api/json";
//1、创建client,理解为创建浏览器
OkHttpClient okHttpClient = new OkHttpClient();
//2、创建请求内容
Request request = new Request.Builder()
.url(url)
.get()
.build();
//3、用浏览器创建调用任务
final Call call = okHttpClient.newCall(request);
//4、执行任务
new Thread(new Runnable() {
@Override
public void run() {
try {
Response response = call.execute();
Log.d(TAG,"response -- > " + response.body().string());
} catch(IOException e) {
e.printStackTrace();
Log.d(TAG,"failure -- > " + e.toString());
}
}
}).start();
}

POST 请求

与 GET 请求基本相似,下面以搜索接口为例

接口地址:https://xxxx.com/api/search

参数:keysword -搜索的关键词

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void doSearch(View view) {
String url = "https://xxxx.com/api/search";
RequestBody requestBody = new FormBody.Builder()
.add("keyword","电脑")
.build();
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call,@NotNull IOException e) {
Log.d(TAG,"onFailure -- > " + e.toString());
}

@Override
public void onResponse(@NotNull Call call,@NotNull Response response) throws IOException {
Log.d(TAG,"response json --> " + response.body().string());
}
});
}

可以从代码中看出,Formbody 是 RequestBody 的子类,RequestBody 下有两个子类:FormBody、MultipartBody,所以除了提交表单还可以提交文件。

POST 上传单文件

单文件上传的使用场景一般有:上传头像或者上传日志等

操作文件需先声明权限,因为读取的数据一般是在拓展卡里面,所以一般都需要此权限。

1
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void postFile(View view) {
String url = "https://127.0.0.1:8080/file/upload";
OkHttpClient httpClient = new OkHttpClient.Builder().build();
File file = new File("/storage/emulated/0/Download/1.jpg");
MediaType mediaType = MediaType.parse("jpeg");
RequestBody fileBody = RequestBody.create(file,mediaType);
RequestBody requestBody = new MultipartBody.Builder()
.addFormDataPart("file",file.getName(),fileBody)
.build();
Request request = new Request.Builder().url(url).post(requestBody).build();
Call call = httpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call,@NotNull IOException e) {
Log.d(TAG,"上传失败--> " + e.toString());
}

@Override
public void onResponse(@NotNull Call call,@NotNull Response response) throws IOException {
Log.d(TAG,"上传结果:" + response.body().string());
}
});
}

返回结果:

1
{"success":true,"code":10000,"message":"上传成功.文件路径为:E:\\service\\Upload\\1.jpg","data":null}

上传的文件类型可参考这个表:https://blog.csdn.net/qyt0147/article/details/80610481

POST 上传多文件

多文件上传需要根据接口程序所提供的参数来。比如接口地址为:http://127.0.0.1:8080/api/files/uploads,参数key为:files,所以多文件上传参数要求必须为files,实现代码如下:

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
public void postMultiFile(View view) {
String url = "http://127.0.0.1:8080/api/files/upload";
OkHttpClient httpClient = new OkHttpClient.Builder().build();
File fileOne = new File("/storage/emulated/0/Download/1.jpg");
File fileTwo = new File("/storage/emulated/0/Download/rBsADV3nxtKACoSfAAAPx8jyjF8169.png");
File fileThree = new File("/storage/emulated/0/Download/rBsADV2rEz-AIzSoAABi-6nfiqs456.png");
MediaType mediaType = MediaType.parse("jpeg");
RequestBody fileOneBody = RequestBody.create(fileOne,mediaType);
RequestBody fileTwoBody = RequestBody.create(fileTwo,mediaType);
RequestBody fileThreeBody = RequestBody.create(fileThree,mediaType);
RequestBody requestBody = new MultipartBody.Builder()
.addFormDataPart("files",fileOne.getName(),fileOneBody)
.addFormDataPart("files",fileTwo.getName(),fileTwoBody)
.addFormDataPart("files",fileThree.getName(),fileThreeBody)
.build();
Request request = new Request.Builder().url(url).post(requestBody).build();
Call call = httpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call,@NotNull IOException e) {
Log.d(TAG,"多文件上传失败--> " + e.toString());
}

@Override
public void onResponse(@NotNull Call call,@NotNull Response response) throws IOException {
Log.d(TAG,"多文件上传结果:" + response.body().string());
}
});
}

返回结果:

1
{"success":true,"code":10000,"message":"上传成功3个文件,路径:E:/service/Upload","data":null}

文件下载

文件下载也是提供代码实现对网络文件的请求,文件内容在响应头中,只需把它写入文件里面即可,具体代码则不再演示。

关于 Android 权限问题

Android 6 以下的权限

Android6.0 以下的权限为安装时权限,如果一个应用跑在 android6.0 以下的系统,那么应用所声明的权限,会在安装的时候提示用户是否允许。还有一种 case 是升级,升级应用的时候,如果有新的权限,那么也会提示用户是否允许,不过呢,基本上很少人使用 android6.0 以下的系统了,所以,我们关注点还是在 6.0 以上的运行时权限获取。

Android 6 以上的权限

权限的声明

在配置文件中解析声明即可,如下图所示:

权限检查

危险权限如下图所示:

检查权限代码:

1
2
3
4
5
int readPermission = checkSelfPermission(Manifest.permission.READ_CALENDAR);
int writePermission = checkSelfPermission(Manifest.permission.WRITE_CALENDAR);
if(readPermission != PackageManager.PERMISSION_GRANTED || writePermission != PackageManager.PERMISSION_GRANTED) {
//至少有一个没有权限
}

请求权限

1
2
3
4
5
6
int readPermission = checkSelfPermission(Manifest.permission.READ_CALENDAR);
int writePermission = checkSelfPermission(Manifest.permission.WRITE_CALENDAR);
if(readPermission != PackageManager.PERMISSION_GRANTED || writePermission != PackageManager.PERMISSION_GRANTED) {
//请求权限
requestPermissions(new String[]{Manifest.permission.READ_CALENDAR,Manifest.permission.WRITE_CALENDAR},PERMISSION_REQUEST_CODE);
}

权限请求的结果处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onRequestPermissionsResult(int requestCode,@NonNull String[] permissions,@NonNull int[] grantResults) {
if(requestCode == PERMISSION_REQUEST_CODE) {
//判断结果
if(grantResults.length == 2 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG,"has permissions..");
//有权限
} else {
Log.d(TAG,"no permissionS...");
//没权限
finish();
}
}
}

重写方法:onRequestPermissionsResult()

以上代码中的结果,是没有做处理的。这里面有一个特殊的 case,就是用户在权限请求时,拒绝了,再次提示时,会有一个勾选框:不再询问。如果用户勾选了不再询问,那么你再次请求同一个权限时,安卓系统不再给出权限请求的提示,用户只可以到设置里开启权限。

如果,在 onRequestPermissionsResult 方法中,检查到的结果是无权限,那么,还要判断用户是不是勾选了不再询问,如果是的话,那么用户看不到任何提示。那么可以能通过以下这个方法进行检查:

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
@Override
public void onRequestPermissionsResult(int requestCode,@NonNull String[] permissions,@NonNull int[] grantResults) {
if(requestCode == PERMISSION_REQUEST_CODE) {
//判断结果
if(grantResults.length == 2 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG,"has permissions..");
//有权限
} else {
Log.d(TAG,"no permissionS...");
//没权限
if(!ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_CALENDAR)&&!ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_CALENDAR)) {
//走到这里,说明用户之前用户禁止权限的同时,勾选了不再询问
//那么,你需要弹出一个dialog,提示用户需要权限,然后跳转到设置里头去打开。
Log.d(TAG,"用户之前勾选了不再询问...");
//TODO:弹出一个框框,然后提示用户说需要开启权限。
//TODO:用户点击确定的时候,跳转到设置里去
//Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
//Uri uri = Uri.fromParts("package", getPackageName(), null);
//intent.setData(uri);
////在activity结果范围的地方,再次检查是否有权限
//startActivityForResult(intent, PERMISSION_REQUEST_CODE);
} else {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_CALENDAR,Manifest.permission.READ_CALENDAR}, PERMISSION_REQUEST_CODE);
//请求权限
Log.d(TAG,"请求权限...");
}
}
}
}

End