上节介绍了okio中几个重要的对象,但还没有对他们进行深入地分析。本结将其深入源码对他们进行详细的分析。首先回顾下几个重要的对象
-
sink和source:定义了两个接口,此接口是对原有java io中outputStream和inputStream的替代(实际上是做了二次封装)
-
Buffer:缓冲,内部是一个由segment组成的双向循环链表
-
BufferedSink和BufferedSource:带缓冲功能的sink和source接口
-
RealBufferedSink和RealBufferedSource:BufferedSink和BufferedSource的实现类,内部依赖Buffer和sink(source)对象
下面先重点分析底层几个关键的类
Segement
segment是Buffer内部的数据存储对象,它的结构如下图所示:

//存储数据的数组
final byte[] data;
//数据可读的位置
int pos;
//数据可写的位置
int limit;
//表示是否是可分享的。如果data引用其他segment的data数组或此segment的data数组被其他segment的data数组引用,shared为true
boolean shared;
//表示独占data数组,即data数组不被其他segment引用或此segment也不引用其他segment的data数组
boolean owner;
//指向下一个segment的指针
Segment next;
//指向上一个segment的指针
Segment prev;
对于参数shared如果为true则不能再写数据了,只能读取。提供segment共享功能的目的在与数据的复用,节省开销。segment有三个构造函数,第一种创建的segment是独占的
Segment() {
this.data = new byte[SIZE];
this.owner = true;
this.shared = false;
}
后面的方式创建的segment是共享的,数据与其他segment共享
Segment(Segment shareFrom) {
this(shareFrom.data, shareFrom.pos, shareFrom.limit);
shareFrom.shared = true;
}
Segment(byte[] data, int pos, int limit) {
this.data = data;
this.pos = pos;
this.limit = limit;
this.owner = false;
this.shared = true;
}
segment对外提供的方法并不多,下面对几个方法做下介绍。
pop:此方法就是将当前segment元素移出链表,并返回下一个setment元素,方法比较简单,就是链表的一些常规操作,这就不细说了。
public Segment pop() {
Segment result = next != this ? next : null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
return result;
}
push:此方法将segment加入当前segment的后面
public Segment push(Segment segment) {
segment.prev = this;
segment.next = next;
next.prev = segment;
next = segment;
return segment;
}
split:此方法主要是将头结点segment分裂为两个segment,第一个segment的数据为原节点数据[pos--pos+byteCount]范围内的数据,第二个节点为原节点数据[pos+byteCount--limit]范围内的数据。
public Segment split(int byteCount) {
if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
Segment prefix;
//如果要分割出字节数大于了最小分享门限,则通过分享的方式,创建一个segment,与原segment共享底层的data数组
if (byteCount >= SHARE_MINIMUM) {
prefix = new Segment(this);
} else {
//小于最小分享门限,则通过复制的方式,将原数据复制byteCount个字节到新的segment
prefix = SegmentPool.take();
System.arraycopy(data, pos, prefix.data, 0, byteCount);
}
//修改分割出去的segment数据
prefix.limit = prefix.pos + byteCount;
pos += byteCount;
//将分割出来的segment加到当前segment之前
prev.push(prefix);
return prefix;
}
此过程对于初次接触的同学来说可能有点难理解,画了一张图来描述此过程:

writeTo:此方法将当前segment中bytecCount数量的数据移动到另一个segement sink中去。
public void writeTo(Segment sink, int byteCount) {
if (!sink.owner) throw new IllegalArgumentException();
if (sink.limit + byteCount > SIZE) {
if (sink.shared) throw new IllegalArgumentException();
//如果sink中存不下byteCount数量的数据,则先将sink中的数据移动到pos位置为0处
if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
sink.limit -= sink.pos;
sink.pos = 0;
}
//将当前segment中数据复制到到sink中
System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
sink.limit += byteCount;
pos += byteCount;
}
}
compact:当链表尾部的元素和它的上一个元素中的数据都没有达到一半时,调用此方法将会把尾元素中的内容复制到他的上一个元素中去,并移除掉尾元素。
public void compact() {
if (prev == this) throw new IllegalStateException();
if (!prev.owner) return; // Cannot compact: prev is not writable.
int byteCount = limit - pos;
//可移动空间
int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
//如果要移动数据大于了可用空间,则返回
if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
//将当前元素的内容复制到上一个元素中去
writeTo(prev, byteCount);
//将自己弹出链表
pop();
//回收当前segment
SegmentPool.recycle(this);
}
上面就是segment提供的几个方法,这些方法都比较简单,不必深入代码细节,知道每个方法的作用即可。后续很多上层的操作都会用到这些方法。
SegmentPool
从名字上就不难理解这个类是用来干啥的了,提供Segment对象的创建和回收,避免垃圾回收产生性能损耗,其内部是有Segment元素组成的单链表结构,如下图所示:

static final long MAX_SIZE = 64 * 1024; // 64 KiB.
除此之外,还提供了两个方法take和recyle分别表示获取和回收segment元素,take方法源代码如下,方法很简单,就是一个单链表的操作,描述了获取链表头部元素的操作:
static Segment take() {
synchronized (SegmentPool.class) {
if (next != null) {
Segment result = next;
next = result.next;
result.next = null;
byteCount -= Segment.SIZE;
return result;
}
}
return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
}
recycle方法的源代码,将回收的segment放置在链表头部:
static void recycle(Segment segment) {
if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
if (segment.shared) return; // This segment cannot be recycled.
synchronized (SegmentPool.class) {
if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
byteCount += Segment.SIZE;
segment.next = next;
segment.pos = segment.limit = 0;
next = segment;
}
}
Buffer
前面已经说过RealBufferedSink和RealBufferedSouce内部的接口实现,实际上是依赖于Buffer和具体的Sink和Source。Buffer提供缓冲功能,sink和souce实现对象完成具体的数据传输。Buffer类中有个重要的方法,write(Buffer source, long byteCount),此方法是一个比较底层的方法,完成将source缓冲中的数据从头结点开始,移动byteCount数量到当前Buffer的尾部中去。此方法的实现有它独特的地方,源码及注释如下:
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) {
//如果移动的字节数小于要移动的元素的数据大小
if (byteCount < (source.head.limit - source.head.pos)) {
Segment tail = head != null ? head.prev : null;
//如果当前buffer的尾节点有足够的空间
if (tail != null && tail.owner
&& (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
//将source头结点中的元素移动byteCount到尾节点中
source.head.writeTo(tail, (int) byteCount);
source.size -= byteCount;
size += byteCount;
return;
} else {
//如果尾节点不能存储元素,则先将source头节点分割成两个segment,其中prefix节点大小为byteCount
source.head = source.head.split((int) byteCount);
}
}
//获取到要移动的segment
Segment segmentToMove = source.head;
//获取要移动segment的数据大小
long movedByteCount = segmentToMove.limit - segmentToMove.pos;
//将要移动的segment从source中移除,并将source的head指针指向下一个元素
source.head = segmentToMove.pop();
//如果当前buffer为空,则将当前buffer的head指针指向要移动的segment
if (head == null) {
head = segmentToMove;
head.next = head.prev = head;
} else {
Segment tail = head.prev;
//将移动的segment加入到链表尾部
tail = tail.push(segmentToMove);
//对尾部元素进行压缩
tail.compact();
}
//修改相关数据参数
source.size -= movedByteCount;
size += movedByteCount;
byteCount -= movedByteCount;
}
}
从上面的方法可以看出,在Buffer中传输数据时,实际上并没有发生数据的复制和转移,仅仅只是segment的指针发生了变化而已,通过引用指针的变化,就完成了数据在不同Buffer间的传输,这是其高性能的重要原因之一,也是值得我们平时在写代码的过程借鉴的。 有了上面这些重要的介绍,我们来详细完整的分析一个数据在底层的传输过程。上节我们以请求头的传输为例简单大概地介绍了一下数据的传输,跟踪到RealBufferedSink类中的writeUtf8(String string)方法后就点到为止了,下面将深入代码细节,看看究竟是怎么完成数据传输的。
public BufferedSink writeUtf8(String string) throws IOException {
if (closed) throw new IllegalStateException("closed");
//将传输的内容写入到segment链表中,此方法涉及到很多utf8编码的细节,就不展开说了,最终都会将内容转换成对应的字节存储
buffer.writeUtf8(string);
//将segment中的数据传输出去
return emitCompleteSegments();
}
此方法中出现了一个emitCompleteSegments()方法,你会看到此方法机会在类中的每个方法中都会出现,它负债完成数据从segment缓存到目的端的具体传输过程。
public BufferedSink emitCompleteSegments() throws IOException {
if (closed) throw new IllegalStateException("closed");
//返回当前buffer中写入的字节数(即可传输的字节数)
long byteCount = buffer.completeSegmentByteCount();
//通过流对象,将buffer中的数据传输到目的端
if (byteCount > 0) sink.write(buffer, byteCount);
return this;
}
上述代码中的最后的sink.write(buffer, byteCount)中的sink,是在创建RealBufferedSink对象是传入的,即sink = Okio.buffer(Okio.sink(rawSocket)),Okio.sink的代码如下
public static Sink sink(Socket socket) throws IOException {
if (socket == null) throw new IllegalArgumentException("socket == null");
AsyncTimeout timeout = timeout(socket);
//创建了一个匿名对象
Sink sink = sink(socket.getOutputStream(), timeout);
return timeout.sink(sink);
}
来看看创建sink对象的代码
private static Sink sink(final OutputStream out, final Timeout timeout) {
if (out == null) throw new IllegalArgumentException("out == null");
if (timeout == null) throw new IllegalArgumentException("timeout == null");
return new Sink() {
//将buffer中的数据写入到目的端
@Override public void write(Buffer source, long byteCount) throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {
timeout.throwIfReached();
//从头结点开始传输
Segment head = source.head;
int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
//这里的out是socket对象的outputStream,调用其write方法来完成最后的数据传输
out.write(head.data, head.pos, toCopy);
//修改相关参数
head.pos += toCopy;
byteCount -= toCopy;
source.size -= toCopy;
//segment中的数据完成传输后,回收segment
if (head.pos == head.limit) {
source.head = head.pop();
SegmentPool.recycle(head);
}
}
}
@Override public void flush() throws IOException {
out.flush();
}
@Override public void close() throws IOException {
out.close();
}
@Override public Timeout timeout() {
return timeout;
}
@Override public String toString() {
return "sink(" + out + ")";
}
};
}
上面的write方法中我们可以看出,数据的最后传输还是依赖于socket对象outputStream,框架做了那么多工作其实都是在对out.write(head.data, head.pos, toCopy)这段代码的封装。整个库的核心在于其缓存的实现,通过引入缓存来减少io发生的次数,从而提高效率。 实际上这里的sink,除了完成网络的处理,还可以完成文件的处理,这里就不具体分析了。实际上流程都是一样的。
public static Sink sink(File file) throws FileNotFoundException {
if (file == null) throw new IllegalArgumentException("file == null");
return sink(new FileOutputStream(file));
}
总结
本结分析了okio底层几个重要的类。okio在原有的面向流(sink和source)的基础之上提供了面向了缓冲的能力,其缓冲内部依赖于一个双向循环的Segment链表,通过改变数据的指针,而不是操作数据,提高了数据的数据效率。同时避免频繁的创建Segment,提供了SegmentPoll来缓存对象,这些实现细节都是值得我们平时在编码过程中学习的。