在上一篇博客中,我们介绍了OkHttp的快速集成和使用,其中提到了有关于使用post
请求提交字符串/文件/表单
等需要使用到OkHttp中的底层传输框架OkIo。鉴于框架的功能划分,这篇博客专门讲解OkIo的常用api和应用场景。当然,考虑到OkIo和OkHttp一般是搭配使用的,所以这里的演示更多是网络传输场景。若想使用OkIo做为一个专门用来流传输的框架,可以参考官方文档:OkIo,其中有更详细的使用方法。
OkIo库是一个由square公司开发的,它补充了java.io和java.nio的不足,以便能够更加方便,快速的访问、存储和处理你的数据。而OkHttp的底层也使用该库作为支持。而在开发中,使用该库可以大大给你带来方便。
紧凑的封装:是对Java IO/NIO 的一个非常优秀的封装,绝对的“短小精焊”,不仅支持文件读写,也支持Socket通信的读写。
使用简单:不用区分字符流或者字节流,也不用记住各种不同的输入/输出流,统统只有一个输入流Source和一个输出流Sink,它们之间所包含的主要api如图所示:
API丰富:其封装了大量的API接口用于读/写字节或者一行文本
读写速度快:这得益于其优秀的缓冲机制和处理内存的技巧,使I/O在缓冲区得到更高的复用处理,从而尽量减少I/O的实际发生。
强大的支撑机制:与这些特性相比,就是其有强大的保障机制保驾护航
然而,在正式分析之前有两个核心基础类ByteString
和Buffer
和两个核心API类需要提前理解一下,因为大量的API都是以这两个类为基础来实现的。了解它们,以便有助于后面的分析。
这两个类也可以看作是上面机制的实现,正是上面机制的实现,才使得该库以最少的实际IO来实现快速的IO需求。
理论的知识就说明到这里,接下来正式进入演示环节。OkHttp的请求分为异步/同步,这里的演示统一使用异步请求,方便读者参考。
在使用任何框架之前,集成都是第一步。由于在使用OkIo的同时还会使用到OkHttp,所以这里要导入两个依赖,修改module下的build.gradle,代码如下:
implementation("com.squareup.okhttp3:okhttp:4.6.0")
implementation("com.squareup.okio:okio:1.11.0")
修改完成后Sync一下,确保OkHttp和OkIo都集成到了你的项目中。
既然你集成了OkHttp,就不可不免地要进行与网络有关的操作。另外,由于涉及到文件上传的操作,需要存储卡读写权限,在清单文件下声明需要的权限,即:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
另外:如果你的Android版本为Android P(即targetSdkVersion 27),在运行该项目时可能会报有关网络的错误,如图所示:
原因:在Android P系统的设备上,如果应用使用的是非加密的明文流量的http网络请求,则会导致该应用无法进行网络请求,https则不会受影响,同理若应用内使用WebView加载网页 则加载网页也需要是https请求。
解决方法:
三种方法亦可,这里主要介绍一下第三种解决方法,以拓宽解决思路
network_config(名字非固定)的xml
,代码如下:<network-security-config>
<base-config cleartextTrafficPermitted="true" />
network-security-config>
android:networkSecurityConfig
属性,代码如下: <application
...
android:networkSecurityConfig="@xml/network_security_config"
...
/>
接下来,我们直接开始布局文件activity_main.xml的编写。该布局很简单,仅有四个按钮,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<Button
android:id="@+id/btn_post_keyvalue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送post请求——提交键值对"/>
<Button
android:id="@+id/btn_post_string"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送post请求——提交字符串"/>
<Button
android:id="@+id/btn_post_file"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送post请求——提交文件"/>
<Button
android:id="@+id/btn_post_form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送post请求——提交表单"/>
LinearLayout>
之后,我们简单用字符串封装一下要请求的URL。与上一篇博客不同的是,为了模拟出网络传输的效果,这里架设一个本地服务器,然后将相应URL进行封装,代码如下(服务器URL不固定,根据自己的服务器路径名进行相应修改):
private static final String URL = "http://10.0.2.2:8080/androidframelearn/";
另外:如果你的Android版本为Android M(即targetSdkVersion 23),在运行该项目时可能会报有关网络的错误,如图所示:
原因:这是因为新的保护机制对于仅使用安全通信的应用,Android 6.0 Marshmallow(API 级别 23)引入了两种机制来解决回退到明文通信的问题:(1) 在生产/安装库中,禁止明文通信,以及 (2) 在开发/QA 期间,在遇到任何非 TLS/SSL 通信时,予以记录或者触发崩溃。
解决方法:如果一定要使用明文通信的话,则可以打开AndroidManifest.xml 文件,在 application 元素中添加:
android:usesCleartextTraffic=”true”
异步POST请求提交键值对的步骤跟上一篇步骤提到的一样,大抵分为:
代码如下:
private void postKeyValue() {
btn_post_keyvalue.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1.构建Client对象
OkHttpClient client = new OkHttpClient();
// 2.采用建造者模式和链式调用构建键值对对象
FormBody formBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build();
// 3.采用建造者模式和链式调用构建Request对象
final Request request = new Request.Builder()
.url(URL) // 请求URL
.post(formBody) // 默认就是get请求,可以不写
.build();
// 4.通过1和3产生的Client和Request对象生成Call对象
Call call = client.newCall(request);
// 5.调用Call对象的enqueue()方法,并且实现一个回调实现类
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d(TAG, "发送post请求键值对失败!");
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
Log.d(TAG, "发送post请求键值对成功!请求到的信息为:" + response.body().string());
}
});
}
});
}
异步POST请求提交字符串的步骤跟提交键值对类似,只有第二步不同,大抵分为:
这种方式需要构造一个RequestBody对象,用它来携带我们要提交的数据。在构造 RequestBody 需要指定MediaType,用于描述请求/响应 body 的内容类型,关于 MediaType 的更多信息可以查看RFC 2045。
一般来说,提交字符串的应用场景多为:客户端给服务器发送一个json字符串,这种时候就需要提交字符串了。
代码如下:
private void postString() {
btn_post_string.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1.构建Client对象
OkHttpClient client = new OkHttpClient();
// 2.构造RequestBody
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{username:admin;password:123456}");
// 3.采用建造者模式和链式调用构建Request对象
final Request request = new Request.Builder()
.url(URL) // 请求URL
.post(requestBody) // 默认就是get请求,可以不写
.build();
// 4.通过1和3产生的Client和Request对象生成Call对象
Call call = client.newCall(request);
// 5.调用Call对象的enqueue()方法,并且实现一个回调实现类
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d(TAG, "发送post请求字符串失败!");
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
Log.d(TAG, "发送post请求字符串成功!请求到的信息为:" + response.body().string());
}
});
}
});
}
上面的MediaType我们指定传输的是纯文本,而且编码方式是utf-8,通过上面的方式我们就可以向服务端发送json字符串了。
异步POST请求提交文件的步骤跟提交其他数据类似,只有第二步比较不同,就是要构建出一个文件对象。这里在存储卡路径下放置一个test.txt作为测试,大抵分为:
private void postFile() {
btn_post_file.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1.构建Client对象
OkHttpClient client = new OkHttpClient();
// 2.构造RequestBody
// 2.1 构造文件对象
File file = new File(MainActivity.this.getExternalFilesDir(null), "test.txt");
Log.i(TAG,Environment.getExternalStorageDirectory().toString());
// 2.2 判断文件是否为空
if (!file.exists()){
Log.i(TAG,"文件为空,无法创建");
}else{
// 2.3 通过文件构造构造RequestBody对象
RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
// 3.采用建造者模式和链式调用构建Request对象
final Request request = new Request.Builder()
.url(URL) // 请求URL
.post(requestBody) // 默认就是get请求,可以不写
.build();
// 4.通过1和3产生的Client和Request对象生成Call对象
Call call = client.newCall(request);
// 5.调用Call对象的enqueue()方法,并且实现一个回调实现类
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d(TAG, "发送post请求文件失败!");
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
Log.d(TAG, "发送post请求文件成功!请求到的信息为:" + response.body().string());
}
});
}
}
});
}
另外:如果你的Android版本为Android X(即targetSdkVersion 29,万恶的Android 10),若使用Environment.getExternalStorageDirectory()
来获取路径名,则会报出传输权限的错误,如图所示:
原因:Android长久以来都支持外置存储空间这个功能,也就是我们常说的SD卡存储。这个功能使用得极其广泛,几乎所有的App都喜欢在SD卡的根目录下建立一个自己专属的目录,用来存放各类文件和数据。
简单来讲,就是Android系统对SD卡的使用做了很大的限制。从Android 10开始,每个应用程序只能有权在自己的外置存储空间关联目录下读取和创建文件,这个特性也被叫做作用域存储。所以使用之前的api来获取存储路径的方式,已然行不通了。
解决方法:使用获取该应用的关联路径的api来替代之前获取路径的api,代码如下
context.getExternalFilesDir()
路径为:
/storage/emulated/0/Android/data/<包名>/files
异步POST请求提交表单的步骤跟提交其他数据类似,只有第二步比较不同,就是要构建出一个MuiltipartBody对象,大抵分为:
构造OkHttpClient对象;
构造MuiltipartBody(RequestBody)对象;
构造Request对象;
通过前两步中的对象构建Call对象;
通过call.newCall()方法来提交异步请求。
一般来说,提交字符串的应用场景多为:客户端给服务器发送一个携带有账号、密码、头像等信息的表单,这种时候就需要提交表单了。
这里我们会用到一个OkIo包含的类:MuiltipartBody,这是RequestBody的一个子类,我们提交表单就是利用这个类来构建一个RequestBody,下面的代码我们会发送一个包含用户名、密码、头像的表单到服务端。
代码如下:
private void postForm() {
btn_post_form.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1.构建Client对象
OkHttpClient client = new OkHttpClient();
// 2.构造RequestBody
// 2.1 构造文件对象
File file = new File(MainActivity.this.getExternalFilesDir(null), "test.png");
Log.i(TAG,Environment.getExternalStorageDirectory().toString());
// 2.2 判断文件是否为空
if (!file.exists()){
Log.i(TAG,"文件为空,无法创建");
}else{
// 2.3 通过表单构造构造RequestBody对象
RequestBody muiltipartBody = new MultipartBody.Builder()
//一定要设置这句
.setType(MultipartBody.FORM)
.addFormDataPart("username", "admin")//
.addFormDataPart("password", "admin")//
.addFormDataPart("myfile", "test.png", RequestBody.create(MediaType.parse("application/octet-stream"), file))
.build();
// 3.采用建造者模式和链式调用构建Request对象
final Request request = new Request.Builder()
.url(URL) // 请求URL
.post(muiltipartBody) // 默认就是get请求,可以不写
.build();
// 4.通过1和3产生的Client和Request对象生成Call对象
Call call = client.newCall(request);
// 5.调用Call对象的enqueue()方法,并且实现一个回调实现类
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d(TAG, "发送post请求表单失败!");
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
Log.d(TAG, "发送post请求表单成功!请求到的信息为:" + response.body().string());
}
});
}
}
});
}
注意,在使用MuiltipartBody前,我们需要注意:
setType(MultipartBody.FORM)
这一句addFormDataPart()
的第一个参数,就上面代码中的myfile就是类似于键值对的键,是供服务端使用的,就类似于网页表单里面的name属性,例如:<input type="file" name="myfile">
addFormDataPart()
的第二个参数文件的本地的名字,第三个参数是RequestBody,里面包含了我们要上传的文件的路径以及MidiaTypeAFL——Android框架学习