Okio原理解析

Okio原理解析随着越来越多的应用使用OKHttp来进行网络访问,我们有必要去深入研究OKHTTP的基石,一套更加轻巧方便高效的IO库okio。一、OKIO的介绍:okio是大名鼎鼎的square公司开发出来的,其是okhttp的底层io操作库。其相对于原生的JavaIO读写,更具有(1)紧凑的封装是对JavaIO/NIO的封装使用,支持文件读写,也支持Socket通信的读写,不需要再套上一系列的装饰类;(2)使用简单不用区分字符流或者字节流,也不用记住各种不同的输入/输出流,统统只有一个输入

大家好,又见面了,我是你们的朋友全栈君。

随着越来越多的应用使用OKHttp来进行网络访问,我们有必要去深入研究OKHTTP的基石,一套更加轻巧方便高效的IO库okio。

一、OKIO的介绍:

okio是大名鼎鼎的square公司开发出来的,其是okhttp的底层io操作库。其相对于原生的Java IO 读写,更具有

(1)紧凑的封装 是对Java IO/NIO 的封装使用,支持文件读写,也支持Socket通信的读写,不需要再套上一系列的装饰类;

(2) 使用简单 不用区分字符流或者字节流,也不用记住各种不同的输入/输出流,统统只有一个输入流Source和一个输出流Sink;

(3)API丰富 其封装了大量的API接口用于读/写字节或者一行文本,还有如GZip的透明处理,对数据计算md5、sha1等都提供了支持,对数据校验非常方便;

(4)读写速度快 采用了segment的机制进行内存共享和复用,尽可能少的去申请内存,使I/O在缓冲区得到更高的复用处理,从而尽量减少I/O的GC的性能问题。

二、OKIO 的使用示例:

//okio 向文件中写文件
public static void writeTest(File file) {
   try {
        Sink sink = Okio.sink(file);
        BufferedSink bufferedSink = Okio.buffer(sink);
        bufferedSink.writeString("Hello okio!", Charset.forName("UTF-8"));
        bufferedSink.writeInt(998);
        bufferedSink.writeByte(1);
        bufferedSink.writeLong(System.currentTimeMillis());
        bufferedSink.writeUtf8("Hello end!");
        bufferedSink.close();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

//okio 读文件
public static void readTest(File file) {
   try {
        
        Source source = Okio.source(file);
        BufferedSource bufferedSource = Okio.buffer(source);
        String string = bufferedSource.readString("Hello okio!".length(), Charset.forName("UTF-8"));
        int intValue = bufferedSource.readInt();
        byte byteValue = bufferedSource.readByte();
        long longValue = bufferedSource.readLong();
        String utf8 = bufferedSource.readUtf8();
        source.close();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

三、Okio 的Source + Sink:

Okio之所以轻量,它的代码非常清晰。最重要的两个接口分别是Source和Sink

Source与Sink是Okio中的输入流接口和输出流接口,对应原生IO的InputStream和OutputStream。

这里写图片描述

相关接口代码如下:

public interface Source extends Closeable {

  //读取数据的接口方法,它的第一个参数是Buffer,相当于缓冲区。byteCount就是读取的字节数
  long read(Buffer sink, long byteCount) throws IOException;

  //Okio新增的新特性,超时控制
  Timeout timeout();

  //关闭输入输出流
  @Override void close() throws IOException;
}


public interface Sink extends Closeable, Flushable {

  //写入数据的接口方法,它的第一个参数是Buffer,相当于缓冲区。byteCount就是写入的字节数
  void write(Buffer source, long byteCount) throws IOException;

  //将Buffer缓冲区中的数据写入目标流中
  @Override void flush() throws IOException;

  //Okio新增的新特性,超时控制
  Timeout timeout();

  //关闭输入输出流
  @Override void close() throws IOException;
}

四、BufferedSource与BufferedSink

BufferedSource与BufferedSink同样是两个接口类,分别继承Source与Sink接口,BufferedSource与BufferedSink是具有缓存功能的接口,各自维护了一个buffer,同时提供了很多实用的api调用接口,平时我们使用也主要是调用这两个类中定义的方法。

五、 RealBufferedSink 和 RealBufferedSource

上面提到的都是接口类,具体的实现类分别是RealBufferedSink和 RealBufferedSource ,其实这两个类也不算具体实现类,只是Buffer类的代理类,具体功能都在Buffer类里面实现的。

六、Buffer

我们知道Buffer作为缓冲区,肯定底层需要有数据结构来存储暂存的数据,JDK的BuffedInputStream和BufferedOutputStream中是使用字节数组的,而这里Okio的Buffer不是,Buffer类内部维护了一个Segment构成的双向循环链表,okio将缓存切成一个个很小的片段,每个片段就是Segment,我们写数据或者读数据都是操作的Segment中维护的一个个数组,而SegmentPool维护被回收的Segment,这样创建Segment的时候从SegmentPool取就可以了,有缓存直接用缓存的,没有再新创建Segment。

public final class Buffer implements BufferedSource, BufferedSink, Cloneable {
 。。。。
 Segment head;
 long size;

 public Buffer() {
 }

 @Override public Buffer buffer() {
   return this;
 }

。。。。。

 /** Write {@code byteCount} bytes from this to {@code out}. */
 public Buffer writeTo(OutputStream out, long byteCount) throws IOException {
   if (out == null) throw new IllegalArgumentException("out == null");
   checkOffsetAndCount(size, 0, byteCount);

   Segment s = head;
   while (byteCount > 0) {
     int toCopy = (int) Math.min(byteCount, s.limit - s.pos);
     out.write(s.data, s.pos, toCopy);

     s.pos += toCopy;
     size -= toCopy;
     byteCount -= toCopy;

     if (s.pos == s.limit) {
       Segment toRecycle = s;
       head = s = toRecycle.pop();
       SegmentPool.recycle(toRecycle);
     }
   }
   return this;
 }
}

七、Segment

Segment 是一个双向循环链表,它的内部持有一个byte[] data,默认大小8192(与JDK的BufferedInputStream相同)

public final class Segment {
  /** The size of all segments in bytes. */
  static final int SIZE = 8192;

  /** 默认共享最小字节数*/
  static final int SHARE_MINIMUM = 1024;

  final byte[] data;

  /** 标识下一个读取字节的位置 */
  int pos;

  /** 标识下一个写入字节的位置 */
  int limit;

  /** 是否与其他Segment共享byte[] */
  boolean shared;

  /** 是否拥有这个byte[], 如果拥有可以写入 */
  boolean owner;

  /** Segment后继 */
  public Segment next;

  /** Segment前驱 */
  Segment prev;
  Segment() {
    this.data = new byte[SIZE];
    this.owner = true;
    this.shared = false;
  }

  ......
}

分享数据相关的字段先放一下,后面会详细说明,这里要明白pos与limit含义,Segment中data[]数据整体说明如下:

Okio原理解析
所以Segment中数据量计算方式为:limit-pos

八、SegmentPool解析

接下来我们看下SegmentPool,也就是Segment的缓存池,SegmentPool内部维持一条单链表保存被回收的Segment,缓存池的大小限制为64KB,每个Segment大小最大为8KB,所以SegmentPool最多存储8个Segment。

SegmentPool存储结构为单向链表,结构如图:

Okio原理解析
SegmentPool源码解析:

总结:

有了Segment和SegmentPool的知识,就更容易理解Buffer类的实现了。

write(Buffer source, long byteCount)描述了将一个Buffer数据写入另一个Buffer中的核心逻辑,Buffer之间数据的转移就是将一个Buffer从头部数据开始写入另一个Buffer的尾部,但是上述有个特别精巧的构思:如果目标Segment能够容纳下要写入的数据则直接采用数组拷贝的方式,如果容纳不下则先split拆分source头结点Segment,然后整段移动到目标Buffer链表尾部,注意这里是移动也就是操作指针而不是数组拷贝,这样就非常高效了,而不是一味地数组拷贝方式转移数据,okio将数据分割成一小段一小段并且用链表连接起来也是为了这样的操作来转移数据,对数据的操作更加灵活高效。

解释一下:有时候我们需要将source buffer缓冲区数据部分写入sink buffer缓冲区,比如,sink buffer缓冲区数据状态为 [51%, 91%],source buffer缓冲区数据状态为[92%, 82%] ,我们只想写30%的数据到sink buffer缓冲区,这时我们首先将source buffer中的92%容量的Segment分割为30%与62%,然后将30%的Segment一次写出去就可以了,这样是不是就高效多了,我们不用一点点的写出去,先分割然后一次性写出去显然效率高很多。

public final class Buffer implements BufferedSource, BufferedSink, Cloneable {

 Segment head;//Buffer类中双向循环链表的头结点
 long size;//Buffer中存储的数据大小

 public Buffer() {
 }

 。。。。。。
 //将传入的source Buffer中byteCount数量数据写入调用此方法的Buffer中
 @Override public void write(Buffer source, long byteCount) {
   //从源缓冲区的头部移动字节到该缓冲区的尾部,同时平衡两个相互冲突的目标:
   //不要浪费CPU和不要浪费内存。
   //不要浪费CPU(例如。不要到处复制数据)。复制大量的数据是昂贵的。
   //相反,我们更愿意重新分配整个段从一个缓冲区到另一个。
   //不要浪费内存。作为一个不变变量,缓冲区中相邻的段对应该是在至少50%满,除了头段和尾段。
   //头段不能保持不变,因为应用程序是从这个段中消耗字节数,降低它的级别。
   //尾段不能保持不变,因为应用程序生成字节,这可能需要新的几乎为空的尾段

   //附加。
   //在缓冲区之间移动段:当一个缓冲区写入另一个缓冲区时,我们倾向于重新分配整个段
   //假设我们有一个缓冲器这些分段水平[91%,61%]。
   //如果我们在缓冲区后面加上单一[72%]板块,即收益率[91%,61%,72%]。不复制任何字节。
   //或者假设我们有一个具有以下分段级别的缓冲区:[100%,2%],并且我们
   //想要将它附加到具有这些段级别的缓冲区中。这将产生以下部分:[100%,2%,99%,3%]。
   //这时,我们不花时间复制字节来实现更高效,内存使用像[100%,100%,4%]。
   //当合并缓冲区时,我们将压缩相邻的缓冲区组合等级不超过100%。
   //例如,当我们开始(100%、40%)和附加(30%、80%),结果(100%、70%、80%)。
   // 
   //分割段
   //
   //有时我们只把源缓冲区的一部分写入接收器缓冲区。
   //例如,给定一个接收器[51%,91%],我们可能想要写的前30%
   //一个来源[92%,82%]。为了简化,我们首先将源转换为一个等效的缓冲区[30%,62%,82%]然后移动头 
   //段,最后将是为[51%,91%,30%]和来源[62%,82%]。

   if (source == null) throw new IllegalArgumentException("source == null");
   if (source == this) throw new IllegalArgumentException("source == this");
   checkOffsetAndCount(source.size, 0, byteCount);

   while (byteCount > 0) {
     // Is a prefix of the source's head segment all that we need to move?
     //要写的数据量byteCount 小于source 头结点的数据量,也就是链表第一个Segment包含的数据量大于byteCount
     if (byteCount < (source.head.limit - source.head.pos)) {
       //获取链表尾部的结点Segment
       Segment tail = head != null ? head.prev : null;
       //尾部结点Segment可写并且能够盛放byteCount 数据
       if (tail != null && tail.owner
           && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
         // Our existing segments are sufficient. Move bytes from source's head to our tail.
         //直接写入尾部结点Segment即可,Segment的writeTo方法上面已经分析
         source.head.writeTo(tail, (int) byteCount);
         //改变缓存Buffer中数据量
         source.size -= byteCount;
         size += byteCount;
         return;
       } else {
         // We're going to need another segment. Split the source's head
         // segment in two, then move the first of those two to this buffer.
         //尾部Segment不能盛放下byteCount数量数据,那就将source中头结点Segment进行分割,split方法上面已经分析过
         source.head = source.head.split((int) byteCount);
       }
     }

     // Remove the source's head segment and append it to our tail.
     //获取source中的头结点
     Segment segmentToMove = source.head;
     long movedByteCount = segmentToMove.limit - segmentToMove.pos;
     //将头结点segmentToMove从原链表中弹出
     source.head = segmentToMove.pop();
     //检查要加入的链表头结点head是否为null
     if (head == null) {//head为null情况下插入链表
       head = segmentToMove;
       head.next = head.prev = head;
     } else {//head不为null
       Segment tail = head.prev;
       //将segmentToMove插入新的链表中
       tail = tail.push(segmentToMove);
       //掉用compact尝试压缩
       tail.compact();
     }
     source.size -= movedByteCount;
     size += movedByteCount;
     byteCount -= movedByteCount;
   }
 }
}

 

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/142825.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号