0%

Okio解决了什么?

传统Blocking IO存在什么问题?

传统BIO不管是读入还是写出,缓冲区的存在必然涉及copy的过程,而如果涉及双流操作,比如从一个输入流读入,再写入到一个输出流,那么这种情况下,在缓冲存在的情况下,数据走向是:

-> 从输入流读出到缓冲区

-> 从输入流缓冲区copy到 b[]

-> 将 b[] copy 到输出流缓冲区

-> 输出流缓冲区读出数据到输出流

上面情况存在冗余copy操作。

Okio解决了什么?

Okio减少了冗余的复制。

Okio从输入流读取到的数据用一个Segment链表存储,写入数据会把Segment链表中的结点拿过来,不用做中间拷贝。

Segment 实际上也是对 byte[] 进行封装,再通过各种属性来记录各种状态。在读写时,如果可以,将Segment整体作为数据传授媒介,这样就没有具体数据的copy过程,而是交换了对应的Segment引用,这是减少数据copy进而交换数据的关键。

Okio并没有打算优化底层IO方式以及替代原生IO方式,Okio优化了缓冲策略以减轻内存压力和性能消耗,并且对于部分IO场景,提供了更友好的API。

Okio内部实现

Okio底层还是用Java标准的io流来操作,只是缓存机制是自己实现了一套,避免了减少一次中间拷贝过程。

不管是RealBufferedSource还是RealBufferedSink,内部本质上都是通过Buffer这个类来实现缓存功能,而Buffer内部又是通过Segment这个核心类来缓存读取到的数据,Buffer就是一个管理者用来调度Segment。

Segment是双向链表中的一个结点,Buffer类存储了链表的头结点。

Segment中有个byte数组存储了真正的数据,最大字节数是8K,还有一些指针,表明了有效数据的范围。

有数据超过8K,就会创建新的Segment,跟前面的Segment连起来

1
2
3
4
BufferedSource bufferedSource = Okio.buffer(Okio.source(src));
BufferedSink bufferedSink = Okio.buffer(Okio.sink(dest));
bufferedSink.writeAll(bufferedSource);
bufferedSink.close();
  1. 先从原始的Source对象读数据,到RealBufferedSource的buffer对象,通过尾插法创建Segment,数据依次填满一个个的Segment对象里的byte数组。
  2. RealBufferedSource中的buffer中的Segment转移到RealBufferedSink中的buffer中
  3. 最后遍历RealBufferedSink的buffer中Segment链表写到OutputStream中

(1) Source中需要传递的数据是”满”的情况,也就是8k都是有效数据,这种情况直接从source的buffer中拿到Segment,然后添加到sink的buffer上即可,和java io流相比,省去了中间的一次临时buffer拷贝,从而提高的读写效率

(2) Source中需要传递的数据不”满”的情况,通过pos和limit可以定位到有效数据区间,和Sink中buffer的尾Segment有效数据进行对比,如果两个Segment中的有效数据可以合并到一个Segment中那么会进行数据整理,多余的Segment会被回收到。

如果两个Segment的有效数据总和超过8k,那么直接将Source中的Segment链接到Sink中buffer的尾部即可。

(3) Source的buffer中的Segment只是传递部分数据,如5K的数据值传递其中2K,okio内部会通过split方法将Segment分成2K和3K两个Segment,然后将2K的Segment参照第二种情况和Sink中的Segment进行合并。