为你推荐一款高效的 IO 组件——okio
前不久,三方组件库上新了一批 JS/eTS 组件,其中就包括 okio 组件。okio 是一个可应用于 HarmonyOS 的高效 IO 库,它依托于系统能力,提供字符串的编解码转换能力,基础数据类型的读写能力以及对文件读写的支持。本期将为大家介绍 okio 的工作原理及使用方法。
一、okio 的产生背景
IO,即输入输出(Input/Output)。绝大多数应用都需要与外部进行数据交互,这就会涉及 IO。系统提供了 IO 能力,在使用系统 IO 时,通常需要一个中间缓冲区来保存读取到的数据。数据先从输入流缓冲区复制到中间缓冲区,再从中间缓冲区复制到输出流缓冲区。中间多次拷贝,降低了 IO 效率,同时增加了系统消耗。为了满足开发者对 IO 的更高要求,三方组件库推出 IO 处理利器——okio(JS 版本)。okio 使用 Segment 作为数据存储容器,通过提供 Segment 移动、共享、合并和分割的能力,让数据读写变得非常灵活,也减少了数据复制,提升了 IO 效率。此外,okio 还通过 SegmentPool 对 Segment 进行回收和复用,减少大量创建 Segment 带来的系统消耗。下面就带大家深入了解 JS 版本的 okio 的工作原理,探索它是如何提升 IO 效率的~
二、两个基本概念
在深入解析 okio 的工作原理之前,我们先来了解两个基本概念:Segment 和 SegmentPool。
1. Segment
okio 将数据分割成一块块的片段存放在 Segment 里面。Segment 是一个数据存储的真正类,内部维护着一个大小为 8192 字节的字节数组用于存储数据。Segment 最小可共享、可写入的数据大小为 1024 字节。Segment 使用 pos、limit、shared、owner、prev、next 来分别记录读写位置、是否可写入、是否能共享、数据拥有者、前置节点和后置节点信息。Segment 对外提供 sharedCopy、unsharedCopy、split、push、pop、compact、writeTo 等接口用于操作数据。
Segment 同时拥有前置节点和后置节点,构成一个双向链表。读取数据的时候,从双向链表的头部开始读取;而写入数据的时候,从双向链表的尾部写入数据。
2. SegmentPool
为了管理 Segment,okio 维护了一个 Segment 对象池(即 SegmentPool),对废弃的 Segment 回收、复用和内存共享,从而减少内存的申请和 GC(garbage collection,垃圾收集)的频率,使性能得到优化。SegmentPool 是一个由最多 8 个 Segment 组成的单链表。一个 Segment 的最大大小是 8192 字节(即 8KB),所以 SegmentPool 的最大大小是 64KB。
三、okio 的工作原理
okio 组件最重要的功能就是“读”和“写”。下面我们就从读写开始,了解 okio 的工作原理。
1. 读写数据
okio 读写数据的过程中,遵循大块数据移动、小块数据复制的原则。okio 从输入流读取数据到输入流缓冲区时,会先找到双向链表尾部的 Segment 节点,如果此节点的剩余容量足够,则直接将读取到的数据存入到此节点。如果此节点的剩余容量不足,则从 SegmentPool 里面取一个 Segment 链接到双向链表的尾部,然后将数据存入这个新节点。okio 从输入流缓冲区读取数据,再写入数据到输出流缓冲区。这个过程比较复杂,有以下几种情况:
(1) 从输入流缓冲区获取到 Segment,如果数据是满的(字节数组 data 长度为 8092 字节),那么直接修改此 Segment 的 prev 和 next 信息,将其添加到输出流缓冲区的双向链表的尾部,省去一次数据复制过程。
图 1 大块数据移动
(2) 从输入流缓冲区获取到 Segment(假设为 Segment1),如果数据不是满的,可以通过 pos 和 limit 信息来确定 segment1 的可读数据,再和输出流缓冲区的双向链表的尾部节点(假设为 Segment2)的剩余容量进行对比:如果 Segment1 的可读数据比 Segment2 的剩余容量小,则把 Segment1 的数据复制到 Segment2,然后回收 Segment1 到 SegmentPool。如果 Segment1 的可读数据比 Segment2 的剩余容量大,那么直接修改 Segment1 的 prev 和 next 信息,将其添加到 Segment2 的后面。
(3) 从输入流缓冲区获取到 Segment(假设为 Segment3),如果只需要传递部分数据(比如总数据为 4096 字节,只传递 1024 字节),okio 会通过 split 接口将 Segment3 拆分成含 3072 字节数据的 Segment3-1 和含 1024 字节数据的 Segment3-2,然后按照(2)的逻辑将 Segment3-2 的数据写入输出流缓冲区。
图 2 Segment 拆分
拆分 Segment 的时候,可以通过参数指定拆分后的第一个 Segment 含有的未读字节数(byteCount)。拆分后,第一个 Segment 包含的数据范围是[pos,pos+byteCount),第二个 Segment 包含的数据范围是[pos+byteCount,limit)。拆分 Segment 时也遵循大块数据移动、小块数据复制的原则。当 byteCount 大于 1024 时,使用共享的 Segment,否则复制数据。(注:文件、流、socket 相关的 IO 优化需要系统支持,待后续版本优化提供。)
2. Segment 的回收与复用
接下来,我们再来看看 SegmentPool 是如何回收和复用 Segment 的。
每次 okio 想要使用 Segment 就从 SegmentPool 中获取,使用完毕后又会放回到 SegmentPool 中等待复用,核心方法为 take()和 recycle()。
(1) take()方法
take()方法负责从对象池单链表的头部获取可以使用的 Segment。如果获取不到,说明单链表是空的,此时新创建一个 Segment 给缓冲区使用。如果能获取到,则取出单链表的头部节点,再将下一个节点置为单链表的头部节点,并将取出来的 Segment 的 next 置空,同时更新对象池大小。
(2) recycle()方法
recycle()方法负责回收缓冲区里面使用完毕的 Segment。回收开始时,首先更新对象池大小,然后把回收对象 Segment 添加到单链表头部,接着重置 Segment 的 pos 和 limit 为 0。注意,以下情况不会回收 Segment:
当前 Segment 的 prev 和 next 不为空
当前 Segment 是共享的
对象池已经有 8 个 Segment 了
3. 字符串处理
除了 Segment 和 SegmentPool 外,okio 还封装了 ByteString 类来进行字符串处理。ByteString 提供 Base64 编解码、utf-8 编码、十六进制编解码、大小写转换、内容比较等丰富的 API,可以很方便地处理字符串。在进行字符串处理时,由于 ByteString 同时持有原始字符串和对应的字节数组,可以直接使用字节数组里面的数据进行操作,不需要先将字符串转换为字节数组。特别是在频繁转换编码的场景下,通过这种以空间换时间的方式,可以避免字符串与字节数组的多次转换,减少了时间和系统性能消耗。
四、okio 的使用及示例
1. 前置配置步骤一:在 entry 的 package.json 文件中添加以下依赖项。
步骤二:配置仓库镜像地址。
步骤三:DevEco Studio 的 Terminal 里面输入以下命令下载源代码。
步骤四:文件的头部引入 okio 库。
步骤五:在 config.json 文件中申请存储权限。
2. 代码实现执行完上面的配置操作后,就可以进入代码编写阶段了。开发者可以使用 okio 提供的丰富的 API 接口来实现功能。下面为大家展示四个实现示例,供大家参考学习。
示例 1:文件写入和读取
本示例通过 sink 将内容写入文件,通过 source 从文件读取内容。代码如下:
示例 2:Base64 解码
本示例通过 ByteString 实现 Base64 解码功能,代码如下:
示例 3:十六进制解码
本示例通过 ByteString 实现十六进制解码功能,代码如下:
示例 4:Utf8 编码
本示例通过 ByteString 实现 Utf8 编码功能,代码如下:
本期 okio 组件就为大家介绍到这里了。okio 组件已开源,欢迎大家参与贡献。
开源地址如下:
https://gitee.com/openharmony-tpc/okio
评论