
深秋
大家一定听说或者使用过鼎鼎大名的OKHttp和Retrofit,它们内部都调用了Okio。
它们三个都是Square团队开源的库,主要处理I/O操作和网络请求。
今天我们就来读下Okio的源码,看看Okio有什么神奇之处。
Okio是什么?
Okio是一个完善java.io和java.nio的库,可以更方便的读、写、处理数据。
看到上面的官方介绍,我们可以发现两个重点:
1.完善java.io和java.nio;
2.Okio的主要功能是,读、写、处理数据。
再深入思考下,java.io和java.nio有哪些尚未完善的地方?Okio有做了哪些优化?
java.io能处理所有的IO信息,但是还是有些不完美之处:
1.需要自己去管理byte数组;
2.java.io类和继承关系过于复杂,使用起来更加不便;
3.java.io操作可能会某些原因读取速度特别慢,甚至一直等待无法返回;
4.操作字符流和字节流方法不同,需要区别对待;
5.涉及到多个流之间的数据传递,需要反复拷贝,效率不高。
被Android官方青睐的IO库,Okio一一解决了这些问题:
1.使用ByteString去封装byte数组;
2.简化类关系,主要使用Buffer、Sink、Source等类,相互关系简单;
3.使用Timeout去控制超时;
4.可用相同方法去操作字符流和字节流;
5.提高多个流之间的数据传递速度。
带着这些问题,我们继续前进。
ByteString
记得我最开始编程时,C语言中没有字符串类型,只能使用char*的方式存储字符串,而且经常涉及数组和指针的转换,
需要使用memcpy和memset等等函数,容易出错,而且还需要加入大量的检测。
后面使用面向对象语言后,发现用String对象直接管理字符串,良好的封装char数组的常用方法,使用起来各种清爽。
同样使用过java.io的开发者,肯定会想起被byte数组笼罩的恐惧。
需要不厌其烦的处理一串串byte数组,完成encode和decode函数,还要特别关注编码问题。
这个时候就希望,能有一个对象来管理byte数组,并且帮我们封装好各种函数,使用时直接调用下即可。
Okio就满足开发者这个愿望,提供了ByteString类,把对字节数组的常用方法都封装好了。
使用Okio库后,开发者可以以ByteString作为最小粒度进行操作。
先来看下ByteString的组成
1 | // 0.实现了Serializable和Comparable接口 |
ByteString的内部也不复杂,主要就是byte数组。
这里要看下String的结构,其实String可以命名为CharString。
因为一般开发中,字符串用的比较多,所以java官方直接命名为String。
1 | // 0.同样实现了Serializable和Comparable接口,额外实现了CharSequence处理常规char数组数据 |
接下去看下,ByteString的读和写
1 | public static ByteString read(InputStream in, int byteCount) throws IOException { |
1 | public void write(OutputStream out) throws IOException { |
Buffer
IO操作的最重要部分就是对流的处理,所以定义好一个流非常关键。
Okio中,就是用Buffer去表示流。

Buffer类关系图
先看下翻译
source:来源;水源;
sink:水槽;洗涤槽;
一下子就明白了Source表示输入源,Sink表示输出源。
对应java.io中的inputStream和outputStream。
1 | public final class Buffer implements BufferedSource, BufferedSink, Cloneable { |
size表示Buffer中的字节数量。
主要成员变量也非常简单,就是segment。
源码上注释非常简洁清晰:A segment of a buffer,翻译过来就是Buffer的片段。
1 | final class Segment { |
从prev和next就可以发现这是双向链表。在Buffer中就持有head,通过head去访问整个链表。
通过后续的代码阅读,发现这是一个双向循环链表。

segment双向链表
我们分析下Okio中是如何写入和读取byte数据的。
Buffer.writeByte
先来看看Buffer怎么样写入一个byte
1 | // 0.注意入参是int格式,不过其中有效数据的只有8 bit |
writeByte函数的代码都很简单,我们继续看看如何寻找可供写入的segment。
Buffer.writableSegment
1 | // 0.入参minimumCapacity表示允许写的最小空间 |
这里引入了SegmentPool这个类,主要用来管理Segment对象,回收旧segment,分配segment。
SegmentPool减少了segment的新建和释放次数,缓解java GC的压力。
注:类似于Handler中message.obtain(),其背后也有一个pool去管理所有的message对象。
SegmentPool
这个类代码很短,全部贴出来
1 | final class SegmentPool { |
segmentPool这个类非常简单,内部维护一个单向列表。
take()会从头部取出segment,recycler()将待回收的segment放回到链表头部。
Buffer.readByte
我们来看下readByte,其方法和writeByte完全对称。
1 | public byte readByte() { |
Source
IO最重要的就是对流的处理,Okio中对应的数据结构是Source和Sink
1 | // 0.Closeable接口中方法为close() |
这是一个接口,后续都是使用其实现类作为输入流。
Okio.Source
我们看下Okio.Source,开发者一般都使用这个函数生成Source。
1 | private static Source source(final InputStream in, final Timeout timeout) { |
以上代码逻辑非常清晰,流水型执行下去即可。
这里出现了一个新的对象timeout,用来管理超时,我们看看其内部结构。
Timeout
1 | public class Timeout { |
Timeout主要用来设置超时时间,对Stream的读取超过指定时间后,认定为失败,开发者需要选择close或者重新操作这个Stream。
注:读取网络Socket流时,有时会陷入无限等待中,需要使用AsyncTimeout。AsyncTimeout会新开线程监听超时,当前线程无限等待也没有关系。
继续看下上面被调用的throwIfReached()
1 | public void throwIfReached() throws IOException { |
Okio.buffer
1 | // 0.这里传入上面通过Okio.source生成的Source |
RealBufferedSource
终于找到开发者最后操作的Source类,先来看下它的成员吧。
1 | // 0.实现BufferedSource接口 |
RealBufferedSource.read
对于输入流来说,我们需要紧紧抓住read方法。
1 | @Override public int read(byte[] sink, int offset, int byteCount) throws IOException { |
主要流程就是,现将source数据读取到buffer中,再将buffer提取到byte数组中。buffer.read()这个函数这里就不展开讨论了,其原理和buffer.readByte非常相近。
Sink
同理,完全对应的,也可以按照Okio.sink-> Okio.buffer(Sink)-> RealBufferedSink-> RealBufferedSink.write()流程走完一遍Okio中关于写的操作。
总结,Okio这个库本身并不复杂,将常用的流和字节数据进行良好的封装,加之源码中相近的注释,阅读起来非常流畅,想必使用起来的体验也会非常棒。