本文是的前一篇文章 Okhttp IO 之 Segment & SegmentPool 的基础上写的,如果你没看懂前面的文章,那么看本文会相当的吃力,因为很多关键的代码都是在前面这篇文章中剖析的。

ByteString

okio 中添加一个类 ByteString,顾名思义就是字节串,这里做一个概要的讲解,具体的实现大家可以去看源码。

既然是字节串,它内部就是用一个字节数组支持的。

final byte[] data;

既然用字节数组支持的,那么就可以用一个字节数组来构造,当然还可以用 String,甚至还可以用 NIOByteBufferInputStream 来构造。

既然名字与 String 沾边,也可以像 String 那样进行比较和查询。

当然它的功能不止于此,ByteString 还可以把字节进行编码,例如 md5(),还可以为 URL 进行 URL-safe Base64 转换。

public String base64Url() {return Base64.encodeUrl(data);
}

Source

SourceInputStream 对应,都代表字节输入流。

public interface Source extends Closeable {long read(Buffer sink, long byteCount) throws IOException;Timeout timeout();@Override void close() throws IOException;
}

Source 接口比较简单,只定义了如何从 Buffer 中读取字节。

Bufferokio 的类,既可以当作输入源,也可以当作输出源,后面会详细说明。

Source 接口还加入了一个特色的方法 timeout(),用来规定从输入源读取超时的时间。

Okio 的设计者为了支持 Java IOJava NIOSocket,提供了一个工具类 Okio 来把它们转化为 Source

public static Source source(final InputStream in) {}public static Source source(File file) throws FileNotFoundException {}public static Source source(Path path, OpenOption... options) throws IOException {}public static Source source(final Socket socket) throws IOException {}

BufferedSource

BufferedSource 接口继承于 Source 接口。

  public interface BufferedSource extends Source, ReadableByteChannel {}

从继承关系,它还继承了 NIOReadableByteChannel,也就是说它支持 ByteBuffer 传输数据。

从命名看,它提供了缓存功能,但是这个缓存并不像传统的 Java IO 一样,它用 Buffer 类来代替传统的字节数组。

  /** Returns this source's internal buffer. */Buffer buffer();

Buffer 为何能当作缓存用,后面会说到。

如果你以为 BufferedSource 只是像 Java IOBufferedInputStream 一样提供了单一的缓存功能,那你就错了。

  1. 提供了 ByteArrayInputStream 读取字节数组的方法 read(byte[] sink)
  2. 提供了 DataInputStream 的读取基本类型和String的方法,例如 readInt(), readString(), readUtf8()
  3. 提供了 BufferedReader 特有的 readLine() 方法,只不过在 BufferedSource 中,它的方法名为 readUtf8Line(), readUtf8LineStrict(), readUtf8LineStrict()
  4. 还提供了读取 okioByteString 的方法,readByteString(),读取 okio 的输出流 Sink 的方法 readFully(Buffer sink, long byteCount)readAll(Sink sink)
  5. 还提供了转化为 InputStream 的接口。

Sink

public interface Sink extends Closeable, Flushable {void write(Buffer source, long byteCount) throws IOException;@Override void flush() throws IOException;Timeout timeout();@Override void close() throws IOException;
}

Sinkwrite() 方法指定了输出源只能是 okioBuffer 类。

BufferedSink

BufferedSink 接口继承自 Sink 接口,它也是用 okioBuffer 类实现缓存

public interface BufferedSink extends Sink, WritableByteChannel {Buffer buffer();
}

BufferedSinkBufferedSource 提供的功能是对应的,这里就不细述了。

这里我们需要注意一点, BufferedSink 还继承了 WritableByteChannel,因此它支持 ByteBuffer 操作。

Buffer

重点来了,Bufferokio 的集大成者,为何这么说呢?

public final class Buffer implements BufferedSource, BufferedSink, Cloneable {}

Buffer 居然同时实现了 BufferedSourceBufferedSink

Buffer 接收数据

首先,我们把 Buffer 当作是输出源,先看下最基本的方法,如何写入字节数组。

@Overridepublic Buffer write(byte[] source) {if (source == null) throw new IllegalArgumentException("source == null");return write(source, 0, source.length);}@Overridepublic Buffer write(byte[] source, int offset, int byteCount) {if (source == null) throw new IllegalArgumentException("source == null");// 检测参数的合法性checkOffsetAndCount(source.length, offset, byteCount);// 计算 source 要写入的最后一个字节的 index 值int limit = offset + byteCount;while (offset < limit) {// 获取循环链表尾部的一个 SegmentSegment tail = writableSegment(1);// 计算最多可写入的字节int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);// 把 source 复制到 data 中System.arraycopy(source, offset, tail.data, tail.limit, toCopy);// 调整写入的起始位置offset += toCopy;// 调整尾部Segment 的 limit 位置tail.limit += toCopy;}// 调整 Buffer 的 size 大小size += byteCount;return this;}

在上篇文章中说过 Buffer 是会形成一个循环双向链表的,那么这个写字节数组的原理就很清楚了,循环地获取尾部结点 Segment,然后向其中写入数据,直到数据写完为止。

看下 writableSegment() 是如何获取链表尾部的 Segment 的。

  /*** Returns a tail segment that we can write at least {@code minimumCapacity}* bytes to, creating it if necessary.*/Segment writableSegment(int minimumCapacity) {if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();// 如果链表的头指针为null,就会SegmentPool中取出一个if (head == null) {head = SegmentPool.take(); // Acquire a first segment.return head.next = head.prev = head;}// 获取前驱结点,也就是尾部结点Segment tail = head.prev;// 如果一个字节也不能读,或者不是拥有者if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {// 从SegmentPool中获取一个Segment,插入到循环双链表当前结点的后面tail = tail.push(SegmentPool.take()); // Append a new empty segment to fill up.}return tail;}

参数int minimumCapacity代表获取的 Segment 最少要能写多少个字节进去。

首先判断链表的头指针 head 是否为 null,如果为 null 就从 SegmentPool 中获取一个,然后形成循环双链表。

如果 head 不为 null,就获取前驱结点,也就是尾部结点。 为何要获取尾部结点?因为要写入数据嘛,肯定使用后入式。

获取到尾部 Segment后会有两个判断
1. 是否能写入参数中规定的最少的字节数
2. 这个 Segment 是底层数组的拥有者。 只有是拥有者,才有权力修改底层数组的值。

如果不满足这两个条件,证明获取到这个尾部Segment不合格,就要调用 SegmentPool.take() 再次获取一个 Segment,然后插入到循环链表的尾部,怎么插入的? 调用尾部 Segmentpush() 方法,这个方法我在前面文章中讲述了原理。

当你了解了 writableSegment() 获取链表尾部结点的原理后,通过源码就很容易理解很多向Buffer写入数据的方法,例如 write(ByteBuffer source).

然而,有两个方法,我看了后很不舒服

  @Override public long writeAll(Source source) throws IOException {if (source == null) throw new IllegalArgumentException("source == null");long totalBytesRead = 0;for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {totalBytesRead += readCount;}return totalBytesRead;}@Override public BufferedSink write(Source source, long byteCount) throws IOException {while (byteCount > 0) {long read = source.read(this, byteCount);if (read == -1) throw new EOFException();byteCount -= read;}return this;}

write 类的方法,指的是向当前的 Buffer 中写入数据,而这两个方法从实现的角度看,明明是写出数据好吧? 这样命名我真没看懂。

最后我们来看一个方法,这个方法被人们传的很神,搞得我开始还以为 okio 可以替代 Java IO 来使用。这个方法就是用来Buffer 之间数据传递的 write(Buffer source, long byteCount)

@Override public void write(Buffer source, long byteCount) {if (source == null) throw new IllegalArgumentException("source == null");if (source == this) throw new IllegalArgumentException("source == this");checkOffsetAndCount(source.size, 0, byteCount);while (byteCount > 0) {// Is a prefix of the source's head segment all that we need to move?// 如果 Source Buffer 的头结点可用字节数大于要写出的字节数if (byteCount < (source.head.limit - source.head.pos)) {// 获取当前 Buffer 的尾部结点Segment tail = head != null ? head.prev : null;// 如果尾部结点有足够空间可以写数据,并且这个结点是底层数组的拥有者,就直接向尾部结点中写数据,然后就结束了if (tail != null && tail.owner&& (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {// Our existing segments are sufficient. Move bytes from source's head to our tail.source.head.writeTo(tail, (int) byteCount);source.size -= byteCount;size += byteCount;return;} else { // 如果不满足前面的情况,就把 Source Buffer 的头结点分割为两个 Segment,然后头指针指向分割后的第一个Segment// We're going to need another segment. Split the source's head// segment in two, then move the first of those two to this buffer.source.head = source.head.split((int) byteCount);}}// Remove the source's head segment and append it to our tail.Segment segmentToMove = source.head;long movedByteCount = segmentToMove.limit - segmentToMove.pos;// 头结点从 Source Buffer 的链表中移除source.head = segmentToMove.pop();// 如果头结点为 null, 直接改变指针位置即可if (head == null) {head = segmentToMove;head.next = head.prev = head;} else { // 如果头指针不为 null,那就把 Source Buffer 的 head 加入到 Sink Buffer 的链表Segment tail = head.prev;tail = tail.push(segmentToMove);// 加入链表后,尝试合并尾部的两个结点tail.compact();}source.size -= movedByteCount;size += movedByteCount;byteCount -= movedByteCount;}}

这个方法在源码中有大量的注释,因为是 okio 的核心所在。 现在我对这个注释进行下翻译,为后面分析代码作准备。

write(Buffer source, long byteCount) 是把 Buffer source 中的数据移动到当前 Buffer 的链表尾部的 Segment中。 注意,是移动,不是复制,就是这一点,经常被外界夸大。

用移动而不是复制,是为了平衡两个冲突点:CPU 和 内存。 我们往往会为了性能牺牲内存,或者为了内存牺牲性能。

复制大量数据是一个很昂贵(expensive)的操作,而移动数据,就只是修改修改指针而已,所以这就避免浪费了CPU。

为了节约内存,规定相邻的两个Segment,它们各自的数据填充度至少应该为 50%,如果都少于 50%,会合并这两个结点。 当然,由于是循环链表,头结点和尾结点在理论上说是相邻的,但是它们不能参与合并,因为头结点是为了读数据,尾结点是为了写数据,如果参与合并就乱套了。

那么怎么移动数据呢?有三种情况
1. 假如说 Source Buffer 的链表为 [100%, 2%],而 Sink Buffer 的链表为 [99%, 3%],那么移动进行链表的移动后,Sink Buffer 就变为了 [100%, 2%, 99%, 3%]
2. 假如说 Source Buffer 的链表为 [100%, 40%],而 Sink Buffer 的链表为 [30%, 80%],那么移动后的结果为 [100%, 70%, 80%]。 第三个结点 [30%] 被合并到了前驱结点 [40%] 中去了,然后 [30%] 这个结点被回收了。
3. 假如说从 Source Buffer 中的头结点的 Segment 的填充度是 [100%],我现在只想复制 30% 的数据出去,而 Sink Buffer 的尾部结点不能写(因为不是拥有者)或者空间紧张而不够写,那怎么办呢? 可以把这个 Segment 切分为两个 Segemnt,填充度分为另 [30%][70%],然后把这个 [30%] 移动到 Sink Buffer 的链表中。

那么有人会问,第一种情况下,为何不选择合并 [2%] 和 [99%] 呢,因为就算合并了,也不能合并为一个,这样就不能回收结点,也就不能起到节约内存的目的。 那么有人可能又会问,那我后面的数据一直往前移动,总能回收几个结点吧? 理论是没错,但是这样一样,大量的复制数据岂不是过度浪费CPU了?

现在理解了原理,敢不敢跟着我的注释,去挑战下源码呢?

Buffer 读数据

Buffer 当作输入源,就可以读数据,首先看下把数据读到字节数组中

    public int read(byte[] sink, int offset, int byteCount) {checkOffsetAndCount(sink.length, offset, byteCount);Segment s = head;if (s == null) return -1;int toCopy = Math.min(byteCount, s.limit - s.pos);System.arraycopy(s.data, s.pos, sink, offset, toCopy);s.pos += toCopy;size -= toCopy;if (s.pos == s.limit) {head = s.pop();SegmentPool.recycle(s);}return toCopy;}

原理就是字节数组之间的复制。

最后看一个 Buffer 之间的数据读取

    @Override public long read(Buffer sink, long byteCount) {if (sink == null) throw new IllegalArgumentException("sink == null");if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);if (size == 0) return -1L;if (byteCount > size) byteCount = size;sink.write(this, byteCount);return byteCount;}

原来,它就是复用前面讲到的 Buffer 之间写数据方法来完成 Buffer 之间数据的读取。

结束

网上大量文章一直传着 okioIO 操作是数据的移动而不是复制,看完本文你搞清楚了吗? 它其实指的是在 Buffer 之间传输数据。 而其它的操作,其实都只是建立在 Java IO, Java NIOSocket 之上的。 okioJava IO/NIO 好吗? 彼此彼此吧,关键看用到哪了。

本文把最基础的东西剖析了下,但是 okio 并不止于此。本篇文章是为了后面分析 Okhttp 源码中的 okio 操作做准备的。

最后附上一张 okio 的关系图

Okhttp 之 okio相关推荐

  1. android okhttp3 okio,OkHttp和Okio

    OkHttp和Okio 文本将介绍OkHttp和Okio基本使用 OkHttp HTTP 是现在APP访问网络最流行的方式.通过它我们可以交换数据和媒体信息.而高效的使用HTTP可以让你的加载数据更快 ...

  2. OkHttp的Okio在CacheInterceptor中的应用

    目录 Okio的诞生 OKio的简单介绍 缓存模块 超时机制 几个重要的类 简单的读写操作 一个简单的java+socket来实现请求服务器 在CacheInterceptor的运用 1)写请求的头部 ...

  3. OKHttp之OkIO

    OKIO的核心:Sink和Souce okio的本质是对InputStream和OutputStream做了进一步封装 内部通过Segment组成的双向链表来持有数据

  4. Android Https相关完全解析 当OkHttp遇到Https

    http://blog.csdn.net/lmj623565791/article/details/48129405: 本文出自:[张鸿洋的博客] 1. 概述 其实这篇文章理论上不限于okhttp去访 ...

  5. OkHttp实现文件上传进度

    文件上传就一个没刻度的进度条在那里转怎么行,本篇带你实现上传进度,为你的进度条添加刻度吧,啥都不说了,重点重写RequestBody,看代码 import com.squareup.okhttp.*; ...

  6. android okio使用方法,Android 开源框架 Okio 原理剖析

    Retrofit,OkHttp,Okio 是 Square 团队开源的安卓平台网络层三板斧,它们逐层分工,非常优雅地解决我们对网络请求甚至更广泛的 I/O 操作的需求.其中最底层的 Okio 堪称小而 ...

  7. Android—OkHttp同步异步请求过程源码分析与拦截器

    OkHttp同步请求步骤: 创建OkHttpClient,客户对象 创建Request,请求主体,在请求主体设置请求的url,超时时间等 用newCall(request)将Reuqest对象封装成C ...

  8. 【Okio】Okio 简单入门

    1.概述 好文章:https://www.cnblogs.com/leipDao/p/10521844.html 好文章:https://segmentfault.com/a/119000001270 ...

  9. OkHttp和Volley对比

    OkHttp 物理质量 使用OkHttp需要 okio.jar (80k), okhttp.jar(330k)这2个jar包,总大小差不多400k,加上自己的封装,差不多得410k. 功能介绍 Squ ...

最新文章

  1. STM32下SysTick的一个容易发生的错误,时钟频率设置
  2. VC中按钮控件的启用(enable)和禁用(disable)
  3. Java基础题笔记(数组)4
  4. nginx(三)反向代理和负载均衡
  5. 无心剑中译马塞尔·普鲁斯特《追忆似水年华》
  6. 多线程,多进程实例对比
  7. python+源码如何编译安装mysql_Python源码安装cx_Oracle
  8. Javascript的简单介绍,只作为个人笔记,不作为知识参考,如果想要学习,请找其他文章
  9. 用struts拦截器实现登录验证功能AuthorizationInterceptor
  10. 321影音 多功能播放器
  11. RTMP播放器网页互联网直播音视频流媒体播放器EasyPlayer-RTMP-iOS播放H265格式的视频源
  12. 企业绩效考核管理制度
  13. 计算机毕业设计Java幼儿园管理系统(源码+系统+mysql数据库+Lw文档)
  14. 核心单词Word List 38
  15. 886. 可能的二分法
  16. IDE添加文件头@author信息
  17. 王垠:怎样尊重一个程序员
  18. 新生儿喝奶后不要马上放回床上睡觉,为宝宝健康着想,先做1件事
  19. Windows挂载Linux网络共享文件夹
  20. Windows电脑蓝牙打电话-预研总结

热门文章

  1. 高速数据采集卡合并超宽带高速记录回放系统
  2. 90 年代的网页设计,有点搞笑
  3. 虚拟机CentOS7.0安装万能五笔的方法
  4. Android 简单的侧边栏A-Z
  5. RPC服务器不可用,问题解决
  6. 彩虹世界未能连接到服务器,彩虹世界免费资源-彩虹世界新版基遇免费资源官网链接 v1.0预约_手机乐园...
  7. 浅谈微信抢房中的软件操作逻辑
  8. libreoffice的启动、测试和问题记录
  9. 关闭单节点oracle,oracle rac 如何正确的删除单个节点的actionlist
  10. GIS、遥感、水文、地理常用数据介绍及下载网址(2),补充~