文档开发者中心
我的

Okio好在哪里?【后篇】

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()。



@Override
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() {
@Override 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。



@Override
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;
}

@Override
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的尾节点

上述出现的情况可以用下图表达:


640?wx_fmt=png

640?wx_fmt=png


经过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能达到:


  1. 以Segment作为存储结构,真实数据以类型为byte[]的成员变量data存在,并用其它变量标记数据状态,在需要时,如果可以,移动Segment引用,而非copy data数据

  2. Segment在Segment线程池中以单链表存在以便复用,在Buffer中以双向链表存在存储数据,head指向头部,是最老的数据

  3. Segment能通过slipt()进行分割,可实现数据共享,能通过compact()进行合并。由Buffer来进行数据调度,基本遵守 “大块数据移动引用,小块数据进行copy” 的思想

  4. Source 对应输入流,Sink 对应输出流

  5. TimeOut 以达到在期望时间内完成IO操作的目的,同步超时在每次IO操作中检查耗时,异步超时开启另一线程间隔时间检查耗时


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



点赞
收藏
回复
2
分享
举报
浏览1055 发布于2019-11-01 05:36未知归属地
全部评论
最多点赞
最新发布
最早发布

写的蛮好的啊


2楼回复于2019-11-05 03:45 未知归属地

支持一下,看看怎么搞


1楼回复于2019-11-04 15:01 未知归属地
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要发帖子

了解社区公约,与您携手共创和谐专业的开发者社区。