⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
当一个开发者或团队的水平积累到一定程度,会有自内向外输出价值的需求。在这个专栏里,小彭将为你分享 Android 方向主流开源组件的实现原理,包括网络、存储、UI、监控等。
本文是 Android 开源库系列的第 3 篇文章,完整文章目录请移步到文章末尾~
前言
大家好,我是小彭。
今天,我们来讨论一个 Square 开源的 I/O 框架 Okio,我们最开始接触到 Okio 框架还是源于 Square 家的 OkHttp 网络框架。那么,OkHttp 为什么要使用 Okio,它相比于 Java 原生 IO 有什么区别和优势?今天我们就围绕这些问题展开。
本文源码基于 Okio v3.2.0。
思维导图
- 说一下 Okio 的优势?
相比于 Java 原生 IO 框架,我认为 Okio 的优势主要体现在 3 个方面:
- 1、精简且全面的 API: 原生 IO 使用装饰模式,例如使用 BufferedInputStream 装饰 FileInputStream 文件输入流,可以增强流的缓冲功能。但是原生 IO 的装饰器过于庞大,需要区分字节、字符流、字节数组、字符数组、缓冲等多种装饰器,而这些恰恰又是最常用的基础装饰器。相较之下,Okio 直接在 BufferedSource 和 BufferedSink 中聚合了原生 IO 中所有基础的装饰器,使得框架更加精简;
- 2、基于共享的缓冲区设计: 由于 IO 系统调用存在上下文切换的性能损耗,为了减少系统调用次数,应用层往往会采用缓冲区策略。但是缓冲区又会存在副作用,当数据从一个缓冲区转移到另一个缓冲区时需要拷贝数据,这种内存中的拷贝显得没有必要。而 Okio 采用了基于共享的缓冲区设计,在缓冲区间转移数据只是共享 Segment 的引用,而减少了内存拷贝。同时 Segment 也采用了对象池设计,减少了内存分配和回收的开销;
- 3、超时机制: Okio 弥补了部分 IO 操作不支持超时检测的缺陷,而且 Okio 不仅支持单次 IO 操作的超时检测,还支持包含多次 IO 操作的复合任务超时检测。
下面,我们将从这三个优势展开分析:
- 精简的 Okio 框架
先用一个表格总结 Okio 框架中主要的类型:
类型 | 描述 |
---|---|
Source | 输入流 |
Sink | 输出流 |
BufferedSource | 缓存输入流接口,实现类是 RealBufferedSource |
BufferedSink | 缓冲输出流接口,实现类是 RealBufferedSink |
Buffer | 缓冲区,由 Segment 链表组成 |
Segment | 数据片段,多个片段组成逻辑上连续数据 |
ByteString | String 类 |
Timeout | 超时控制 |
2.1 Source 输入流 与 Sink 输出流
在 Java 原生 IO 中有四个基础接口,分别是:
- 字节流:
InputStream
输入流和OutputStream
输出流; - 字符流:
Reader
输入流和Writer
输出流。
而在 Okio 更加精简,只有两个基础接口,分别是:
- 流:
Source
输入流和Sink
输出流。
Source.kt
1 | kotlin复制代码interface Source : Closeable { |
Sink.java
1 | java复制代码actual interface Sink : Closeable, Flushable { |
2.2 InputStream / OutputStream 与 Source / Sink 互转
在功能上,InputStream - Source 和 OutputStream - Sink 分别是等价的,而且是相互兼容的。结合 Kotlin 扩展函数,两种接口之间的转换会非常方便:
- source(): InputStream 转 Source,实现类是 InputStreamSource;
- sink(): OutputStream 转 Sink,实现类是 OutputStreamSink;
比较不理解的是: Okio 没有提供 InputStreamSource 和 OutputStreamSink 转回 InputStream 和 OutputStream 的方法,而是需要先转换为 BufferSource 与 BufferSink,再转回 InputStream 和 OutputStream。
- buffer(): Source 转 BufferedSource,Sink 转 BufferedSink,实现类分别是 RealBufferedSource 和 RealBufferedSink。
示例代码
1 | kotlin复制代码// 原生 IO -> Okio |
JvmOkio.kt
1 | kotlin复制代码// InputStream -> Source |
Okio.kt
1 | kotlin复制代码// Source -> BufferedSource |
2.3 BufferSource 与 BufferSink
在 Java 原生 IO 中,为了减少系统调用次数,我们一般不会直接调用 InputStream 和 OutputStream,而是会使用 BufferedInputStream
和 BufferedOutputStream
包装类增加缓冲功能。
例如,我们希望采用带缓冲的方式读取字符格式的文件,则需要先将文件输入流包装为字符流,再包装为缓冲流:
Java 原生 IO 示例
1 | java复制代码// 第一层包装 |
同理,我们在 Okio 中一般也不会直接调用 Source 和 Sink,而是会使用 BufferedSource
和 BufferedSink
包装类增加缓冲功能:
Okio 示例
1 | kotlin复制代码val bufferedSource = file.source()/*第一层包装*/.buffer()/*第二层包装*/ |
网上有资料说 Okio 没有使用装饰器模式,所以类结构更简单。 这么说其实不太准确,装饰器模式本身并不是缺点,而且从 BufferedSource 和 BufferSink 可以看出 Okio 也使用了装饰器模式。 严格来说是原生 IO 的装饰器过于庞大,而 Okio 的装饰器更加精简。
比如原生 IO 常用的流就有这么多:
- 原始流: FileInputStream / FileOutputStream 与 SocketInputStream / SocketOutputStream;
- 基础接口(区分字节流和字符流): InputStream / OutputStream 与 Reader / Writer;
- 缓存流: BufferedInputStream / BufferedOutputStream 与 BufferedReader / BufferedWriter;
- 基本类型: DataInputStream / DataOutputStream;
- 字节数组和字符数组: ByteArrayInputStream / ByteArrayOutputStream 与 CharArrayReader / CharArrayWriter;
- 此处省略一万个字。
原生 IO 框架
而这么多种流在 Okio 里还剩下多少呢?
- 原始流: FileInputStream / FileOutputStream 与 SocketInputStream / SocketOutputStream;
- 基础接口: Source / Sink;
- 缓存流: BufferedSource / BufferedSink。
Okio 框架
就问你服不服?
而且你看哈,这些都是平时业务开发中最常见的基本类型,原生 IO 把它们都拆分开了,让问题复杂化了。反观 Okio 直接在 BufferedSource 和 BufferedSink 中聚合了原生 IO 中基本的功能,而不再需要区分字节、字符、字节数组、字符数组、基础类型等等装饰器,确实让框架更加精简。
BufferedSource.kt
1 | kotlin复制代码actual interface BufferedSource : Source, ReadableByteChannel { |
BufferedSink.kt
1 | kotlin复制代码actual interface BufferedSink : Sink, WritableByteChannel { |
2.4 RealBufferedSink 与 RealBufferedSource
BufferedSource 和 BufferedSink 还是接口,它们的真正的实现类是 RealBufferedSource 和 RealBufferedSink。可以看到,在实现类中会创建一个 Buffer 缓冲区,在输入和输出的时候,都会借助 “Buffer 缓冲区” 减少系统调用次数。
RealBufferedSource.kt
1 | kotlin复制代码internal actual class RealBufferedSource actual constructor( |
RealBufferedSink.kt
1 | kotlin复制代码internal actual class RealBufferedSink actual constructor( |
至此,Okio 基本框架分析结束,用一张图总结:
Okio 框架
- Okio 的缓冲区设计
3.1 使用缓冲区减少系统调用次数
在操作系统中,访问磁盘和网卡等 IO 操作需要通过系统调用来执行。系统调用本质上是一种软中断,进程会从用户态陷入内核态执行中断处理程序,完成 IO 操作后再从内核态切换回用户态。
可以看到,系统调用存在上下文切换的性能损耗。为了减少系统调用次数,应用层往往会采用缓冲区策略:
以 Java 原生 IO BufferedInputStream
为例,会通过一个 byte[] 数组作为数据源的输入缓冲,每次读取数据时会读取更多数据到缓冲区中:
- 如果缓冲区中存在有效数据,则直接从缓冲区数据读取;
- 如果缓冲区不存在有效数据,则先执行系统调用填充缓冲区(fill),再从缓冲区读取数据;
- 如果要读取的数据量大于缓冲区容量,就会跳过缓冲区直接执行系统调用。
输出流 BufferedOutputStream
也类似,输出数据时会优先写到缓冲区,当缓冲区满或者手动调用 flush() 时,再执行系统调用写出数据。
伪代码
1 | java复制代码// 1. 输入 |
3.2 缓冲区的副作用
的确,缓冲区策略能有效地减少系统调用次数,不至于读取一个字节都需要执行一次系统调用,大多数情况下表现良好。 但考虑一种 “双流操作” 场景,即从一个输入流读取,再写入到一个输出流。回顾刚才讲的缓存策略,此时的数据转移过程为:
- 1、从输入流读取到缓冲区;
- 2、从输入流缓冲区拷贝到 byte[](拷贝)
- 3、将 byte[] copy 到输出流缓冲区(拷贝);
- 4、将输出流缓冲区写入到输出流。
如果这两个流都使用了缓冲区设计,那么数据在这两个内存缓冲区之间相互拷贝,就显得没有必要。
3.3 Okio 的 Buffer 缓冲区
Okio 当然也有缓冲区策略,如果没有就会存在频繁系统调用的问题。
Buffer 是 RealBufferedSource 和 RealBufferedSink 的数据缓冲区。虽然在实现上与原生 BufferedInputStream 和 BufferedOutputStream 不一样,但在功能上是一样的。区别在于:
- 1、BufferedInputStream 中的缓冲区是 “一个固定长度的字节数组” ,数据从一个缓冲区转移到另一个缓冲区需要拷贝;
- 2、Buffer 中的缓冲区是 “一个 Segment 双向循环链表” ,每个 Segment 对象是一小段字节数组,依靠 Segment 链表的顺序组成逻辑上的连续数据。这个 Segment 片段是 Okio 高效的关键。
Buffer.kt
1 | kotlin复制代码actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel { |
对比 BufferedInputStream:
BufferedInputStream.java
1 | java复制代码public class BufferedInputStream extends FilterInputStream { |
3.4 Segment 片段与 SegmentPool 对象池
Segment 中的字节数组是可以 “共享” 的,当数据从一个缓冲区转移到另一个缓冲区时,可以共享数据引用,而不一定需要拷贝数据。
Segment.kt
1 | kotlin复制代码internal class Segment { |
另外,Segment 还使用了对象池设计,被回收的 Segment 对象会缓存在 SegmentPool 中。SegmentPool 内部维护了一个被回收的 Segment 对象单链表,缓存容量的最大值是 MAX_SIZE = 64 * 1024
,也就相当于 8 个默认 Segment 的长度:
SegmentPool.kt
1 | kotlin复制代码// object:全局单例 |
Segment 示意图
- 总结
- 1、Okio 将原生 IO 多种基础装饰器聚合在 BufferedSource 和 BufferedSink,使得框架更加精简;
- 2、为了减少系统调用次数的同时,应用层 IO 框架会使用缓存区设计。而 Okio 使用了基于共享 Segment 的缓冲区设计,减少了在缓冲区间转移数据的内存拷贝;
- 3、Okio 弥补了部分 IO 操作不支持超时检测的缺陷,而且 Okio 不仅支持单次 IO 操作的超时检测,还支持包含多次 IO 操作的复合任务超时检测。
关于 Okio 超时机制的详细分析,我们在 下一篇文章 里讨论。请关注。
版权声明
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
参考资料
- Github · Okio
- Okio 官网
- Okio 源码学习分析 —— 川峰 著
- Okio 好在哪?—— MxsQ 著
推荐阅读
Android 开源库系列完整目录如下(2023/07/12 更新):
- #1 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(上)
- #2 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)
- #3 IO 框架 Okio 的实现原理,到底哪里 OK?
- #4 IO 框架 Okio 的实现原理,如何检测超时?
- #5 序列化框架 Gson 原理分析,可以优化吗?
- #6 适可而止!看 Glide 如何把生命周期安排得明明白白
- #7 为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为它是真强啊!
- #8 内存缓存框架 LruCache 的实现原理,手写试试?
- #9 这是一份详细的 EventBus 使用教程
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~
本文转载自: 掘金