1. 程式人生 > >OKHttp原始碼分析(二)之RequestBody

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類中核心方法有以下三個:

1public 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方法稍微有些複雜,但這部分程式碼是上傳資料的關鍵,值得我們研究學習。