传统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 | BufferedSource bufferedSource = Okio.buffer(Okio.source(src)); |
- 先从原始的Source对象读数据,到RealBufferedSource的buffer对象,通过尾插法创建Segment,数据依次填满一个个的Segment对象里的byte数组。
- RealBufferedSource中的buffer中的Segment转移到RealBufferedSink中的buffer中
- 最后遍历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进行合并。