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;
   }
 }
}

 

 

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

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

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 百度快照更新是什么意思啊_百度快照和百度推广的区别

    百度快照更新是什么意思啊_百度快照和百度推广的区别百度快照更新是什么意思?    最近发现有很多刚入SEO行业的新手对网站seo的技巧有很多的误区,比如网站快照不更新就代表网站被惩罚。关于这个观点我们先看看什么是百度快照?百度快照的作用是什么?我们有该如何让百度快照持续更新呢?        一、百度快照是什么?    快照即为WebCache,可以翻译为网页缓存,当搜索引擎派出蜘蛛去对网站进行索

    2022年9月28日
    0
  • centos解压命令

    centos解压命令-c:建立压缩档案-x:解压-t:查看内容-r:向压缩归档文件末尾追加文件-u:更新原压缩包中的文件这五个是独立的命令,压缩解压都要用到其中一个,可以和别的命令连用但只能用其中一个。下面的参数是根据需要在压缩或解压档案时可选的:-z:有gzip属性的-j:有bz2属性的-Z:有compress属性的-v:显示所有过程-O:将文件解开到标准输出参数-f是必须的-f:使用档案名…

    2022年5月16日
    30
  • spring中已经内置的几种事件

    spring中已经内置的几种事件

    2021年9月6日
    49
  • java测试类的创建方法_java编写一个类

    java测试类的创建方法_java编写一个类JUnit基础及第一个单元测试实例(JUnit3.8)JUnit基础及第一个单元测试实例(JUnit3.8) 单元测试  单元测试(unittesting) ,是指对软件中的最小可测试单元进行检查和验证。  单元测试不是为了证明您是对的,而是为了证明您没有错误。  单元测试主要是用来判断程序的执行结果与自己期望的结果是否一致。  关键是

    2022年10月17日
    0
  • 企业竞争分析的几种方法:SWOT、波特五力、PEST「建议收藏」

    企业竞争分析的几种方法:SWOT、波特五力、PEST「建议收藏」最近实验室要申报一个互联网+的项目,项目中有关企业经营部分的内容着实令我们这些工科生无从下手,在咨询了某专业相关的学妹后稍微有了点头绪(此处手动感谢学妹的协助哈哈哈~),这里就把学到的有关竞争分析的方法总结一下~目录1、波特五力分析模型2、SWOT分析法3、PEST分析法1、波特五力分析模型波特五力分析模型是迈克尔·波特(MichaelPorter)与20世纪80年代提出的一种用于竞争战略的分析模型。顾名思义就是波特提出一个行业中的竞争,不只是在原有竞争对手中进行,而是存.

    2022年6月10日
    44
  • 博客营销BlogUp

    博客营销BlogUp九丁博客群发工具BlogUp是一款强大的博客营销工具,具有博客全自动群发、博客帐号辅助群建、帐号分组管理、博客文章可视化管理、文章伪原创、超链接自动插入、文章自动采集、关键词设置、标签设置、自动更换IP等核心功能。是商家、站长、写手、个人、公司等用于网络营销、软文推广、博客写作、网络推广、SEO的绝佳工具。利用BlogUp可以帮您增加搜索引擎信息收录量,提高搜索引擎排名,快速提高产品、网站、文章等…

    2022年7月14日
    14

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

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