讲到这里,如果你都明白了的话,我想你已经能体会到 Okio 优化的精妙之处,构建在原生输入输出流的基础上,舍弃原生IO的缓存功能,自己实现一套流读取和写入的缓存机制,大大提高了缓存使用的效率。我们对比原生 IO 和 Okio 缓存处理时,数据从输入流到输出流的工程。
原生 IO 缓存处理过程 InputStream -> inBuffer -> 临时 byte[] -> outBuffer -> OutpuStream
Okio 缓存处理过程 InputStream -> inBuffer -> outBuffer -> OutpuStream,可以发现少了中转临时 byte[]的过程,inBuffer -> outBuffer 之间直接交互数据。同时 inBuffer 和 outBuffer 很多时候是可以共享 Segment 数据,这意味这个 inBuffer -> outBuffer 不是单纯的复制数据,而是可以以 Segment 为单位,直接从 inBuffer 去 pop 移除,然后 push 添加到outBuffer 中,这意味着 inBuffer 和 outBuffer 之间的数据很多情况下就是共享的,也就是说会更加趋近与 InputStream -> Buffer -> OutpuStream,那么现在是不是更能体会 Okio 的优化策略,它将之前的inBuffer -> 临时 byte[] -> outBuffer 优化成一体的了。
使用Okio
try {
Okio.buffer(Okio.sink(file)).writeUtf8(name).writeInt(age).close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException ex){
ex.printStackTrace();
}
Okio 可以很方便的使用链式调用,一句代码就可以完成流的创建->操作->关闭
Okio 为了精简我们常用的字节字符流操作,抽象出了 BufferedSource 缓存流输入和BufferedSink 缓存流输出接口,它们具有处理字节和字符,和数据缓存的功能。
java IO流:
输入 buf -> 临时 byte 数组 -> 输出 buf,经历两次拷贝。
Okio的优势:
Buffer 交互增强(首先是缓存交互的设计,我们看 Source 的 read 和 Sink 的 write 方法都是直接将对方的 Buffer 传入进去)
Buffer 结构优化,数据转移优化(Okio 转而使用一个个大小2048的小数组(这段数组数据封装为 Segment),采用双链表的方式组织在一起,形成一条前后循环的以小段数组为单位的链表。那么这样的结构有一个潜在的好处就是,操作的数据是以一小段数组(segment)为单位,从source 流的缓存数据中转移到sink流缓数据中的过程,大部分请求下就不必像原生中对字节数据一个个拷贝过去,而是可以直接指向过去,注意,是直接指向,将 source中的该小段数组(Segment)从 source 的缓存链表中移除,添加到 sink 中缓存链表中,不需要任何的数据拷贝工作,这显然可以节省很多 cpu 时间。)
灵活智能的 Segment 数据控制。同时这个 segment 又是一个很灵活智能的小段数组,它会考虑和它的前一个 segment 进行数据的合并(compact),节省出一个 segment 空间。同时它也支持将一个 segment 分割(split)为两个相连的 segment,这样的话可以将原来[offset, limit]的数据,分割为[offset, offset+byteCount]和[offset+byteCount, limit]两段 segment 数据,这样的好处在于,分离出来后,操作更灵活,比如可以将[offset, offset+byteCount]这段 segment 直接从 source 缓存指向 sink 缓存,免去了数据拷贝工作,而其实这种分离操作,只是逻辑上的分离,这两个分离的 segment 其实还是用的通过一个数组数据,也就是说它们共享同一段数据,只是它们标记数据范围不一样而已。
Segment 数据池的引入。你以为就这样了吗,Okio 为了防止一个个的 segment 数据被频繁的创建和销毁,使用了一个 SegmentPool 用于维护使用完成的 segment 数据,由它来管理 segment 的销毁或循环使用。SegmentPool 提供了64*1024的最大大小,也就是说它可以容纳最多32个 segment 数据。它提供了 take 用于获取可使用的segment 和 recycle 用来判定是否回收。这样的好处在于,segment 数据和 Buffer 是独立分开的,Buffer 只负责 segment 的使用,而不负责对它的创建和销毁,转而由SegmentPool 来进行管理,这样的话,segment 就像是公有资源一样,Buffer 使用完了之后交还给 SegmentPool,下一个 Buffer 需要了再从 SegmentPool 中获取就可以了,这样大大的减少了内存的分配和回收的开销。
TimeOut 超时的引入(我们希望IO阻塞到一定时间后能够抛出异常告诉我们发生 TimeOut 超时了,而不是可能会一直阻塞下去)
同步超时 TimeOut(TimeOut 会在每次的读写操作中判断是否到达了超时时间,进而做超时处理,因而它有个缺点,就是如果在读写方法中,它一直阻塞,那么 TimeOut的计时方法也将被阻塞,这样的超时情况,它也无能为力去判断了)
异步超时 AsyncTimeOut。而 AsyncTimeOut 则不同,有一个单独的线程 Watchdog(姑且称它为看门狗吧)用于监控这些 AsyncTimeOut 是否超时,如果某个 AsyncTimeOut 超时了,它就汪汪两声调用 AsyncTimeOut 的 timedOut 方法,而你需要做的是实现这个 timedOut 方法,比如在 Okio 实现 Socket 的超时中,它实现的 timedOut 是关闭 Socket,这样如果一直阻塞,看门狗发现超时了,会调用 AsyncTimeOut的 timedOut,然后就会关闭关闭 Socket,这样系统就会抛出 IO 异常,阻塞也就中止了
原文链接: http://mp.weixin.qq.com/s/_wdTYvNYuTLxBSiL2fdBKg
将 srcFile文件的数据全部复制到文件 destFile
Okio.buffer(Okio.sink(destFile)).writeAll(Okio.buffer(Okio.source(srcFile)));
上面的代码等价于:
try {
RealBufferedSource source = Okio.buffer(Okio.sink(srcFile));
RealBufferedSink sink = Okio.buffer(Okio.sink(destFile));
sink.writeAll(source);
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException ex){
ex.printStackTrace();
}
精辟言论:
对于目标 Segment 容纳的下的小段数据,采用直接复制的方法,而大段的 Segment 数据,则是直接移动,而不是复制,只是一个引用指向的变化,那效率超级高啊,这个设计很绝妙,所以说 Okio 为什么要设计成一小段的 Segment,因为段小好操作啊,你要复制多少个数据,我可以根据情况来考虑大段的整个移动,小段的采用复制,而如果像原生 IO 那一大段的数组,就只能乖乖的采用复制的方法了。