一,概述
在上篇blog中以get请求为例分析了OKHttp框架的表层源码,具体参见:OKHttp源码分析(一)
在post请求中用到的API很大部分与get请求中用到的API相同,最大的不同就是Request.Builder类的post方法,这个方法的作用是设置post请求的请求体,接收的参数是RequestBody类及子类对象。
Request.Builder类的post方法的源码是:
public Builder post(RequestBody body) {
return method("POST", body);
}
method方法的源码是:
public Builder method(String method, RequestBody body) {
this.method = method;
this.body = body;
return this;
}
这个方法的作用就是将body对象给Request对象的body字段赋值。
在OKHttp源码分析(一)中我们知道:OKHttpClient对象和Request对象都传递给了RealCall类,即可以在RealCall对象中拿到body对象,然后就可以调用body的writeTo方法得到流对象,向服务器写数据。body的writeTo方法中的代码原理类似于httpURLconnection的post方式上传参数,我们可以对比学习。
具体在哪儿调用的body的writeTo方法不是本篇的重点,本篇的重点是分析RequestBody类及其子类。
具体为以下几点:
1,RequestBody类中核心方法
2,RequestBody类中Create方法
3,FromBody类
4,MultipartBody类
二,RequestBody类及其核心方法
RequestBody类是请求体类,这是上传数据的核心类,它的writeTo方法可以得到流对象,然后将请求体数据写到服务器。这是上传数据的核心方法。
RequestBody类中核心方法有以下三个:
1,public abstract MediaType contentType()//数据类型
2,public long contentLength()//数据长度
3,public abstract void writeTo(BufferedSink sink)//写操作
对于熟悉http协议的小伙伴都知道,在http上传数据时都有数据类型和数据长度,所以前两个方法就不做介绍。下面重点看第三个方法。
RequestBody类中的writeTo方法是抽象方法,具体实现在子类中,所以具体写数据的逻辑也在子类中。这儿需要说明的是BufferedSink 类。
BufferedSink 类是square公司开源的IO框架Okio中的一个类,这个类封装了OutputStream,即本质是一个输出流,具有write方法。Okio框架和BufferedSink 类也不是本篇介绍的重点,这儿不做讲解。把BufferedSink 当成OutputStream使用即可。
三,RequestBody类的create方法
我们知道RequestBody是一个抽象类,它不能进行实例化。因此RequestBody类提供了create方法来创建RequestBody的实例对象。
RequestBody类的create方法有多个重载,但重要的方法只有两个:
//1,创建上传byte数据的RequestBody对象。
create(final MediaType contentType, final byte[] content,final int offset, final int byteCount)
//2,创建上传File数据的RequestBody对象。
create(final MediaType contentType, final File file)
1,创建上传byte数据的RequestBody对象
这个方法的表面意思是上传byte数据,但根据程序员的第六感觉可以清晰的发现它可以上传String数据和file数据。因为String数据和file数据都可以轻松转换成byte数组。由于File文件较大,转换成Byte数组太占内存,所以提供了File数据的专用方法。对于上传String数据常常使用该方法。在OKHttp的使用详解中上传Json数据底层调用的就是该方法。
方法的原码是:
public static RequestBody create(final MediaType contentType, final byte[] content,
final int offset, final int byteCount) {
return new RequestBody() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return byteCount;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.write(content, offset, byteCount);
}
};
}
其实方法的实现很简单,就是创建RequestBody 的子类对象,并重写三个方法。下面注重看writeTo方法。writeTo方法的实现只有一行代码:
sink.write(content, offset, byteCount);
这行代码的意思是:写byte数组,从offset开始,写byteCount长度。
sink.write方法就类似于OutputStream的write方法,此时我们应该明白这行代码的含义了。
此时我们发现:OKHttp虽然是偏底层的网络请求框架,但底层实现并不麻烦,这与httpURLconnection的用法很类似。
2,创建上传File数据的RequestBody对象
方法的原码如下:
public static RequestBody create(final MediaType contentType, final File file) {
if (file == null) throw new NullPointerException("content == null");
return new RequestBody() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return file.length();
}
@Override public void writeTo(BufferedSink sink) throws IOException {
Source source = null;
try {
source = Okio.source(file);
sink.writeAll(source);
} finally {
Util.closeQuietly(source);
}
}
};
}
这个方法也是创建RequestBody类的实例对象,下面也重点看writeTo方法。writeTo方法的核心代码是:
source = Okio.source(file);//根据文件得到输入流对象。
sink.writeAll(source);//将输入流对象写出去。
writeAll是RealBufferSink类的方法,这个也是属于Okio框架中的。方法的原码是:
@Override public long writeAll(Source source) throws IOException {
long totalBytesRead = 0;
for (long readCount; (readCount = source.read(buffer, Segment.SIZE)) != -1; ) {
totalBytesRead += readCount;
emitCompleteSegments();
}
return totalBytesRead;
}
emitCompleteSegments方法的源码是:
@Override public BufferedSink emitCompleteSegments() throws IOException {
if (closed) throw new IllegalStateException("closed");
long byteCount = buffer.completeSegmentByteCount();
if (byteCount > 0) sink.write(buffer, byteCount);
return this;
}
有没有一种很熟悉的感觉。对,这就是常用的IO流操作。
四,FormBody类
虽然RequestBody提供了create方法可以上传String类型数据。但对于上传键值对数据来说需要拼接数据,比较麻烦,所以框架中提供了专业上传键值对数据的FormBody类。
FormBody类的基本用法如下:
FormBody.Builder formBody = new FormBody.Builder();//创建表单请求体
formBody.add("username","zhangsan");//传递键值对参数
formBody.add("password","000000");//传递键值对参数
RequestBody body= formBody.build();
这儿明显是Builder设计模式。
1,分析FormBody的内部类Builder类
Builder是FormBody的内部类,原码是:
public static final class Builder {
private final List<String> names = new ArrayList<>();
private final List<String> values = new ArrayList<>();
public Builder add(String name, String value) {
names.add(HttpUrl.canonicalize(name, FORM_ENCODE_SET, false, false, true, true));
values.add(HttpUrl.canonicalize(value, FORM_ENCODE_SET, false, false, true, true));
return this;
}
public FormBody build() {
return new FormBody(names, values);
}
}
这个类的逻辑非常简单,重点如下:
1,首先创建两个list集合,分别用来盛放key和value;
2,add方法的作用就是将键值对分别放入两个集合中。
3,build方法的作用是将key集合与value集合传递给FormBody对象。
2,FormBody类的writeTo方法
下面看重点,FormBody类的writeTo方法的原码:
@Override public void writeTo(BufferedSink sink) throws IOException {
writeOrCountBytes(sink, false);
}
writeOrCountBytes方法的核心原码是:
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
long byteCount = 0L;
Buffer buffer;
if (countBytes) {
buffer = new Buffer();
} else {
buffer = sink.buffer();
}
for (int i = 0, size = encodedNames.size(); i < size; i++) {
if (i > 0) buffer.writeByte('&');
buffer.writeUtf8(encodedNames.get(i));
buffer.writeByte('=');
buffer.writeUtf8(encodedValues.get(i));
}
if (countBytes) {
byteCount = buffer.size();
buffer.clear();
}
return byteCount;
}
看到这儿是不是仍是一种熟悉的感觉,在使用HttpURLconnection的post请求传递键值对参数时就是这么拼接的。
五,MultipartBody类
根据类名就可得知这个类是多重的body,即可以上传键值对数据,又可以同时上传File数据。比如在微信中发朋友圈时,既需要上传文字又需要上传图片,此时就需要使用这种多重的body。
MultipartBody类的基本使用如下:
MultipartBody multipartBody =new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("groupId",""+groupId)//添加键值对参数
.addFormDataPart("title","title")
.addFormDataPart("file",file.getName(),RequestBody.create(MediaType.parse("file/*"), file))//添加文件
.build();
这儿明显也是builder设计模式。
1,分析MultipartBody 内部类builder
MultipartBody 实现同时上传键值对数据和File数据的原理与httpURLconnection相似,都是仿照Web中提交Form表单数据时的数据格式。
下面看addFormDataPart方法,这个方法有两个重要重载:
//1,添加键值对数据
addFormDataPart(String name, String value)
//2,添加File数据
addFormDataPart(String name, String filename, RequestBody body)
首先看添加键值对数据addFormDataPart方法的源码:
public Builder addFormDataPart(String name, String value) {
return addPart(Part.createFormData(name, value));
}
再看addPart方法:
public Builder addPart(Part part) {
if (part == null) throw new NullPointerException("part == null");
parts.add(part);
return this;
}
这个方法的本质将Part对象赋值给Builder的parts字段。下面看part对象的创建。
在addFormDataPart方法中可知,part对象创建的代码是:
Part.createFormData(name, value)。
方法的源码是:
public static Part createFormData(String name, String value) {
return createFormData(name, null, RequestBody.create(null, value));
}
createFormData方法的源码是:
public static Part createFormData(String name, String filename, RequestBody body) {
StringBuilder disposition = new StringBuilder("form-data; name=");
appendQuotedString(disposition, name);
if (filename != null) {
disposition.append("; filename=");
appendQuotedString(disposition, filename);
}
return create(Headers.of("Content-Disposition", disposition.toString()), body);
}
此时开始初步仿照web中提交form表单数据的格式进行封装。将name和fileName封装到Header对象中。
Part类的create方法的源码是:
public static Part create(Headers headers, RequestBody body) {
return new Part(headers, body);
}
将header对象和body对象传递给part对象,然后将part对象放入Builder的字段parts集合中。
首先看添加File数据addFormDataPart方法的源码:
public Builder addFormDataPart(String name, String filename, RequestBody body) {
return addPart(Part.createFormData(name, filename, body));
}
首先将File对象得到body对象。下面的方法addPart和Part类的createFormData方法都已经讲解过。
添加File数据的流程如下:
1,首先将File对象得到body对象。
2,将name和fileName封装到Header对象中。
3,将header对象和body对象传递给part对象,然后将part对象放入Builder的字段parts集合中。
最后看Builder类的build方法:
public MultipartBody build() {
return new MultipartBody(boundary, type, parts);
}
创建MultipartBody对象,将三个参数传递过去。
boundary和type作用是封装数据格式,parts中封装了header和body数据。
2,分析MultipartBody类的WriteTo方法
方法的源码是:
@Override public void writeTo(BufferedSink sink) throws IOException {
writeOrCountBytes(sink, false);
}
writeOrCountBytes方法的源码如下:
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
long byteCount = 0L;
Buffer byteCountBuffer = null;
for (int p = 0, partCount = parts.size(); p < partCount; p++) {//遍历part集合
Part part = parts.get(p);
Headers headers = part.headers;
RequestBody body = part.body;
sink.write(DASHDASH);//写数据格式字符
sink.write(boundary);
sink.write(CRLF);
if (headers != null) {
for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
sink.writeUtf8(headers.name(h))
.write(COLONSPACE)
.writeUtf8(headers.value(h))
.write(CRLF);//写Header数据
}
}
MediaType contentType = body.contentType();
if (contentType != null) {
sink.writeUtf8("Content-Type: ")
.writeUtf8(contentType.toString())
.write(CRLF);
}
long contentLength = body.contentLength();
if (contentLength != -1) {
sink.writeUtf8("Content-Length: ")
.writeDecimalLong(contentLength)
.write(CRLF);
} else if (countBytes) {
byteCountBuffer.clear();
return -1L;
}
sink.write(CRLF);
if (countBytes) {
byteCount += contentLength;
} else {
body.writeTo(sink);//写body数据
}
sink.write(CRLF);
}
sink.write(DASHDASH);//写数据格式字符
sink.write(boundary);
sink.write(DASHDASH);
sink.write(CRLF);
if (countBytes) {
byteCount += byteCountBuffer.size();
byteCountBuffer.clear();
}
return byteCount;
}
MultipartBody类的WriteTo方法稍微有些复杂,但这部分代码是上传数据的关键,值得我们研究学习。