学习Okio的优化思想

目录

  1. 简介
  2. Okio简单demo
  3. 核心类
    1. 结论
  4. Buffer详解
    1. 两个优化小细节
    2. 两个疑问
  5. 其他优化
    1. ByteString
    2. Timeout
  6. 总结
  7. 参考

简介

Okio是由Square公司开发并开源的java IO组件库,最初用于其开发的HTTP客户端OkHttp的底层IO封装,从Android 4.4起,HttpURLConnection底层实现采用Okhttp,Okio组件也被开发者所认可。根据GitHub介绍,该组件定位是:

Okio is a new library that complements java.io and java.nio to make it much easier to access, store, and process your data.

我们主要是通过源码分析学习其优化的思想。

Okio简单demo

1
2
3
4
5
6
7
8
9
10
// inputStream  : 输入
// outputStream : 输出
try (BufferedSource source = Okio.buffer(Okio.source(inputStream));
BufferedSink sink = Okio.buffer(HashingSink.md5(Okio.sink(outputStream)))
) {
sink.writeAll(source);
sink.flush();
} catch (IOException e) {
e.printStackTrace();
}

核心类

结论

  1. Source/Sink类似于Java的InputStreamOutputStream,且没有区分字节流和字符流,类图直观感觉是比Java IO简洁

  2. Java IO使用装饰器模式,导致各种IO类比较臃肿,Okio将常用方法封装在BufferedSource/BufferedSink接口中,把底层字节流直接加工成需要的数据类型,摒弃Java IO中各种输入流和输出流的嵌套模式;但Java IO装饰器模式优点是根据各种情况可以选择最优方式,Okio只是针对大多数场景进行封装,使用方便。

  3. 以输入流Source为例,数据流程如下:

    (1) Okio#source(InputStream)最终会调用Okio#(InputStream, Timeout),实现了InputStream –> Buffer

    (2) Okio#buffer(Source)会创建RealBufferedSource对象,该对象包括(1)中构建的source和一个Buffer,该Buffer就是(1)中Buffer

    (3) 数据读取最终是Buffer对象的读取,也就是说先把InputStream数据放入Buffer对象,然后从Buffer对象读取;如果数据已经预先在Buffer中,则减少一次IO

    (4) Buffer好处是:Buffer对象是以数据块Segment(下面会分析)从InputStream读取数据的,相比单个字节读取来说,效率会有一定提高,一种空间换时间的策略

  4. 扩展性: Okio实现了透明Gzip和Hash编码的IO,以输入流为例,读取Gzip输入流只需如下构建:

    1
    BufferedSource source = Okio.buffer(new GzipSource(Okio.source(inputStream)));

通过装饰器模式实现,当前只实现了Gzip和常用Hash,因其本身是作为HTTP客户端的IO组件,支持这些常用编码。其对应输入流类图如下:

如果有新编码需求,可继承Source进行扩展,总之,装饰器模式使其扩展性还是不错的

Buffer详解

Buffer维护Segment组成的双向链表,Segment存储底层数据(byte数组);提供SegmentPool用于快速分配和回收Segment节点,每个Segment底层byte数组大小为8192,而SegmentPool暂设定为8个Segment。每个Segment的pos表示可读byte坐标,limit表示可写byte坐标,系统维护这两个变量。使用Segment维护底层数据的好处:

  1. 一定程度上提高IO吞吐量

  2. 如果两个线程之间数据移动时,只需移动对应Segment和少量数据copy即可

考虑Okio最初是作为HTTP客户端底层的IO封装,而HTTP客户端不同线程间数据移动是非常常见的,所以这部分优化对HTTP客户端整体性能提升是比较明显的。

两个优化小细节

目标是:尽量保证Segment有效数据量多且占用个数少,同时减少底层字节数组copy。

两个疑问

SegmentPool会减少内存的多次分配,个人感觉当前设定8个Segment会不会浪费资源;当前单个Segment大小选择8KB,如果选择linux默认页大小(4KB)会不会更好一点?

其他优化

ByteString

本质是一个持有byte数组和对应utf8编码String的类,提供字符串常见操作和各种编码方法(hex,base64等),缓存utf-8字符串可有效提高utf-8编解码效率,更适用于网络传输。

Timeout

Java原生IO并没有提供超时机制,尤其向Socket写数据时由于网络问题会长时间阻塞,所以Okio针对IO本身提供超时机制,主要提供两种机制:同步和异步机制,分别由Timeout和AsyncTimeout实现,示意图如下:


提供超时机制是对Java IO进行补充,尤其异步超时机制对于Socket编程非常友好

总结

Okio组件是针对HTTP客户端场景对Java I/O进行的优化,使用更加便捷,app中一般IO操作也可使用,尤其是其提供很便捷的加解密、压缩解压效果。

参考

  1. https://github.com/square/okio