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方法稍微有些复杂,但这部分代码是上传数据的关键,值得我们研究学习。

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值