网络编程——使用更简洁且性能高效的Okio库来做IO和NIO

Posted CrazyMo_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程——使用更简洁且性能高效的Okio库来做IO和NIO相关的知识,希望对你有一定的参考价值。

引言

OKHttp相比做Java 和android 的无所不知吧,早期通过Gradle 引入OKHttp时都需要额外引入Okio的库,最早使用时和大多数一样我也没有去关注,直到后面慢慢喜欢去追根结底,深入到各种框架的核心中才发现Okio是一个宝库,不仅仅是从提供给我们的高效性能,设计思想也是有很多值得借鉴的,最后给个建议抛弃传统Java的IO、NIO吧,无论你是Java还是Android都应该尽快拥抱Okio。文章整理摘自Okio官网文档以后更多精选系列文章以高度完整的系列形式发布在公众号,真正的形成一套完整的技术栈,欢迎关注,目前第一个系列是关于Android系统启动系列的文章,大概有二十几篇干货:

基于 OKio 1.x

一、Okio 概述

Okio最初是OkHttp的一个组件,作为OKHttp内部使用的IO库,可以说是一个比JDK原生io和nio更高效的优秀组件,Okio 底层采用对象池复用技术避免了频繁的GC,而且对IO对象进行了高度的抽象(有点类似socket的设计思想),使得访问、存储和处理数据变得更加高效和便捷。

二、Okio的核心元素

1、Okio的两种数据类型

Okio是进行了对数据类型进行了高度的抽象,主要就是围绕两种类型构建的:

  • ByteStrings
  • Buffers

所有数据的操作都抽象统一到对应的API中,也正是因为他们内部的一些设计使得Okio相对于传统IO 更节省CPU和内存资源。

1.1、ByteStrings ——不可变的字节序列

ByteString类实现原JDK生的Serializable, Comparable接口,本质就是一个不可变的字节序列,对于字符串数据对应的最基本类型就是StringByteString相当于是更聪明的String,因为它自带灵活的编码和解码为十六进制、base64和utf-8机制

当将用UTF-8将字符串编码为ByteString时,内部会对该字符串进行缓存处理,在解码时就可以直接从缓存中获取。

1.2、Buffers——可变的字节序列

Buffer类实现Okio自定义的BufferedSource, BufferedSink, Cloneable, ByteChannel接口,本质是是一个可变的字节序列。与Arraylist类似,不需要预先设置缓冲区的大小,可以将缓冲区看成一个队列结构读写,将数据写到队尾,然后从队头读取。Buffer是作为片段的链表实现的,当将数据从一个缓冲区移动到另一个缓冲区时,它会重新分配片段的持有关系,而不是跨片段复制数据。这对多线程特别有用,尤其与网络交互的子线程可以与工作线程交换数据,而无需任何复制或多余的操作。

2、Okio的Stream流类型

Okio对于Stream流类型也是进行了高度的抽象,所有流都可以用以下两种类型来表示:

  • Source
  • Sink

可以看成是所有的流的基类。

2.1、Source——Okio中的InputStream

Supplies a stream of bytes. Use this interface to read data from wherever it’s located: from the network, storage, or a buffer in memory.

public interface Sink extends Closeable, Flushable {
  /** Removes {@code byteCount} bytes from {@code source} and appends them to this. */
  void write(Buffer source, long byteCount) throws IOException;

  /** Pushes all buffered bytes to their final destination. */
  @Override void flush() throws IOException;

  /** Returns the timeout for this sink. */
  Timeout timeout();

  /**
   * Pushes all buffered bytes to their final destination and releases the
   * resources held by this sink. It is an error to write a closed sink. It is
   * safe to close a sink more than once.
   */
  @Override void close() throws IOException;
}

当我们需要读数据时就可以直接调用Source类及其子类BufferedSource的方法即可。BufferedSource的设计有点类似于JDK原生的I/O架构设计,相当于是一个Source的装饰者角色,当通过Okio.buffer(fileSource)来得到Source输入流时,在方法内部首先会生成一个实现了BufferedSource接口的RealBufferedSource类对象,二RealBufferedSource内部持有Buffer缓冲对象可使IO速度更快。

2.2、Sink——Okio中的OutputStream

Receives a stream of bytes. Use this interface to write data wherever it’s needed: to the network, storage, or a buffer in memory. Sinks may be layered to transform received data, such as to compress, encrypt, throttle, or add protocol framing.

public interface Source extends Closeable {
  /**
   * Removes at least 1, and up to {@code byteCount} bytes from this and appends
   * them to {@code sink}. Returns the number of bytes read, or -1 if this
   * source is exhausted.
   */
  long read(Buffer sink, long byteCount) throws IOException;

  /** Returns the timeout for this source. */
  Timeout timeout();

  /**
   * Closes this source and releases the resources held by this source. It is an
   * error to read a closed source. It is safe to close a source more than once.
   */
  @Override void close() throws IOException;
}

当我们需要写数据时就可以直接调用Sink类及其子类BufferedSink的方法即可。BufferedSink的设计与BufferedSource类似,当通过Okio.buffer(fileSource)来得到Source输入流时,在方法内部首先会生成一个实现了BufferedSink接口的RealBufferedSink类对象,二RealBufferedSink内部持有Buffer缓冲对象可使IO速度更快。

2.3、Okio 的Stream 和JDK的Stream的对比

Source和Sink 与 JDK中的InputStream和OutputStream可以互相操作,即可以将任何Source视为一个InputStream,也可以将任何InputStream视为一个Source;Sink和OutputStream之间也是如此。但它们之间还是存在一些区别的:

  • 超时(Timeouts),与java.io的socket字流不同,Okio流支持对底层I/O超时机制的访问,其read()write()方法都给予超时机制。

  • 易于实现且安全Source只定义了三个方法——read()、close()和timeout(),避免了调用java.io.InputStream#available available()方法或单字节读取操作时可能会导致未知的隐患。

    Source avoids the impossible-to-implement java.io.InputStream#available available() method. Instead callers specify how many bytes BufferedSource#require() method.

  • 使用便捷,虽然Okio 的流基类接口SourceSink的实现只有三种方法,其子接口BufferedsourceBufferedsink定了丰富的API,除了支持用户自己扩展实现之外,内部也是封装了绝大部分类型的流操作对应的实现类供用户直接调用,使得一些类似C/C++的偏移操作变得很简单等等。

  • 调用者不必显著区分字节流和字符流的操作,都是数据,可以用字节UTF-8字符串big-endian的32位整数little-endian的短整数等任何形式进行读写操作,不需要像JDK 中额外的流转换操作,只需直接调用对应的方法即可。

三、Okio 的简单使用

1、BufferedSource读文本文件

  • 通过Okio.source(file)方法得到Source输入流
  • 再把原始输入流包装成BufferedSource
  • 调用BufferedSource的方法,比如以行形式读取则调用readLines。
public void readLines(File file) throws IOException {
    //不论try-catch中使用的资源是自己创造的还是java内置的类型,try-with-resources都是一个能够确保资源能被正确地关闭的强大方法。
    try (Source fileSource = Okio.source(file);
         BufferedSource bufferedSource = Okio.buffer(fileSource)) {
        while (true) {
            String line = bufferedSource.readUtf8Line();
            if (line == null) break;

            if (line.contains("square")) {
                System.out.println(line);
            }
        }
    }
}

另一种形式:

public void readLines(File file) throws IOException {
  try (BufferedSource source = Okio.buffer(Okio.source(file))) {
    for (String line; (line = source.readUtf8Line()) != null; ) {
      if (line.contains("square")) {
        System.out.println(line);
      }
    }
  }
}

try-with-source是jdk1.7开始提供的语法糖,在try语句()里面的资源对象,jdk会自动调用它的close方法去关闭它,就不用自己在finally中去手动关闭了, 即便try里有多个资源对象也是没有影响。但是在android里面使用的话,会提示你要求API level最低为19才可以。

一般说来readUtf8Line()方法就可以用于读取适用于大多数文件,但对于某些特例可以考虑使用readUtf8LineStrict()功能大同小异,区别在于readUtf8LineStrict()要求每一行都以**\\n或\\r\\n**结尾。如果在这之前遇到文件结尾,它将抛出一个EOFException。

在JDK 1.7后可以调用 System.lineSeparator()方法自动获取对应系统下的换行标志符。

  • \\n——Unix 系下的行结束标志
  • \\r\\r——Window系下的行结束标志
 The returned string will have at most {@code limit} UTF-8 bytes, and the maximum number
of bytes scanned is {@code limit + 2}. If {@code limit == 0} this will always throw an {@code EOFException} because no bytes will be scanned.public void readLines(File file) throws IOException {
  try (BufferedSource source = Okio.buffer(Okio.source(file))) {
    while (!source.exhausted()) {
        /**
        * The returned string will have at most {@code limit} UTF-8 bytes, and the maximum number
of bytes scanned is {@code limit + 2}. If {@code limit == 0} this will always throw an {@code EOFException} because no bytes will be scanned.
		  * Buffer buffer = new Buffer();
           *   buffer.writeUtf8("12345\\r\\n");
           *   // This will throw! There must be \\r\\n or \\n at the limit or before it.
           *   buffer.readUtf8LineStrict(4);
           *   // No bytes have been consumed so the caller can retry.
           *   assertEquals("12345", buffer.readUtf8LineStrict(5));	 
        */
        String line = source.readUtf8LineStrict(1024L);
        System.out.println(line);
    }
  }
}

除了读文本文件,也可以通过ObjectOutputStream读取序列化后对象

2、序列化和反序列化

2.1、将对象序列化为ByteString

Okio的Buffer替代JDK的ByterrayOutputstream,再从Buffer中获得输出流对象,并通过JDK的ObjectOutputStream将输出流写入到到buffer缓冲区当中,当你向Buffer中写数据时,总是会写到缓冲区的末尾(由于内部的数据结构决定的),最后通过buffer对象的readByteString()从缓冲区读取一个ByteString对象,此时会从缓冲区的头部开始读取,readByteString()方法(可指定要读取的字节数,但如果不指定,则读取全部内容)

private ByteString serialize(Object o) throws IOException {
  Buffer buffer = new Buffer();
  try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) {
    objectOut.writeObject(o);
  }
  return buffer.readByteString();
}

通过以上方法可以将一个对象进行序列化并得到的ByteString对象,直接调用**ByteString#base64()**方法就可以进行Base64编码:

Point point = new Point(8.0, 15.0);
ByteString pointBytes = serialize(point);
System.out.println(pointBytes.base64());

Okio把这个base64 处理后得到的字符串称为Golden value,对Golden value 进行ByteString#decodeBase64

ByteString goldenBytes = ByteString.decodeBase64(pointBytes.base64());

2.2、将ByteString 反序列化为对象

就可以从Golden value中得到原始的ByteString,再通过原始的ByteString构造JDK的ObjectInputStream对象并通过其readObject方法反序列化就可以得到对象

private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException {
  Buffer buffer = new Buffer();
  buffer.write(byteString);
  try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) {
    return objectIn.readObject();
  }
}

简而言之就是可以通过Golden value来确保序列化后和反序列化的对象是一致的。

Point point = new Point(8.0, 15.0);
ByteString pointBytes = serialize(point);
String glodenValue =pointBytes.base64();
ByteString goldenBytes = ByteString.decodeBase64(glodenValue);
Point decoded = (Point) deserialize(goldenBytes);
assertEquals(new Point(8.0, 15.0), decoded);//true

Okio序列化与JDK原生序列化有一个明显的区别就是GodenValue可以在不同客户端之间兼容(只要序列化和反序列化的Class是相同的)。比如我在PC端使用Okio序列化一个User对象生成的GodenValue字符串,这个字符串你拿到手机端照样可以反序列化出来User对象。

3、BufferedSink写文本文件

  • 通过Okio.sink()方法得到Sink输出流
  • 再把原始输出流包装成BufferedSink
  • 调用BufferedSink的write系列方法
public void writeEnv(File file) throws IOException {
  try (Sink fileSink = Okio.sink(file);
       BufferedSink bufferedSink = Okio.buffer(fileSink)) {

    for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
      bufferedSink.writeUtf8(entry.getKey());
      bufferedSink.writeUtf8("=");
      bufferedSink.writeUtf8(entry.getValue());
      bufferedSink.writeUtf8("\\n");
        System.lineSeparator()
    }
  }
}

其他用法和设计思想参考读,write系列方法大多数都是带UTF-8编码的读写方法,Okio推荐优先使用UTF-8的方法,因为UTF-8在世界各地都已标准化。但还需要使用其他编码的字符集,可以使用readString() 和writeString()来指定字符编码参数,但在大多数情况下应该只使用带UTF-8的方法。

4、写二进制的字节序列

Okio中无论是任何类型的写操作都是由BufferedSink 发起,而且相对于传统的IO,Okio 写二进制字节序列尤其方便,实现了类似C/C++中的指针位移后再插入的功能,也因此引入了几个概念:

  • The width of each field——要写入的字节的数量,但Okio没有写入部分字节数的机制,但如果需要的话,需要自己在写之前对字节进行位移等运算。

  • The endianness of each field——计算机中对于大于一个字节的二进制序列都是有分为大小端排序的之分的,字节的顺序是从最高位到最低位(大端 big endian),还是从最低位到最高位(小端 little endian)。

    Okio中针对小端排序的方法都带有Le的后缀;而没有后缀的方法默认是大端排序的

  • Signed vs Unsigned——有符号和无符号,Java中除了char是无符号的基础类型,剩下的基础类型都是有符号的,因此可直接把一个“无符号”字节像255(int类型)传入到writeByte()writeShort()方法,Okio会自己去处理。

方法宽度字节排序编码后的值
writeByte1303
writeShort2big300 03
writeInt4big300 00 00 03
writeLong8big300 00 00 00 00 00 00 03
writeShortLe2little303 00
writeIntLe 4little303 00 00 00
writeLongLe8little303 00 00 00 00 00 00 00
writeByte1Byte.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

以下这个例子,基本上就是对于Bitmap算法协议的解析和实现,通过Okio 快速实现。

public final class BitmapEncoder {
    static final class Bitmap {
        private final int[][] pixels;
        Bitmap(int[][] pixels) {
            this.pixels = pixels;
        }
        int width() {
            return pixels[0].length;
        }
        int height() {
            return pixels.length;
        }
        int red(int x, int y) {
            return (pixels[y][x] & 0xff0000) >> 16;
        }
        int green(int x, int y) {
            return (pixels[y][x] & 0xff00) >> 8;
        }
        int blue(int x, int y) {
            return (pixels[y][x] & 0xff);
        }
    }
    /**
     * Returns a bitmap that lights up red subpixels at the bottom, green subpixels on the right, and
     * blue subpixels in bottom-right.
     */
    Bitmap generateGradient() {
        int[][] pixels = new int[1080][1920];
        for (int y = 0; y < 1080; y++) {
            for (int x = 0; x < 1920; x++) {
                int r = (int) (y / 1080f * 255);
                int g = (int) (x / 1920f * 255);
                int b = (int) ((Math.hypot(x, y) / Math.hypot(1080, 1920)) * 255);
                pixels[y][x] = r << 16 | g << 8 | b;
            }
        }
        return new Bitmap(pixels);
    }

    void encode(Bitmap bitmap, File file) throws IOException {
        try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {
            encode(bitmap, sink);
        }
    }

    /**
     * https://en.wikipedia.org/wiki/BMP_file_format
     */
    void encode(Bitmap bitmap, BufferedSink sink) throws IOException {
        int height = bitmap.height();
        int width = bitmap.width();

        int bytesPerPixel = 3;
        int rowByteCountWithoutPadding = (bytesPerPixel * width);
        int rowByteCount = ((rowByteCountWithoutPadding + 3) / 4) * 4;
        int pixelDataSize = rowByteCount * height;
        int bmpHeaderSize = 14;
        int 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);
        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 (int y = height - 1; y >= 0; y--) {
            for (int x = 0; x < width; x++) {
                sink.writeByte(bitmap.blue(x, y));
                sink.writeByte(bitmap.green(x, y));
                sink.writeByte(bitmap.red(x, y));
            }

            // Padding for 4-byte alignment.
   

以上是关于网络编程——使用更简洁且性能高效的Okio库来做IO和NIO的主要内容,如果未能解决你的问题,请参考以下文章

[小黄书后台]更高效的nodejs调试

玩转 Java8 Stream,让你代码更高效紧凑简洁

08 | Android 高级进阶(源码剖析篇) Square 高效易用的 IO 框架 okio

由已分配的 ByteString 支持的高效 okio 源?

2021 Google 开发者大会 | 更简洁更高效,创造更流畅的移动端用户体验

13 | Android 高级进阶(源码剖析篇) Square 高效易用的 IO 框架 okio