前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Okio原理解析

Okio原理解析

作者头像
全栈程序员站长
发布2022-08-25 15:57:47
3190
发布2022-08-25 15:57:47
举报
文章被收录于专栏:全栈程序员必看

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

随着越来越多的应用使用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 的使用示例:

代码语言:javascript
复制
//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。

相关接口代码如下:

代码语言:javascript
复制
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。

代码语言:javascript
复制
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相同)

代码语言:javascript
复制
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[]数据整体说明如下:

所以Segment中数据量计算方式为:limit-pos

八、SegmentPool解析

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

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

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一次写出去就可以了,这样是不是就高效多了,我们不用一点点的写出去,先分割然后一次性写出去显然效率高很多。

代码语言:javascript
复制
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;
   }
 }
}

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、OKIO的介绍:
  • 二、OKIO 的使用示例:
    • 三、Okio 的Source + Sink:
    • 四、BufferedSource与BufferedSink
      • 五、 RealBufferedSink 和 RealBufferedSource
      • 六、Buffer
      • 七、Segment
      • 八、SegmentPool解析
      • 总结:
      相关产品与服务
      对象存储
      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档