TimeOut
提供超时功能,希望IO能在一定时间内进行完毕,否则视为异常。分两种情况,同步超时和异步超时。
同步超时:在每次读写中判断超时条件,因为处于同步方法,因此当IO发生阻塞时,不能及时响应。
异步超时:用单独的线程监控超时条件,如果IO发生阻塞,并且检测到超时,抛出IO异常,阻塞终止。
这部分也不做深入。
/ 缓冲实现 /
假设使用Okio复制一个文件,那么实例代码可能是这样的
/**
* 构造带缓冲的输入流
*/
Source source = null;
BufferedSource bufferedSource = null;
source = Okio.source(new File('yourFilePath'));
bufferedSource = Okio.buffer(source);
/**
* 构造带缓冲的输出流
*/
Sink sink = null;
BufferedSink bufferedSink = null;
sink = Okio.sink(new File('yourSaveFilePath'));
sink = Okio.buffer(sink);
int bufferSize = 8 * 1024; // 8kb
// 复制文件
while (!bufferedSource.exhausted()){
// 从输入流读取数据到输出流缓冲
bufferedSource.read(
bufferedSink.buffer(),
bufferSize
);
// 输出流缓冲写出
bufferedSink.emit();
}
source.close();
sink.close();
上面代码中,Okio.source() 和 Okio.sink() , Source 接收的输入流为 FileInputStream, Sink接收输出流为FileOutputStream。Okio.buffer 和 Okio.sink分别返回 RealBufferedSource, 和 RealBufferedSink,Buffer作为这两个类的成员变量存在,在实例化时初始化,这部分代码不贴出。主要看 RealBufferedSource.read()。
public long read(Buffer sink, long byteCount) throws IOException {
// 用来接收数据的Buffer 不能为空
if (sink == null) throw new IllegalArgumentException('sink == null');
// 读取数据不能为负数
if (byteCount < 0) throw new IllegalArgumentException('byteCount < 0: ' + byteCount);
if (closed) throw new IllegalStateException('closed');
// 缓冲区没有数据了
if (buffer.size == 0) {
// 从输入流中读取数据
long read = source.read(buffer, Segment.SIZE);
if (read == -1) return -1;
}
// 比较 byteCount 与 缓冲中的数据容量,得到到实际要读取的数据量
long toRead = Math.min(byteCount, buffer.size);
// 从Buffer 中读取数据
return buffer.read(sink, toRead);
}
与Java原生的缓冲方式类似,都先考虑缓冲区中的数据情况,如果缓冲区中没有数据,则先向流读取数据填充缓冲区,再根据所需读取容量与实际缓冲区中存有的数据容量进行读取。这里有一点和Java原生的不同,如果byteCount的数据超出Segment的容量的话,不会直接向流读取。可以看出Okio非常希望以Segment为单位来对流数据进行操作,看接收byte[]为参数的read()的重载方法也最受这个规则。
先看source.read(buffer, Segment.SIZE)。source通过之前Okio.source()得来,见Okio.source()。
private static Source source(final InputStream in, final Timeout timeout) {
// 输入流不能为空
if (in == null) throw new IllegalArgumentException('in == null');
// 超时条件不能为空
if (timeout == null) throw new IllegalArgumentException('timeout == null');
// 匿名内部类Source
return new Source() {
public long read(Buffer sink, long byteCount) throws IOException {
// 获取的byteCount不能为负数
if (byteCount < 0) throw new IllegalArgumentException('byteCount < 0: ' + byteCount);
if (byteCount == 0) return 0;
try {
// 检查是否超时
timeout.throwIfReached();
// 获取尾节点的Segment,尾节点不满足填充新数据条件则拿到新的Segment
// 也是位于尾节点
Segment tail = sink.writableSegment(1);
// 实际从in读取的数据,能看出最大不能超过 Segment.SIZE
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
// 从in中读取数据到Segment.data
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
// 这里说明没有读到有效数据
if (bytesRead == -1) return -1;
// 更新索引位置
tail.limit += bytesRead;
// 更新buffer容量
sink.size += bytesRead;
return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}
......
};
}
从输入流中读取数据,数据存于Buffer中,位于尾节点的Segment,与前面说的一样,单次向流读出操作,大小不能超过Segment.SIZE。回到read(),在确认缓冲区有数据之后,从缓冲区中读取数据到sink,即从Buffer中读取数据到另一Buffer。
public long read(Buffer sink, long byteCount) {
// 用来接收数据的sink不能为空
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;
// 从当前缓冲区,将数据写入到另一缓冲区,即从 this,写到sink
sink.write(this, byteCount);
return byteCount;
}
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);
/**
缓冲区数据从 缓冲区source 移动到 缓冲区this,
在当前的案例中,缓冲区source代表输入流缓冲数据,缓冲区this代表输出流缓冲数据
此函数源码内部有一大段注释,可以细细品味,我就不贴了
*/
while (byteCount > 0) {
if (byteCount < (source.head.limit - source.head.pos)) {
// 进到这里说明,说明source的头节点有足够的数据
// 获取当前缓冲区尾节点
Segment tail = head != null ? head.prev : null;
if (tail != null && tail.owner
&& (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
// 尾节点不为空,并且尾可解是owner状态
// 并且尾节点能够装下byteCount数量的数据
// 将数据从source的头节点 copy 到 当前缓冲区的尾节点
source.head.writeTo(tail, (int) byteCount);
source.size -= byteCount;
size += byteCount;
return;
} else {
// 说明数据当前缓冲区尾节点不能存下byteCount大小的数据
// 将source头节点的Segment分割,byteCount过阀值则共享,否则拷贝
// 共享过程则不用copy
source.head = source.head.split((int) byteCount);
}
}
/**
将source缓冲区的头节点pop,加入到当前缓冲区
*/
Segment segmentToMove = source.head;
long movedByteCount = segmentToMove.limit - segmentToMove.pos;
source.head = segmentToMove.pop();
if (head == null) {
// 进到这里说明当前缓冲区没有数据,将segmentToMove作为头节点
head = segmentToMove;
head.next = head.prev = head;
} else {
// 进到这里则是将segmentToMove放到链表尾部
Segment tail = head.prev;
tail = tail.push(segmentToMove);
tail.compact();
}
/**
更新缓冲区大小,已经还需的byteCount数量
*/
source.size -= movedByteCount;
size += movedByteCount;
byteCount -= movedByteCount;
}
}
Buffer.write()方法,将数据从一个缓冲区移动到另一个缓冲区,根据不同情况来决定是进行copy或者是引用的引动,沿用上面代码的source表示为来源缓冲区,this表示为当前缓冲区,则source到this的过程为:
假设source头节点有足够的数据,当this的尾节点能装得下是,将数据copy入this的尾节点;当this的尾节点装不下时,将source头节点数据进行slipt()操作,this需要的数据会被分割在source的Segment链表里并成为新的头节点,再将source的头节点pop出,push到this的尾节点
假设source头节点没有足够的数据,说明整个头节点都要移动出去。当this内无数据时,将source头节点pop,成为this的头节点;当this内有数据时,将source头节点pop,push到this的尾节点
上述出现的情况可以用下图表达:


经过Buffer.write()操作,也就把数据从一个缓冲区,移动到了另一个缓冲区。对应当前的案例,则是从文件输入流缓冲区拿数据,读出到文件输出缓冲区。
/ 其它 /
Okio最亮眼的操作,就是设计出了Segment存储数据,通过Buffer进行缓冲管理,并在Buffer.write()则里,通过移动引用而不是真实数据,是减少数据copy进而交换数据的关键。
上面分析了RealBufferedSource,而RealBufferedSink也是同样的道理,只是方向相反,缓冲数据存储依然离不开Segment和Buffer,RealBufferedSink拿到数据后,再通过emit()将数据写出到输出流,RealBufferedSink拿到的sink的操作,可以通过Okio.sink()拿到的匿名内部类Sink()查看,分析方法类似。相较于Java原生IO的缓冲方案,双流操中,或者说以Buffer来代替 写入/写出 的 byte[],减少了copy的过程,通过Segment的移动达到目的。
此外,Okio的写入/写出操作,也可以像原生那样,接受byte[]参数,或者直接获取下一个数据,这种情况时,则于原生相似,需要时一样依赖copy,不再有减少copy的优势。并且,Okio接口也更友好,如之前说原生实现向文件写入自定义数据时,需要Data的流类型进行转译,自身就封装了这样的操作。TimeOut 方案就不深入了,篇幅过长,自行查阅,优点但非必要核心点。
/ 总结 /
Okio核心竞争力为,增强了流于流之间的互动,使得当数据从一个缓冲区移动到另一个缓冲区时,可以不经过copy能达到:
以Segment作为存储结构,真实数据以类型为byte[]的成员变量data存在,并用其它变量标记数据状态,在需要时,如果可以,移动Segment引用,而非copy data数据
Segment在Segment线程池中以单链表存在以便复用,在Buffer中以双向链表存在存储数据,head指向头部,是最老的数据
Segment能通过slipt()进行分割,可实现数据共享,能通过compact()进行合并。由Buffer来进行数据调度,基本遵守 “大块数据移动引用,小块数据进行copy” 的思想
Source 对应输入流,Sink 对应输出流
TimeOut 以达到在期望时间内完成IO操作的目的,同步超时在每次IO操作中检查耗时,异步超时开启另一线程间隔时间检查耗时
Okio并没有打算优化底层IO方式以及替代原生IO方式,Okio优化了缓冲策略以减轻内存压力和性能消耗,并且对于部分IO场景,提供了更友好的API,而更多的IO场景,该记的还得记。





































苏公安网备 32011402010933号