浅析 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.prev
和Segment.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 是线程安全的 |