Okio 文档翻译

此为 Okio 文档翻译,文档地址:

Okio

Okio 对 java.iojava.nio 进行了封装,通过使用 Okio 这个库,你可以很方便地进行数据 访问,存储和处理。Okio 最初是作为 Okhttp 的一个模块。但现在它独立了出来,我们可以单独使用其来简化 IO 操作。

ByteStrings and Buffers

Okio 中规定了两种 对象,这两种对象封装了大量的功能 :

  • ByteString 是一个不可变的 bytes 类型的序列(类似 byte 数组),对于 字符数据,它可以起到类似 String 的功能,但其封装了解码和编码的相关操作,能够快速进行 String 与 ByteArray 之间的转换。支持 Hex, base64 和 UTF-8 等编码。
  • Buffer 是一个可变的 bytes 类型的序列,类似 ArrayList,你可以从 Buffer 尾部写数据,从头部读数据。而不需要管理 nio 中 Buffer 的 positions, limits 等指针 。

ByteStringBuffer 内部进行了一些优化。例如,如果你使用 ByteString 来编码一个 UTF-8 的字符串,它会保存对原字符串的引用,当你下次解码时,直接返回字符串的引用。

Buffer 是 segments 的链表,当你想要将 Buffer 中的数据进行复制时,可直接将 segments 的引用进行复制,而不复制数据本身。

Sources and Sinks

java.io 中一个优雅的地方是如何将 streams 分层进行传输,例如压缩和传输一体化。Okio 规定了自己的流类型,Source 和 Sink,类似 InputStream 与 OutputStream,但他们有以下本质区别:

  • Timeouts. Source 和 Sink 都提供了超时的 api,而 java.io 中这些方法都是阻塞方法。
  • Easy to implement. Source 接口只规定了三个方法:read() ,close() 和 timeout() .没有像 available() 或者单字节读取这种可能会有歧义和性能损失的接口 。
  • Easy to use.虽然 Source 与 Sink 只规定了简单的三个方法,不过调用者会得到一个 BufferedSource 或 BufferedSink 接口,这两者有丰富的 Api ,可以实现各种你需要的功能。
  • No artificial distinction between byte streams and char streams. 字符流与字节流都是数据,你可以以 bytes, UTF-8 strings, bit/little-endian 32-bit integer 来读取你想要的数据。而在 java.io 中,字节流与字符流是分开的。
  • Easy to test. Buffer 类实现了 BufferedSource 与 BufferedSink 接口因此测试代码是很简单和简洁的。

Source 与 Sink 可以与 InputStream 与 OutputStream 相互交互。可以相互转换。

Presentations

A Few “Ok” Libraries (slides): An introduction to Okio and three libraries written with it.

Decoding the Secrets of Binary Data (slides): How data encoding works and how Okio does it.

Ok Multiplatform! (slides): How we changed Okio’s implementation language from Java to Kotlin.

Requirements

Okio 2.x supports Android 4.0.3+ (API level 15+) and Java 7+.

Okio 3.x supports Android 4.0.3+ (API level 15+) and Java 8+.

Okio depends on the Kotlin standard library. It is a small library with strong backward-compatibility.

Recipes

我们写了一些用例来展示如何解决 Okio 使用中的一些问题。你可以随意复制剪切他们,因为这就是他们的作用。

Read a text file line-by-line

使用 Okio.source(File) 来开启一个 Source 来读取一个文件。该方法返回了一个 Source 接口。这个接口非常小,用处有限,因此,我们需要使用 buffer 包装该 source,这有两个好处

  • It makes the API more powerful.
  • It makes your program run faster

每一个 Source 开启后,都需要关闭,代码中需要手动关闭。

在 Kotlin 中,除了 Okio.source(File) ,还可以使用扩展方法 file.source() 来开启一个 Source,同时可以使用 kotlin 中 use 方法来自动关闭流:

@Throws(IOException::class)
fun readLines(file: File) {
  file.source().use { fileSource ->
    fileSource.buffer().use { bufferedFileSource ->
      while (true) {
        val line = bufferedFileSource.readUtf8Line() ?: break
        if ("square" in line) {
          println(line)
        }
      }
    }
  }
}

readUtf8Line() 方法会读取一整行的数据,直到遇到行标志符,例如 \n, \r\n 或者文件的末尾 ,这里会将行标志符去掉。当读到一个空行时,会返回空字符串,而当没有数据可以读时,会返回 null 。

而在 Kotlin 中,我们可以将 source.readUtf8Line() 的调用封装到 generateSequence 的构造器中,一旦返回 null,循环就会结束:

@Throws(IOException::class)
fun readLines(file: File) {
  file.source().buffer().use { source ->
    generateSequence { source.readUtf8Line() }
      .filter { line -> "square" in line }
      .forEach(::println)
  }
}

readUtf8Line() 适合解析接大多数文件,但在某些情况你也可以使用 readUtf8LineStrict(),该方法只有遇到 \n 或者 \r\n 才会返回,如果在此之前遇到了文件末尾,则会直接抛出一个 EOFException。此外,该方法也可以限制读取的字节数来限制输入:

@Throws(IOException::class)
fun readLines(file: File) {
  file.source().buffer().use { source ->
    while (!source.exhausted()) {
      val line = source.readUtf8LineStrict(1024)
      if ("square" in line) {
        println(line)
      }
    }
  }
}

Write a text file

以上我们使用了 Source 与 BufferedSource 来读取一个文件。这里我们使用 Sink 与 BufferedSink 来写入一个文件。这里将功能封装到 BufferedSink 的原因与 BufferedSource 相同。

BufferedSink 没有提供写入一行的 api,你需要手动写入换行符,你可以使用 System.lineSeparator() 来获取当前系统的换行符。在 Windows 中它会返回 \r\n ,而在其他系统中会返回 \n

在 Kotlin 中,我们可以通过内联扩展方法来写出更加紧凑的代码:

@Throws(IOException::class)
fun writeEnv(file: File) {
  file.sink().buffer().use { sink ->
    for ((key, value) in System.getenv()) {
      sink.writeUtf8(key)
      sink.writeUtf8("=")
      sink.writeUtf8(value)
      sink.writeUtf8("\n")
    }
  }
}

这里我们调用了四次 writeUtf8,相比于以下的写法,这种调用多次的写法能防止 jvm 创建多个字符串对象:

sink.writeUtf8(entry.getKey() + "=" + entry.getValue() + "\n"); // Slower!

UTF-8

在上面的 api 中,您可以看到 Okio 非常喜欢UTF-8。早期的计算机系统遭受了许多不兼容的字符编码: ISO-8859-1、 ShiftJIS、 ASCII、 EBCDIC等。编写支持多种字符集的软件非常糟糕,我们甚至没有表情符号!今天我们很幸运,世界上所有地方都标准化了 UTF-8,在之后的系统中很少使用其他字符集。

你可以使用 readString()writeString() 方法来按照指定字符集获取数据。该方法需要手动传入字符集。在当今的程序中大多数都是使用 UTF-8 字符集。

在编码字符串时,您需要注意字符串表示和编码的不同方式。当符号具有重音或其他装饰时,可以将其表示为单个复杂代码点 (é) 或后跟修饰符 (´) 的简单代码点 (e) 。当整个符号是一个单独的代码点,叫做 NFC ;当它是倍数时,它是 NFD。

尽管我们在 I/O 中读取或写入字符串时使用 UTF-8,但当它们在内存中时,Java 字符串使用一种被称为 UTF-16 的过时字符编码。它是一种糟糕的编码,因为它对大多数字符使用16位字符,但有些字符不适合。特别是,大多数表情符号使用两个Java字符。这是有问题的,因为String.length() 返回一个令人惊讶的结果: UTF-16 字符的数量,而不是符号的自然数量。

Café 🍩Café 🍩
FromNFCNFD
Code Pointsc a f é ␣ 🍩c a f e ´ ␣ 🍩
UTF-8 bytes43 61 66 c3a9 20 f09f8da943 61 66 65 cc81 20 f09f8da9
String.codePointCount67
String.length78
Utf8.size1011

在大多数情况下,Okio 可以让您忽略这些问题,专注于您的数据。但是当您需要它们时,有一些方便的 api 来处理低级 UTF-8 字符串。

使用 Utf8.size() 计算将字符串编码为 UTF-8 而不进行实际编码所需的字节数。这对于像协议缓冲区这样以长度为前缀的编码很方便。

Use BufferedSource.readUtf8CodePoint() to read a single variable-length code point, and BufferedSink.writeUtf8CodePoint() to write one.

使用 BufferedSource.readUtf8CodePoint()BufferedSink.writeUtf8CodePoint() 读取或写入单个变长代码点。

Golden Values

Okio 喜欢测试。这个库经过了大量的测试,它的特性在测试应用程序代码时通常很有帮助。我们发现一个非常好用的测试模式是 golden value 测试。这种测试是为了测试旧版应用编码的数据能否被新版的应用解码。

我们使用 Java Serialization 为数据编码。尽管我们必须承认 Java Serialization 是一种好的编码系统,但应用程序还是更倾向于使用其他编码,比如 JSON 或 protobuf 。有一个方法可以接受实例化一个 object,并转换为 ByteString:

@Throws(IOException::class)
private fun serialize(o: Any?): ByteString {
  val buffer = Buffer()
  ObjectOutputStream(buffer.outputStream()).use { objectOut ->
    objectOut.writeObject(o)
  }
  return buffer.readByteString()
}

看着调用了很少方法,但这里的过程还是比较复杂

  1. 我们创建了一个 buffer 作为序列化数据存放的容器,这里 buffer 类似 ByteArrayOutputStream 。
  2. 我们调用 buffer.outputStream() 获取其 OutputStream 对象,当向该 OutputStream 写入数据时,相当于向 buffer 末尾写入数据 。
  3. 我们创建一个 ObjectOutputStream 并装饰我们获取到的 OutputStream ,然后写入我们的 Object。这里使用 use 来自动关闭缓冲流,但关闭一个 buffer 的 OutputStream 不会做任何事情。
  4. 最后我们从 buffer 中调用 readByteString() 来读取 byte string 。这个方法允许我们传入读取的字节大小,缺省则表示读取全部数据。这里总是从 buffer 的起点开始读。

调用我们刚刚写的 serialize() 方法,并打印一个 golden value

val point = Point(8.0, 15.0)
val pointBytes = serialize(point)
println(pointBytes.base64())

最终我们打印 ByteString 的 base64 值,因为 base64 是一种比较紧凑的格式。以下为打印的值:

rO0ABXNyAB5va2lvLnNhbXBsZXMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA

这就是我们的 golden value,我们可以将其作为一个测试用例嵌入进我们的测试用例代码中,构造 ByteString:

val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" +  "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()

之后是将该 ByteString 解码回对象,首先还是创建一个 Buffer,然后在其中写入我们的 ByteString,最终在调用 buffer.inputStream() 获取 inputStream 再用 ObjectInputStream 装饰,最终反序列化该对象:

@Throws(IOException::class, ClassNotFoundException::class)private fun deserialize(byteString: ByteString): Any? {  val buffer = Buffer()  buffer.write(byteString)  ObjectInputStream(buffer.inputStream()).use { objectIn ->    return objectIn.readObject()  }}

现在我们可以使用该 golden value 来测试该解码器

val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" +  "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()!!val decoded = deserialize(goldenBytes) as PointassertEquals(point, decoded)

Write a binary file

对 二进制文件 进行编码与 对文本文件进行编码一样。对这两种情况,Okio 使用同样的 Api,BufferedSink 与 BufferedSource 。这对于同时包含字节和字符数据的二进制格式很方便。

写二进制数据比写文本更危险,因为如果出错,通常很难诊断。要避免这些错误,就要小心以下陷阱:

  • The width of each field. 这是使用的字节数。Okio不包含发送部分字节的机制。如果你需要,则你需要在发送之前自己进行位移或修饰等操作。
  • The endianness of each field. 所有大于一个字节的字段都需要注意其字节端,是大端还是小端。Okio 中,Le 开头的方法都是小端,没有前缀的方法为大端。
  • Signed vs. Unsigned. 在 Java 中没有 unsigned 的类型(除了 char ,因此处理符号的问题通常需要在业务中完成。Okio 提供了 writeByte() 和 writeShort() 方法来简单的处理这些事,参数类型为 int 类型。你可以传入一个无符号数数据直接强转成的 int,例如 255 。Okio 会自动处理。
MethodWidthEndiannessValueEncoded Value
writeByte1 303
writeShort2big300 03
writeInt4big300 00 00 03
writeLong8big300 00 00 00 00 00 00 03
writeShortLe2little303 00
writeIntLe4little303 00 00 00
writeLongLe8little303 00 00 00 00 00 00 00
writeByte1 Byte.MAX_VALUE7f
writeShort2bigShort.MAX_VALUE7f ff
writeInt4bigInt.MAX_VALUE7f ff ff ff
writeLong8bigLong.MAX_VALUE7f ff ff ff ff ff ff ff
writeShortLe2littleShort.MAX_VALUEff 7f
writeIntLe4littleInt.MAX_VALUEff ff ff 7f
writeLongLe8littleLong.MAX_VALUEff ff ff ff ff ff ff 7f

该代码按照 BMP 文件格式对位图进行编码。

@Throws(IOException::class)fun encode(bitmap: Bitmap, sink: BufferedSink) {  val height = bitmap.height  val width = bitmap.width  val bytesPerPixel = 3  val rowByteCountWithoutPadding = bytesPerPixel * width  val rowByteCount = (rowByteCountWithoutPadding + 3) / 4 * 4  val pixelDataSize = rowByteCount * height  val bmpHeaderSize = 14  val dibHeaderSize = 40  // BMP Header  sink.writeUtf8("BM") // ID.  sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize) // File size.  sink.writeShortLe(0) // Unused.  sink.writeShortLe(0) // Unused.  sink.writeIntLe(bmpHeaderSize + dibHeaderSize) // Offset of pixel data.  // DIB Header  sink.writeIntLe(dibHeaderSize)  sink.writeIntLe(width)  sink.writeIntLe(height)  sink.writeShortLe(1) // Color plane count.  sink.writeShortLe(bytesPerPixel * Byte.SIZE_BITS)  sink.writeIntLe(0) // No compression.  sink.writeIntLe(16) // Size of bitmap data including padding.  sink.writeIntLe(2835) // Horizontal print resolution in pixels/meter. (72 dpi).  sink.writeIntLe(2835) // Vertical print resolution in pixels/meter. (72 dpi).  sink.writeIntLe(0) // Palette color count.  sink.writeIntLe(0) // 0 important colors.  // Pixel data.  for (y in height - 1 downTo 0) {    for (x in 0 until width) {      sink.writeByte(bitmap.blue(x, y))      sink.writeByte(bitmap.green(x, y))      sink.writeByte(bitmap.red(x, y))    }    // Padding for 4-byte alignment.    for (p in rowByteCountWithoutPadding until rowByteCount) {      sink.writeByte(0)    }  }}

解码过程中最困难的部分是 bmp 文件格式的填充。该格式规定每行以 4 字节的边界开始,因此需要添加 0 。

其他二进制格式的编码通常非常相似。这里给出一些建议:

  • 用 golden value 编写测试。确保程序发出预期的结果,这可以使调试更容易。
  • 使用 Utf8.size() 计算已编码字符串的字节数。这对于长度前缀格式是必不可少的。
  • 使用 Float.floatToIntBits() 和 Double.doubleToLongBits() 对浮点值进行编码。

Communicate on a Socket

通过网络发送和接收数据有点像写和读文件。我们使用 BufferedSink 对输出进行编码,使用 BufferedSource 对输入进行解码。与文件一样,网络协议可以是文本、二进制或两者的混合。但是在网络和文件系统之间也有一些实质性的区别。

对于文件,您可以直接读写,操作系统会帮我们处理并发,但对于网络,存在并发处理。有些协议是轮流处理的:写请求、读响应、重复。你可以用一个线程来实现这种协议。在其他协议中,您可以同时读写。通常情况下,您需要一个专用线程来进行读取。对于写线程,可以使用专用线程,也可以使用同步线程,这样多个线程就可以共享一个接收器。 Okio 的流对于并发使用并不安全。

接收缓冲区出站数据,以减少 I/O 操作。这是有效的,但这意味着您必须手动调用 flush() 来传输数据。通常,面向消息的协议在每条消息之后刷新。注意,当缓冲数据超过某个阈值时,Okio 将自动刷新。这是为了节省内存,您不应该在交互协议中依赖它。

Okio 内部是通过 java.io.Socket 实现通讯。创建服务器或客户端的 Socket,然后使用 Okio.source(socket) 进行读取,使用 Okio.sink(socket) 进行写入。同时你还可以使用 SSLSocket。并且我们建议你使用 SSLSocket。

调用 socket .close() 可以立即关闭该连接。同时所有它的 Source 与 Sink 立即无法使用,并抛出 IOException。同时还可以为所有 Socket 配置超时。此外还可以通过 source 与 sink 的公开方法来设置超时时间。即使使用其他流来装饰,这个 API 也可以工作。

以下用例实现了一个 SOCKS 代理服务器:

val fromSocket: Socket = ...val fromSource = fromSocket.source().buffer()val fromSink = fromSocket.sink().buffer()

创建 source 与 sink 的 api 与文件类似。注意,一旦你创建了 source 与 sink,则不能在使用该 Socket 的 inputStream 与 outputStream 。否则会出现冲突。

val buffer = Buffer()var byteCount: Longwhile (source.read(buffer, 8192L).also { byteCount = it } != -1L) {  sink.write(buffer, byteCount)  sink.flush()}

上面的循环将数据从源复制到接收器,在每次读取后进行刷新。如果我们不需要刷新,我们可以用一个调用 BufferedSink.writeAll(Source) 来替换这个循环。

read() 的 8192 参数是在返回之前读取的最大字节数。我们可以在这里传递任何值,但我们喜欢 8kib,因为这是 Okio 在单个系统调用中可以做到的最大值。大多数时候,应用程序代码不需要处理这样的限制!

val addressType = fromSource.readByte().toInt() and 0xffval port = fromSource.readShort().toInt() and 0xffff

Okio 使用有符号类型,如 byte 和 short,但协议通常需要无符号值。按位&操作符是Java将有符号值转换为无符号值的首选习惯用法。以下是bytes、shorts 和 int 的备忘单:

TypeSigned RangeUnsigned RangeSigned to Unsigned
byte-128..1270..255int u = s & 0xff;
short-32,768..32,7670..65,535int u = s & 0xffff;
int-2,147,483,648..2,147,483,6470..4,294,967,295long u = s & 0xffffffffL;

Java 没有可以表示无符号长类型的基本类型。

Hashing

作为 Java 程序员,我们工作中经常接触到 哈希。我们在前面介绍了hashCode() 方法,我们知道需要重写这个方法,否则会发生不可预见的坏事情。接下来我们会看到 LinkedHashMap 及其好友。它们建立在hashCode()方法之上,以组织数据以实现快速检索。

在其他地方,我们有加密哈希函数。这些东西到处都在用。HTTPS 证书、Git 提交、BitTorrent 完整性检查和区块链块都使用加密哈希。良好地使用哈希可以提高应用程序的性能、私密性、安全性和简洁性。

每个加密哈希函数接受一个变长输入字节流,并产生一个固定长度的字节字符串值,称为“哈希”。哈希函数有这些重要的特性:

  • Deterministic:每个输入总是产生相同的输出
  • Uniform:每个输出字节串的可能性是相等的。要找到或创建产生相同输出的不同输入对是非常困难的。这被称为“碰撞”。
  • Non-reversible:知道输出并不能帮助你找到输入。注意,如果您知道一些可能的输入,您可以计算它的哈希值,看看它们的哈希值是否匹配。
  • Well-known:哈希 在任何地方都被实现并被严格理解。

好的哈希函数计算起来非常便宜(几十微秒),而反向计算(千万亿次)则非常昂贵。计算和数学的稳步发展使得一度伟大的哈希函数变得不那么昂贵。在选择哈希函数时,要注意不是所有函数都是一样的。 Okio 支持这些常见的加密哈希函数:

  • MD5:128位(16字节)加密哈希。它既不安全又过时。之所以提供此散列,是因为它很流行,而且便于在不安全敏感的遗留系统中使用。
  • SHA-1:160位(20字节)的加密哈希。最近证明了创建SHA-1碰撞是可行的。考虑从SHA-1升级到SHA-256。
  • SHA-256:256位(32字节)加密哈希。SHA-256得到了广泛的理解,而且要反向使用也很昂贵。这是大多数系统应该使用的散列。
  • SHA-512 :512位(64字节)加密哈希。想要逆转是很昂贵的。

计算 Hash 值需要先创建一个长度指定的 ByteString,计算后可以调用 hex() 方法获取其十六进制表示。

val byteString = readByteString(File("README.md"))println("       md5: " + byteString.md5().hex())println("      sha1: " + byteString.sha1().hex())println("    sha256: " + byteString.sha256().hex())println("    sha512: " + byteString.sha512().hex())

也可也使用 Buffer

val buffer = readBuffer(File("README.md"))println("       md5: " + buffer.md5().hex())println("      sha1: " + buffer.sha1().hex())println("    sha256: " + buffer.sha256().hex())println("    sha512: " + buffer.sha512().hex())

source 与 sink 也可以实现:

sha256(blackholeSink()).use { hashingSink ->  file.source().buffer().use { source ->    source.readAll(hashingSink)    println("    sha256: " + hashingSink.hash.hex())  }}
sha256(blackholeSink()).use { hashingSink ->  hashingSink.buffer().use { sink ->    file.source().use { source ->      sink.writeAll(source)      sink.close() // Emit anything buffered.      println("    sha256: " + hashingSink.hash.hex())    }  }}

Okio 还支持 HMAC (哈希消息验证码),它结合了一个秘密和一个哈希。应用程序使用 HMAC 进行数据完整性和身份验证。

val secret = "7065616e7574627574746572".decodeHex()println("hmacSha256: " + byteString.hmacSha256(secret).hex())

与哈希一样,您可以从ByteString、Buffer、HashingSource和HashingSink生成HMAC。注意 Okio 没有为 MD5 实现 HMAC。

其中 Okio使用 Java 的 Java .security. messagedigest 用于加密哈希,javax.crypto.Mac 用于HMAC。

Encryption and Decryption

使用 Okio.Cipher (Sink, Cipher) 或 Okio.cipherSource(Source, Cipher) 通过块密码加密或解密流。

调用者负责使用所选算法、密钥和特定于算法的附加参数(如初始化向量)初始化加密或解密密码。下面的示例显示了 AES 加密的典型用法,其中 key 和 iv 参数都应该是 16 字节长。

fun encryptAes(bytes: ByteString, file: File, key: ByteArray, iv: ByteArray) {  val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")  cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))  val cipherSink = file.sink().cipherSink(cipher)  cipherSink.buffer().use {     it.write(bytes)   }}fun decryptAesToByteString(file: File, key: ByteArray, iv: ByteArray): ByteString {  val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")  cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))  val cipherSource = file.source().cipherSource(cipher)  return cipherSource.buffer().use {     it.readByteString()  }}

File System Examples

Okio 最近获得了一个多平台文件系统 API。这些示例可以在JVM、native 和 Node.js 平台上工作。在下面的例子中 fileSystem 是 fileSystem 的一个实例,例如 fileSystem。系统或 FakeFileSystem。

读取 readme.md 文件的所有数据

val path = "readme.md".toPath()val entireFileString = fileSystem.read(path) {  readUtf8()}

读取 thumbnail.png 的数据并加载到 ByteString:

val path = "thumbnail.png".toPath()val entireFileByteString = fileSystem.read(path) {  readByteString()}

读取 /etc/hosts 文件,并按行写入 List<String>

val path = "/etc/hosts".toPath()val allLines = fileSystem.read(path) {  generateSequence { readUtf8Line() }.toList()}

读取 index.html 文件中第一个 <html> 之前的数据:

val path = "index.html".toPath()val untilHtmlTag = fileSystem.read(path) {  val htmlTag = indexOf("<html>".encodeUtf8())  if (htmlTag != -1L) readUtf8(htmlTag) else null}

使用 ByteString 写入文件:

val path = "data.bin".toPath()fileSystem.write(path) {  val byteString = "68656c6c6f20776f726c640a".decodeHex()  write(byteString)}

List<String>中数据按行写入:

val path = "readme.md".toPath()val lines = listOf(  "Hello, World",  "------------",  "",  "This is a sample file.",  "")fileSystem.write(path) {  for (line in lines) {    writeUtf8(line)    writeUtf8("\n")  }}

Releases

Our change log has release history.

repositories {    maven {        url = uri("https://oss.sonatype.org/content/repositories/snapshots/")    }}dependencies {   implementation("com.squareup.okio:okio:3.0.0-alpha.9")}