OKHttp原始碼分析(二)之RequestBody
一,概述
在上篇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方法稍微有些複雜,但這部分程式碼是上傳資料的關鍵,值得我們研究學習。