Okio—— 更加高效易用的IO库

2019-04-14 20:09发布

class="markdown_views prism-dracula"> 在OkHttp的源码中经常能看到Okio的身影,所以单独拿出来学习一下,作为OkHttp的低层IO库,Okio确实比传统的java输入输出流读写更加方便高效。Okio补充了java.io和java.nio的不足,使访问、存储和处理数据更加容易,它起初只是作为OKHttp的一个组件,现在你可以独立的使用它来解决一些IO问题。 ByteStrings and Buffers
Okio是围绕这两种类型构建的,它们将大量功能打包到一个简单的API中:
  • ByteString是不可变的字节序列。对于字符数据,最基本的就是String。而ByteString就像是String的兄弟一般,它使得将二进制数据作为一个变量值变得容易。这个类很聪明:它知道如何将自己编码和解码为十六进制、base64和utf-8。
  • Buffer是一个可变的字节序列。像Arraylist一样,你不需要预先设置缓冲区的大小。你可以将缓冲区读写为一个队列:将数据写到队尾,然后从队头读取。

在内部,ByteStringBuffer做了一些聪明的事情来节省CPU和内存。如果您将UTF-8字符串编码为ByteString,它会缓存对该字符串的引用,这样,如果您稍后对其进行解码,就不需要做任何工作。
Buffer是作为片段的链表实现的。当您将数据从一个缓冲区移动到另一个缓冲区时,它会重新分配片段的持有关系,而不是跨片段复制数据。这对多线程特别有用:与网络交互的子线程可以与工作线程交换数据,而无需任何复制或多余的操作。
Sources and Sinks
java.io设计的一个优雅部分是如何对流进行分层来处理加密和压缩等转换。Okio有自己的stream类型: SourceSink,分别类似于java的InputstreamOutputstream,但是有一些关键区别:
  • 超时(Timeouts)。流提供了对底层I/O超时机制的访问。与java.io的socket字流不同,read()write()方法都给予超时机制。
  • 易于实施source只声明了三个方法:read()close()timeout()。没有像available()或单字节读取这样会导致正确性和性能意外的危险操作。
  • 使用方便。虽然sourcesink的实现只有三种方法可写,但是调用方可以实现BufferedsourceBufferedsink接口, 这两个接口提供了丰富API能够满足你所需的一切。
  • 字节流和字符流之间没有人为的区别。都是数据。你可以以字节、UTF-8字符串、big-endian的32位整数、little-endian的短整数等任何你想要的形式进行读写;不再有InputStreamReader
  • 易于测试Buffer类同时实现了BufferedSourceBufferedSink接口,因此测试代码简单明了。

Sources 和 Sinks分别与InputStreamOutputStream交互操作。你可以将任何Source看做InputStream ,也可以将任何InputStream 当做Source。对于SinkOutputstream也是如此。

读文本文件

public void readLines(File file) throws IOException { Source fileSource = Okio.source(file); BufferedSource bufferedSource = Okio.buffer(fileSource); for (String line; (line = bufferedSource.readUtf8Line()) != null; ) { System.out.println(line); } bufferedSource.close(); } 这个示例代码是用来读取文本文件的,Okio通过Okio.source(File) 的方式来读取文件流,它返回的是一个Source对象,但是Source对象的方法是比较少的(只有3个),因此Okio提供了一个装饰者对象接口BufferedSource,通过Okio.buffer(fileSource)来生成,这个方法内部实际会生成一个RealBufferedSource类对象,RealBufferedSource内部持有Buffer缓冲对象可使IO速度更快,该类实现了BufferedSource接口,而BufferedSource接口提供了大量丰富的接口方法:

可以看到,几乎你想从输入流中读取任何的数据类型都可以,而不需要你自己去转换,可以说是非常强大而且人性化了,除了read方法以外,还有一些别的方法:

在上面的示例代码中,打开输入流对象的方法需要负责关闭对象资源,调用close方法,okio官方推荐使用java的try-with-source语法,上面示例代码可以写成下面这样: public void readLines(File file) throws IOException { try (BufferedSource bufferedSource = Okio.buffer(Okio.source(file))) { for (String line; (line = bufferedSource.readUtf8Line()) != null; ) { System.out.println(line); } } } try-with-source是jdk1.7开始提供的语法糖,在try语句()里面的资源对象,jdk最终会自动调用它的close方法去关闭它, 即便try里有多个资源对象也是可以的,这样就不用你手动去关闭资源了。但是在android里面使用的话,会提示你要求API level最低为19才可以。 readUtf8Line()方法适用于大多数文件。对于某些用例,还可以考虑使用readUtf8LineStrict()。类似readUtf8Line(),但它要求每一行都以 结尾。如果在这之前遇到文件结尾,它将抛出一个EOFException。它还允许设置一个字节限制来防止错误的输入。 public void readLines(File file) throws IOException { try (BufferedSource source = Okio.buffer(Okio.source(file))) { while (!source.exhausted()) { String line = source.readUtf8LineStrict(1024L); System.out.println(line); } } }

写文本文件

public void writeEnv(File file) throws IOException { 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(" "); } bufferedSink.close(); } 类似于读文件使用SourceBufferedSource, 写文件的话,则是使用的SinkBufferedSink,同样的在BufferedSink接口中也提供了丰富的接口方法:
其中Okio.buffer(fileSink)内部返回的实现对象是一个RealBufferedSink类的对象, 跟RealBufferedSource一样它也是一个装饰者对象,具备Buffer缓冲功能。同样,以上代码可以使用jdk的try-with-source语法获得更加简便的写法: public void writeEnv(File file) throws IOException { try (BufferedSink sink = Okio.buffer(Okio.sink(file))) { sink.writeUtf8("啊啊啊") .writeUtf8("=") .writeUtf8("aaa") .writeUtf8(" "); } } 其中的换行符 ,Okio没有提供单独的api方法,而是要你手动写,因为这个跟操作系统有关,不过你可以使用System.lineSeparator()来代替 ,这个方法在Windows上返回的是" "在UNIX上返回的是" "。
在上面的代码中,对writeUtf8()进行了四次调用, 这样要比下面的代码更高效,因为虚拟机不必对临时字符串进行创建和垃圾回收。 sink.writeUtf8(entry.getKey() + "=" + entry.getValue() + " "); Gzip压缩和读取 //zip压缩 GzipSink gzipSink = new GzipSink(Okio.sink(file)); BufferedSink bufferedSink = Okio.buffer(gzipSink); bufferedSink.writeUtf8("this is zip file"); bufferedSink.flush(); bufferedSink.close(); //读取zip GzipSource gzipSource = new GzipSource(Okio.source(file)); BufferedSource bufferedSource = Okio.buffer(gzipSource); String s = bufferedSource.readUtf8(); bufferedSource.close();

UTF-8

在上面的代码中,可以看到使用的基本都是带UTF-8的读写方法。Okio推荐优先使用UTF-8的方法,why UTF-8? 这是因为UTF-8在世界各地都已标准化,而在早期的计算机系统中有许多不兼容的字符编码如:ISO-8859-1、ShiftJIS、 ASCII、EBCDIC等,使用UTF-8可以避免这些问题。 如果你需要使用其他编码的字符集,可以使用readString()writeString(),这两个方法可以指定字符编码参数,但在大多数情况下应该只使用带UTF-8的方法。 在编码字符串时,需要特别注意字符串的表达形式和编码方式。当字形有重音或其他装饰时,情况可能会有点复杂。尽管在I/O中读写字符串时使用的都是UTF-8,但是当在内存中,Java字符串使用的是已过时的UTF-16进行编码的。这是一种糟糕的编码格式,因为它对大多数字符使用 16-bit char,但有些字符不适合。特别是大多数的表情符号使用的是两个Java字符, 这时就会出现一个问题: String.length()返回的结果是utf-16字符的数量,而不是字体原本的字符数量。 Café