浅析 IO 框架:Okio

基本概念

Okio java.io
ByteString String
Buffer ByteArray
Source InputStream
Sink OutputStream
Segment
SegmentPool

ByteString

虽然 okio.ByteString 对标的是 java.lang.String,但是它操作的对象却是 String 内部的 ByteArray,相较于 String 的 字符数据 更贴近于 字节数据 的概念

类别 API
字符编码(charset) utf8()
string(charset)
消息摘要(message digest) md5() 128-bit
hmacSha1(key) 160-bit, hmacSha256(key) 256-bit, hmacSha512(key) 512-bit
hmac 是额外添加了一个秘钥作为影响因子的 sha
sha1() 160-bit, sha256() 256-bit, sha512() 512-bit
digest(algorithm)
基于字节的匹配和查找(而不是字符) rangeEquals(offset, other ByteString/ByteArray, otherOffset, byteCount)
startsWith(ByteString/ByteArray)
endsWith(ByteString/ByteArray)
indexOf(other ByteString/ByteArray, fromIndex)
lastIndexOf(other ByteString/ByteArray, fromIndex)
其他 hex()
toAsciiLowercase()
toAsciiUppercase()

Buffer

  • Buffer 对其内部的内存(ByteArray)进行分段管理(Segment),就像内存分页一样
  • 分段管理可以逐步回收已读的 Segment 而不必始终占用一大块的内存(Buffer 属于 流式 API,不能重复读,那么已读的数据就可以及时释放掉)
  • 分段管理还可以很方便地与另一个 Buffer 共享某一段数据(或是构造出共享数据的 clone,或是以共享替代传输的 copyTo,或是共享某几段的 ByteString),只需要让别的段(Segment)引用同一块内存(ByteArray)即可,别的 Buffer 不能往共享数据的 Segment 里写数据,只能自己往环里新插入一个段来写入
  • 小块的段还很方便回收和复用,用一个池即可(SegmentPool),段太大容易造成内存浪费
  • Buffer 内部的 Segment 通过 Segment.prevSegment.next 形成一个环(头尾相连的链表)
  • Segment.limit 是下一次写操作写入的位置,Segment.pos 是下一次读操作读取的位置,正常情况下 0 <= pos <= limit <= max
  • 写入(write)的节点总是 tail 节点(head.prev),如果当前的 tail 不满足写入条件(Segment 不够容量 or 是个共享的 Segment),会插入一个新的 Segment 到 head.prev 作为当前 tail 来使用
  • 读操作(read)总是从 head 开始,读操作会使 pos 逐步增长(往前走),当 pos 与 limit 相遇时(pos == limit),说明此节点的数据已读完,会从环里删除此节点,并将 next 作为新的 head
  • 初始环是空的(null),第一次写操作会初始化第一个节点 head,此时 head.prev 和 head.next 都指向它自己
API 描述
read/write 读写各种类型的数据类型
copy()/clone() 返回一个共享数据的 Buffer
这个就比较有意思了,我们知道 Buffer 用 Segment 对数据进行分片管理(类似于内存的分页),这里并没有真正将数据拷贝一份到另一个 Buffer(要节约内存嘛),而是让另一个 Buffer 里的 Segment 与当前 Buffer 里的 Segment 共享用一个 ByteArray
新的 Segment 可以有自己的 pos 和 limit,可以读但不能写(Segment.owner == false,写入的数据会放到新创建的/自己的 Segment 里去),不然会把别人的数据搞乱,当然 Segment 的持有者是可以写的(Segment.owner == true)
共享了数据的 Segment 不能被 SegmentPool 回收(因为可能别人还在用,而且内部没有计算器指示被多少人持有),它的 Segment.share == true
copyTo(Buffer) 使另一个 Buffer 与当前 Buffer 共享某一段数据
peek() 返回一个可以重复读的 Source(一般的 Source 跟 InputStream 一样是单向/流式的,不能重复读),但是这个 Buffer 作为 Source 的 backend,已读的数据 Source 也是无法读取的
snapshot(byteCount) 返回一个 ByteString(上面说过 Okio ByteString 对标 java.lang.String),特别的是它与 Buffer 是共享底层 ByteArray 的(不是共享 Segment,但会使 Segment.shared 置真)
ByteString 是不可变的所以它不会修改共享的 ByteArray
skip(byteCount) 跳过 byteCount 字节的数据,使读指针 pos 前进 byteCount 位(已读完的 Segment 将被回收)
inputStream() 把 Buffer 作为输入流的源
outputStream() 把 Buffer 作为输出流的目的地
copyTo(OutputStream) 将 Buffer 里的数据拷贝一份到 OutputStream,不移动 Segment.pos(也即不会释放已读数据)
writeTo(OutputStream) 将 Buffer 里的数据写入到 OutputStream,会移动 Segment.pos
readFrom(InputStream) 将 InputStream 里的数据读到 Buffer 里

SegmentPool

  • 当需要新的 Segment 时总是会从池里拿(Segment.take()),当一个 Segment 从环里移除不再需要时总是会放回到池里去(Segment.recycle(Segment)
  • SegmentPool 是全局的,内部结构类似于 HashMap(数组 + 链表),只不过数组容量是固定的,通过 tid 决定用哪个链表
  • 回收时(recycle)插入链表头部,复用时(take)取链表头
  • 为了适应多线程环境链表的节点是 AtomicReference<Segment?>,回收和复用都使用 CAS 操作整个 SegmentPool 使用 CAS 替代锁(lock-free),竞争失败则放弃相关操作

各种各样的装饰器(Decorators)

java.io 一样,Okio 为 Source/Sink 提供了各种各样的装饰器

class 作用
HashingSource/HashingSink 提供成员属性 hash,在 read/write 时实时更新
GzipSource/GzipSink 类似于 GzipInputStream/GzipOutputStream,对 write 进行压缩,对 read 进行解压缩
BufferSource/BufferSink 具有一段 buffer 的输入流/输出流,默认实现就是 Buffer
CipherSource/CipherSink 实时对输入流/输出流进行加解密
Throttler 给输入流/输出流装一个节流阀,模拟限流/弱网的情况
Pipe 顾名思义它是一段管道,管道两端连着 Source 和 Sink,流向 Sink 的数据即是 Source 的输出,管道及其 Source 和 Sink 是线程安全的