OkHttp是Android中蕴含的功能强大的HTTP客户端,此框架平时用的还挺多的,然而明天的配角是OkHttp的低层IO库——Okio,Okio是对java.io和java.nio的补充,使拜访、存储和解决数据变得更加容易。 这里是它的官网:https://square.github.io/okio/ , 最开始它作为OKHttp的一个组件,当初能够独立应用它来解决一些IO问题,接下来的内容来自对Okio官网的文档以及一些代码示例。

<!-- more -->

ByteString与Buffer

Okio是围绕这两种类型构建的,它们将大量性能集成到了简略的API中:

ByteString 是一个不可变的字节序列,String的根底是字符,而ByteString就像是String的兄弟一样,它能够轻松将二进制数据视为某些值。这个类十分聪慧:它晓得如何对本人进行十六进制,base64和UTF-8编码和解码。

Buffer 是可变的字节序列。与ArrayList一样,无需事后设置缓冲区大小。以队列的形式读取和写入缓冲区:将数据写入开端,而后从队列头部读取。没有必要去治理读取地位,范畴或容量。

在外部,ByteString和Buffer做一些奇妙的事件来节俭CPU和内存。 如果将UTF-8字符串编码为ByteString,它会缓存对该字符串的援用,以便当前进行解码时无需做任何工作。

缓冲区被实现为段的链表。当您将数据从一个缓冲区移到另一个缓冲区时,它会重新分配段的所有权,而不是跨缓冲区复制数据。这种办法对多线程程序特地有用:与网络申请相干的线程能够与工作线程替换数据,而无需任何复制或多余的操作。

Source与Sink

java.io中的一个优雅的设计是如何对流进行分层来解决加密和压缩等转换。同样的Okio有本人的stream类型:Source和Sink,别离相似于java的Inputstream和Outputstream,然而有一些要害区别:

  • 超时(Timeout):流提供了对底层I/O超时机制的拜访。与java.io的socket字节流不同,read()和write()办法都给予超时机制。
  • 实现简略: Source只申明了三个办法:read()、close()和timeout()。没有像available()或单字节读取这样会导致性能降落问题。
  • 使用方便:尽管source和sink中只有三个办法须要实现,然而调用方能够实现Bufferedsource和Bufferedsink接口,这两个接口提供了丰盛API可能满足你所须要的所有。
  • 字节流和字符流的解决没有直观的区别:因为它们都是数据。你能够以字节、UTF-8字符串、big-endian的32位整数、little-endian的短整数等任何你想要的模式进行读写;再也不须要InputStreamReader!
  • 测试简略: Buffer类同时实现了BufferedSource和BufferedSink,因而测试代码简单明了。

Sources 和 Sinks别离与InputStream和OutputStream交互操作。你能够将任何Source看做InputStream ,也能够将任何InputStream当做Source。对于Sink和Outputstream也是如此。

Okio的应用示例

这是它的Maven形式依赖:

<dependency>    <groupId>com.squareup.okio</groupId>    <artifactId>okio</artifactId>    <version>2.9.0</version></dependency>

1、逐行读取文本

public void readLines(File file) throws IOException {    try (Source fileSource = Okio.source(file);         BufferedSource bufferedSource = Okio.buffer(fileSource)) {        while (true) {            String line = bufferedSource.readUtf8Line();            if (line == null) break;            System.out.println(line);        }    }}

其中readUtf8Line()这个API读取所有数据,直到下一行分隔符 \n、\r\n或文件开端。它以字符串模式返回该数据,并在最初省略定界符。当遇到空行时,该办法将返回一个空字符串。 如果没有更多要读取的数据,它将返回null,所以应用for来代替while(true)也是OK的,这样的写法会让程序更加紧凑:

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

2、将字符串写入文本文件

下面咱们应用了Source和BufferedSource来读取文件。在写入文件时,咱们应用一个Sink和一个BufferedSink。他们有着殊途同归之处:性能更弱小的API和更高的性能。

public void writeToFile(File file) throws IOException {    try (Sink fileSink = Okio.sink(file);         BufferedSink bufferedSink = Okio.buffer(fileSink)) {        bufferedSink.writeUtf8("Hello");        bufferedSink.writeUtf8("\n");        bufferedSink.writeAll(Okio.source(new File("my.txt")));    }}

3、UTF-8编码

在以上API中,能够看到Okio十分喜爱UTF-8。晚期的计算机系统遇到了许多不兼容的字符编码:ISO-8859-1,ASCII,EBCDIC等。编写反对多种字符集的软件太蹩脚了,咱们甚至没有表情符号!明天,咱们很侥幸,全世界各地都曾经在UTF-8上实现了标准化,而在遗留零碎中很少应用其余字符集。

如果你须要其余字符集,则能够应用readString()和writeString()。 这些办法要求传入指定字符集的参数。 否则,可能会意外地创立只能由本地计算机读取的数据,大多数程序应该仅应用writeUtf8()这类办法。

只管每当咱们在I/O中读写字符串时都应用UTF-8,但当它们在内存中时,Java字符串会应用过期的字符编码UTF-16。这是一种谬误的编码方式,因为它对大多数字符应用16位字符,但有些字符不适合。 特地是,大多数表情符号应用两个Java字符。这是有问题的,因为String.length()返回一个令人诧异的后果:UTF-16字符数而不是字体本来的字符数量:

String s1 = "Café \uD83C\uDF69";String s2 = "Cafe \uD83C\uDF69";System.out.println(s.length());System.out.println(s2.length());

在大多数状况下,Okio能够让你疏忽这些问题并专一于数据。然而当你须要它们时,能够使用方便的API解决低级UTF-8字符串。应用Utf8.size()来计算将字符串编码为UTF-8所需的字节数(然而并不会真正去做一次编码操作)。这在诸如协定缓冲区中解决固定长度前缀的时候十分不便。

应用BufferedSource.readUtf8CodePoint()读取一个Codepoint,并使BufferedSink.writeUtf8CodePoint()写入一个Codepoint。

4、序列化和反序列化

Okio喜爱测试。该库自身曾经过严格的测试,咱们发现一种十分有用的模式是"黄金价值"测试,此类测试的目标是确认以后程序能够平安地解码应用程序的晚期版本编码的数据。

咱们将通过应用Java序列化对值进行编码来阐明这一点。只管咱们必须否定Java序列化是一个蹩脚的编码零碎,并且大多数程序应该更喜爱JSON或protobuf之类的其余格局!无论如何,这是一个获取对象,对其进行序列化并以ByteString返回后果的办法:

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

这里应用Buffer对象代替Java的ByteArrayOutputstream,而后从buffer中取得输入流对象,并通过ObjectOutputStream写入对象到buffer缓冲区当中,当你向Buffer中写数据时,总是会写到缓冲区的开端。最初,通过buffer对象的readByteString()从缓冲区读取一个ByteString对象,这会从缓冲区的头部开始读取,readByteString()办法能够指定要读取的字节数,如果不指定,则读取全部内容。

咱们利用下面的办法将一个对象进行序列化,并失去的ByteString对象依照base64格局进行输入:

Point point = new Point(8, 15);ByteString pointBytes = serialize(point);System.out.println(pointBytes.base64());
rO0ABXNyAA5qYXZhLmF3dC5Qb2ludLbEinI0fsgmAgACSQABeEkAAXl4cAAAAAgAAAAP

Okio将这个字符串称之为Golden Value,接下来,咱们尝试将这个字符串(Golden Value)反序列化为一个Point对象,首先转回ByteString对象:

public static void main(String[] args) throws Exception {    Point point = new Point(8, 15);    ByteString pointBytes = new App().serialize(point);    String base64 = pointBytes.base64();    System.out.println(base64);    ByteString byteString = ByteString.decodeBase64(base64);    Point other = (Point) new App().deserialize(byteString);    System.out.println(other.equals(point)); // true}private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException {    Buffer buffer = new Buffer();    buffer.write(byteString);    try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) {        Object result = objectIn.readObject();        if (objectIn.read() != -1) throw new IOException("Unconsumed bytes in stream");        return result;    }}private ByteString serialize(Object o) throws IOException {    Buffer buffer = new Buffer();    try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) {        objectOut.writeObject(o);    }    return buffer.readByteString();}

这样咱们能够在不毁坏兼容性的状况下更改对象的序列化形式。

这个序列化与Java原生的序列化有一个显著的区别就是GodenValue能够在不同客户端之间兼容(只有序列化和反序列化的Class是雷同的)。什么意思呢,比方我在PC端应用Okio序列化一个User对象生成的GodenValue字符串,这个字符串你拿到手机端照样能够反序列化进去User对象。

5、将字节流写入文件

编码二进制文件与编码文本文件没有什么不同。Okio应用雷同的BufferedSink和BufferedSource字节。这对于同时蕴含字节和字符数据的二进制格局十分不便。写入二进制数据比写入文本更危险,因为如果你犯了谬误,通常很难诊断,防止这样的谬误须要留神以下几点:

  • 每个字段的宽度:即字节的数量。Okio没有开释局部字节的机制。如果你需要的话,须要本人在写操作之前对字节进行shift和mask运算。
  • 每个字段的字节序:所有多字节的字段都具备结束符:字节的程序是从最高位到最低位(大字节 big endian),还是从最低位到最高位(小字节 little endian)。Okio中针对小字节排序的办法都带有Le的后缀;而没有后缀的办法默认是大字节排序的。
  • 有符号和无符号: Java没有无符号的根底类型(除了char!)因而,在应用程序层常常会遇到这种状况。为方便使用,Okio的writeByte() 和 writeShort()办法能够承受int类型。你能够间接传递一个无符号字节像255,Okio会做正确的解决。
办法宽度字节排序编码后的值
writeByte1303
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
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

上面的示例代码是依照 BMP文件格式 对文件进行编码:

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.    for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) {      sink.writeByte(0);    }  }}

代码中对文件依照BMP的格局写入二进制数据,这会生成一个bmp格局的图片文件,BMP格局要求每行以4字节开始,所以代码中加了很多0来做字节对齐。

编码其余二进制的格局十分类似。一些值得注意的点:

  • 应用Golden values编写测试,对于确认程序的预期后果能够使调试更容易。
  • 应用Utf8.size()办法计算编码字符串的字节长度。这对于length-prefixed格局必不可少。
  • 应用Float.floatToIntBits()Double.doubleToLongBits()来编码浮点型的数值。

6、应用Socket进行通信

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

当你有一个文件对象,你只能够抉择读或者写,然而网络与之不同的是能够同时进行读和写!在有一些协定中,解决这个问题的形式是轮流的进行:写入申请、读取响应、反复以上操作。你能够用一个单线程来实现这种协定。而在其余协定中,你能够同时进行读写。通常你须要一个专门的线程来读取数据。对于写入数据,你能够应用专门线程或者应用synchronized,以便多个线程能够共享一个Sink。Okio的流在并发状况下应用是不平安的。

对于Okio的Sinks缓冲区,必须手动调用flush()来传输数据,以最小化I/O操作。通常,面向音讯的协定会在每条音讯之后刷新。留神,当缓冲数据超过某个阈值时,Okio将主动刷新。但这只是为了节俭内存,不能依赖它进行协定交互。

Okio是基于java.io.socket建设连贯的,当你通过socket创立服务器或客户端后,能够应用Okio.source(Socket)进行读取,应用Okio.sink(Socket)进行写入,这些API也同样实用于SSLSocket。

在任意线程中想要勾销socket连贯能够调用Socket.close()办法,这将导致sources 和 sinks 对象立刻抛出IOException而失败。Okio中能够为所有的socket操作配置超时限度,但并不需要你去调用Socket的办法来设置超时:Source和Sink会提供超时的接口,即便对流进行了装璜,此API依然无效。

Okio官网Demo中编写了一个 简略的Socket代理服务 来示例残缺的网络交互操作,上面是其中的局部代码截取:

private void handleSocket(final Socket fromSocket) {    try {        final BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket));        final BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket));        //..............        //..................    }  catch (IOException e) {        .....    }}

能够看到通过Socket创立sources 和 sinks的形式与通过文件创建的形式一样,都是先通过Okio.source()拿到Socket对应的Source或Sink对象,而后通过Okio.buffer()获取对应的装璜者缓冲对象。在Okio中,一旦你为Socket对象创立了Source 或者 Sink,那么你就不能再应用InputStream或OutputStream了。

Buffer buffer = new Buffer();for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) {  sink.write(buffer, byteCount);  sink.flush();}

以上代码中,循环从source中读取数据写入到sink当中,并调用flush()进行刷新,如果你不须要每次写数据都进行flush(),那么for循环里的两句能够应用BufferedSink.writeAll(Source)一行代码来代替。

你会发现,在read()办法中传递了一个8192作为读取的字节数,其实这里能够传任何数字,然而Okio更喜爱用8 kib,因为这是Okio在单个零碎调用中所能解决的最大值。大多数时候利用程序代码不须要解决这样的限度!

int addressType = fromSource.readByte() & 0xff;int port = fromSource.readShort() & 0xffff;

Okio应用的是有符号类型,如byteshort,但通常协定须要的是无符号的值,而在Java中将有符号的值转换为无符号值的首选形式,就是通过是按位与&运算符。以下是字节、短整型和整型的转换清单:

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中没有可能示意无符号的long型的根本类型。

7、哈希

哈希函数利用宽泛,如HTTPS证书、Git提交、BitTorrent完整性检查和区块链块等都应用到加密散列, 良好地应用哈希能够进步应用程序的性能、隐衷性、安全性和简略性。每个加密哈希函数承受一个可变长度的字节输出流,并生成一个长度固定的字符串值,称之为哈希值。哈希函数具备以下重要个性:

  • 确定性:每个输出总是产生雷同的输入。
  • 对立:每个输入的字节字符串的可能性雷同。很难找到或创立产生雷同输入的不同输出对。即“碰撞”。
  • 不可逆:晓得输入并不能帮忙你找到输出。
  • 易于了解:哈希在很多环境中都已被实现并且被严格了解。

Okio反对一些常见的哈希函数:

  • MD5:128位(16字节)加密哈希。它既不平安又是过期的,因为它的逆向老本很低!之所以提供此哈希,是因为它在安全性较低的零碎中应用比拟十分风行并且不便。
  • SHA-1:160位(20字节)加密散列。最近的钻研表明,创立SHA-1碰撞是可行的。思考从sha-1降级到sha-256。
  • SHA-256:256位(32字节)加密哈希。SHA-256被宽泛了解,逆向操作老本较高。这是大多数零碎应该应用的哈希。
  • SHA-512:512位(64字节)加密哈希。逆向操作老本很高。

Okio能够从ByteString中生成加密哈希:

ByteString byteString = readByteString(new File("README.md"));System.out.println("   md5: " + byteString.md5().hex());System.out.println("  sha1: " + byteString.sha1().hex());System.out.println("sha256: " + byteString.sha256().hex());System.out.println("sha512: " + byteString.sha512().hex());

从Buffer中生成:

Buffer buffer = readBuffer(new File("README.md"));System.out.println("   md5: " + buffer.md5().hex());System.out.println("  sha1: " + buffer.sha1().hex());System.out.println("sha256: " + buffer.sha256().hex());System.out.println("sha512: " + buffer.sha512().hex());

从Source输出流失去哈希值:

try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());     BufferedSource source = Okio.buffer(Okio.source(file))) {  source.readAll(hashingSink);  System.out.println("sha256: " + hashingSink.hash().hex());}

从Sink输入流失去哈希值:

try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());     BufferedSink sink = Okio.buffer(hashingSink);     Source source = Okio.source(file)) {  sink.writeAll(source);  sink.close(); // Emit anything buffered.  System.out.println("sha256: " + hashingSink.hash().hex());}

Okio还反对HMAC(哈希音讯认证代码),它联合了一个秘钥值和一个hash值。应用程序能够应用HMAC进行数据完整性和身份验证:

ByteString secret = ByteString.decodeHex("7065616e7574627574746572");System.out.println("hmacSha256: " + byteString.hmacSha256(secret).hex());

同样样,你能够从ByteString, Buffer, HashingSource, 和HashingSink生成HMAC。留神,Okio没有为MD5实现HMAC。Okio应用Java的java.security.MessageDigest用于加密散列和javax.crypto.Mac 生成HMAC。

8、加密和解密

应用Okio.cipherSink(Sink,Cipher)或Okio.cipherSource(Source,Cipher)应用区块加密算法对Stream进行加密或解密。调用者负责应用算法,密钥和特定于算法的附加参数(如初始化向量)初始化加密或解密明码。 以下示例显示了AES加密的典型用法,其中key和iv参数都应为16个字节长度:

void encryptAes(ByteString bytes, File file, byte[] key, byte[] iv)    throws GeneralSecurityException, IOException {  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));  try (BufferedSink sink = Okio.buffer(Okio.cipherSink(Okio.sink(file), cipher))) {    sink.write(bytes);  }}ByteString decryptAesToByteString(File file, byte[] key, byte[] iv)    throws GeneralSecurityException, IOException {  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");  cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));  try (BufferedSource source = Okio.buffer(Okio.cipherSource(Okio.source(file), cipher))) {    return source.readByteString();  }}

以上就是对OKio官网文档的局部翻译,英文比拟好的话能够参考官网文档:《Okio Reference》,对于Okio的具体实现细节等到前面的源码剖析文章再详谈。