okio篇 1.Buffer

news2025/1/18 6:42:27

总览:

Okio的两个基本概念:Source和Sink。Source对标基础io库中的InputStream,负责读数据。Sink对标OutputStream,负责写数据。

Source和Sink的内部实现,都是一个Buffer。Buffer从字面意思理解就是一个缓冲区,跟BufferInputStream的概念比较类似。缓冲区的作用,就是可以减少调用系统api的次数,等缓冲区装满数据之后,再一次性调用系统api接口。

而Buffer的底层实现是Segment链表。Segment指的是块的意思,每一个Segment内部都是一个固定的8k的字节数组。

以上,都似乎跟基础io库没有很大区别。但okio的高效就在于,将数据,分成了一个个Segment。

当流之间需要传输数据时,只需要以Segment为单位,进行数据的转移,即将一个Segment转移到另一个Buffer的Segment链表上去。跟传统的流传输数据,每一次传输都需要进行一次数据拷贝,明显链表的拆分会显得更高效。

所以这篇Okio的开篇,会讲述Okio的基本结构,Segment和Buffer。

Segment

Segment内部是一个8K的字节数组 byte[] data。

shared表明是否跟另一个Segment共享data,指的是,两个Segment指向同一个data数组。这时候,第一个Segment,owner=true,shared =false。

而通过sharedCopy获取到的Segment,owner=false,shared=true。并且本身的segment也变成了true。

同时所有的segment都身处一个linkedList当中,记载自己的prev和successor。

limit指的是可写的位置。

pos是下一个将要读的位置。

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

  /** Segments will be shared when doing so avoids {@code arraycopy()} of this many bytes. */
  // split操作会用到。当split字节小于这个值,会直接使用arraycopy。否则从segmentpool,获取一个segment
  // 进行sharedCopy操作。
  static final int SHARE_MINIMUM = 1024;

  final byte[] data;

  /** The next byte of application data byte to read in this segment. */
  int pos;

  /** The first byte of available data ready to be written to. */
  int limit;

  /** True if other segments or byte strings use the same byte array. */
  boolean shared;

  /** True if this segment owns the byte array and can append to it, extending {@code limit}. */
  // 只有owner===true时,才能对data进行操作-->为什么呢?
  boolean owner;

  /** Next segment in a linked or circularly-linked list. */
  Segment next;

  /** Previous segment in a circularly-linked list. */
  Segment prev;

  Segment() {
    this.data = new byte[SIZE];
    this.owner = true;
    this.shared = false;
  }

  Segment(byte[] data, int pos, int limit, boolean shared, boolean owner) {
    this.data = data;
    this.pos = pos;
    this.limit = limit;
    this.shared = shared;
    this.owner = owner;
    }
}
Segment sharedCopy() {
  shared = true;// 本身shared也变成true
  return new Segment(data, pos, limit, true, false);// 两个Segment指向data
}

/** Returns a new segment that its own private copy of the underlying byte array. */
Segment unsharedCopy() {
  return new Segment(data.clone(), pos, limit, false, true);// 只是数据的拷贝,data.clone得到的数组是一个内容相同,但地址不同的数组
}
/**
 * Splits this head of a circularly-linked list into two segments. The first
 * segment contains the data in {@code [pos..pos+byteCount)}. The second
 * segment contains the data in {@code [pos+byteCount..limit)}. This can be
 * useful when moving partial segments from one buffer to another.
 *
 * <p>Returns the new head of the circularly-linked list.
 */
public Segment split(int byteCount) {
  if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
  Segment prefix;

  // We have two competing performance goals:
  //  - Avoid copying data. We accomplish this by sharing segments.
  //  - Avoid short shared segments. These are bad for performance because they are readonly and
  //    may lead to long chains of short segments.
  // To balance these goals we only share segments when the copy will be large.
  if (byteCount >= SHARE_MINIMUM) {
    prefix = sharedCopy();
  } else {
    prefix = SegmentPool.take();
    System.arraycopy(data, pos, prefix.data, 0, byteCount);
  }

  prefix.limit = prefix.pos + byteCount;
  pos += byteCount;// this segment的pos增加,表示pos+byteCount之前的数据都不可读了
  prev.push(prefix);// 本来是,prev->this->next,push之后变成prev->prefix->this->next
  return prefix;
}

// 相当于整合,将this segment整合到prev segment
public void compact() {
  if (prev == this) throw new IllegalStateException();
  if (!prev.owner) return; // Cannot compact: prev isn't writable.--》只有是owner,才能往data追加数据
  int byteCount = limit - pos;// this segment剩余未读字节
  // 如果prev是shared,那剩余的空间就是SIZE - (prev.limit-0) ,因为前面的字节有可能被split到其他segment
  // 否则剩余空间就应该是 SIZE - (prev.limit-prev.pos)
  int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);// prev segment剩余空间
  if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
  writeTo(prev, byteCount);
  pop();
  SegmentPool.recycle(this);
}

/** Moves {@code byteCount} bytes from this segment to {@code sink}. */
public void writeTo(Segment sink, int byteCount) {
  if (!sink.owner) throw new IllegalArgumentException();
  if (sink.limit + byteCount > SIZE) {// 往前整合数据
    // We can't fit byteCount bytes at the sink's current position. Shift sink first.
    if (sink.shared) throw new IllegalArgumentException();
    if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
    System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
    sink.limit -= sink.pos;
    sink.pos = 0;
  }

  System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
  sink.limit += byteCount;
  pos += byteCount;
}

compat方法总的来说就是将当前segment挪到prev segment的剩余部分当中

tail整合的规则,当调用tail.compat:

Buffer

buffer.read方法

// 读取所有数据
@Override public byte[] readByteArray() {
  try {
    return readByteArray(size);
  } catch (EOFException e) {
    throw new AssertionError(e);
  }
}

@Override public byte[] readByteArray(long byteCount) throws EOFException {
  checkOffsetAndCount(size, 0, byteCount);
  if (byteCount > Integer.MAX_VALUE) {
    throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
  }

  byte[] result = new byte[(int) byteCount];
  readFully(result);
  return result;
}

@Override public void readFully(byte[] sink) throws EOFException {
  int offset = 0;
  while (offset < sink.length) {
    int read = read(sink, offset, sink.length - offset);// 不断循环从segment中读取数据
    if (read == -1) throw new EOFException();
    offset += read;
  }
}
// 重点都在这个方法里
@Override public int read(byte[] sink, int offset, int byteCount) {
  checkOffsetAndCount(sink.length, offset, byteCount);

  Segment s = head;
  if (s == null) return -1;// 代表这个buffer里面已经没有数据了
  int toCopy = Math.min(byteCount, s.limit - s.pos);// 获取这个segment中所能获取到的字节(s.limit-s.pos)
  System.arraycopy(s.data, s.pos, sink, offset, toCopy);// 进行数组间的复制

  s.pos += toCopy;
  size -= toCopy;// 这个buffer中的可读数据的大小,称为size

  if (s.pos == s.limit) {// 如果这个segment已经读完
    head = s.pop();// 将这个segment从buffer移除,放入segmentPool,并且将head指向下一个segment
    SegmentPool.recycle(s);
  }

  return toCopy;
}

写入到另一个ByteBuffer-->实际也是数组

@Override public int read(ByteBuffer sink) throws IOException {
  Segment s = head;
  if (s == null) return -1;

  int toCopy = Math.min(sink.remaining(), s.limit - s.pos);
  sink.put(s.data, s.pos, toCopy);

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

  if (s.pos == s.limit) {
    head = s.pop();
    SegmentPool.recycle(s);
  }

  return toCopy;
}

buffer.clear

实际是,相当于移动指针。并且回收segment

public void clear() {
  try {
    skip(size);
  } catch (EOFException e) {
    throw new AssertionError(e);
  }
}

/** Discards {@code byteCount} bytes from the head of this buffer. */
@Override public void skip(long byteCount) throws EOFException {
  while (byteCount > 0) {
    if (head == null) throw new EOFException();

    int toSkip = (int) Math.min(byteCount, head.limit - head.pos);
    size -= toSkip;
    byteCount -= toSkip;
    head.pos += toSkip;

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

关于Buffer的写数据

最基础的写方法,将数组写入buffer(即自己)

将source[offset,offset+byteCount]写入到buffer

@Override public Buffer write(byte[] source, int offset, int byteCount) {
  if (source == null) throw new IllegalArgumentException("source == null");
  checkOffsetAndCount(source.length, offset, byteCount);

  int limit = offset + byteCount;
  while (offset < limit) {
    Segment tail = writableSegment(1);// 获取一个可写入的segment

    int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);// 获取这次可写入segment的字节数
    System.arraycopy(source, offset, tail.data, tail.limit, toCopy);

    offset += toCopy;
    tail.limit += toCopy;
  }

  size += byteCount;
  return this;
}
// 写入数据,默认追加到segment的队尾,双端队列这时候就起作用了
// 通过head.prev很容易获取到segment的对尾
Segment writableSegment(int minimumCapacity) {
  if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();

  if (head == null) {
    head = SegmentPool.take(); // Acquire a first segment.
    return head.next = head.prev = head;
  }

  Segment tail = head.prev;
  if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {// 看tail是不是已经写不进去了
    tail = tail.push(SegmentPool.take()); // Append a new empty segment to fill up.// 就从segmentPool里面获取一个segment
  }
  return tail;
}

基于segment.split方法的写数据

@Override public void write(Buffer source, long byteCount) {
  // Move bytes from the head of the source buffer to the tail of this buffer
  // while balancing two conflicting goals: don't waste CPU and don't waste
  // memory.
  //
  //
  // Don't waste CPU (ie. don't copy data around).
  //
  // Copying large amounts of data is expensive. Instead, we prefer to
  // reassign entire segments from one buffer to the other.
  //
  //
  // Don't waste memory.
  //
  // As an invariant, adjacent pairs of segments in a buffer should be at
  // least 50% full, except for the head segment and the tail segment.
  //
  // The head segment cannot maintain the invariant because the application is
  // consuming bytes from this segment, decreasing its level.
  //
  // The tail segment cannot maintain the invariant because the application is
  // producing bytes, which may require new nearly-empty tail segments to be
  // appended.
  //
  //
  // Moving segments between buffers
  //
  // When writing one buffer to another, we prefer to reassign entire segments
  // over copying bytes into their most compact form. Suppose we have a buffer
  // with these segment levels [91%, 61%]. If we append a buffer with a
  // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied.
  //
  // Or suppose we have a buffer with these segment levels: [100%, 2%], and we
  // want to append it to a buffer with these segment levels [99%, 3%]. This
  // operation will yield the following segments: [100%, 2%, 99%, 3%]. That
  // is, we do not spend time copying bytes around to achieve more efficient
  // memory use like [100%, 100%, 4%].
  //
  // When combining buffers, we will compact adjacent buffers when their
  // combined level doesn't exceed 100%. For example, when we start with
  // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%].
  //
  //
  // Splitting segments
  //
  // Occasionally we write only part of a source buffer to a sink buffer. For
  // example, given a sink [51%, 91%], we may want to write the first 30% of
  // a source [92%, 82%] to it. To simplify, we first transform the source to
  // an equivalent buffer [30%, 62%, 82%] and then move the head segment,
  // yielding sink [51%, 91%, 30%] and source [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) {// 从source写byteCount个数据到this buffer
    // Is a prefix of the source's head segment all that we need to move?
    if (byteCount < (source.head.limit - source.head.pos)) {// 如果source.head已经有byteCount个字节的数据
      Segment tail = head != null ? head.prev : null;
      if (tail != null && tail.owner
          && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
          // 如果tail可写入,且tail的空间足够容纳byteCount个数据
        // Our existing segments are sufficient. Move bytes from source's head to our tail.
        source.head.writeTo(tail, (int) byteCount);
        source.size -= byteCount;
        size += byteCount;
        return;
      } else {// tail不可写入,或tail没有足够的空间容纳数据
        // 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.
        // split将source.head分成byteCount->source.head-byteCount
        // 这里返回的source.head是含有byteCount个数据的segment
        source.head = source.head.split((int) byteCount);
      }
    }

    // Remove the source's head segment and append it to our tail.
    Segment segmentToMove = source.head;
    long movedByteCount = segmentToMove.limit - segmentToMove.pos;
    source.head = segmentToMove.pop();
    if (head == null) {
      head = segmentToMove;
      head.next = head.prev = head;
    } else {
      Segment tail = head.prev;
      tail = tail.push(segmentToMove);// 在tail后面追加segmentToMove,并且将tail设置为segmentToMove
      tail.compact();// 对数据进行整合,将tail的数据放到prev里
    }
    source.size -= movedByteCount;
    size += movedByteCount;
    byteCount -= movedByteCount;
  }
}

总结下buffer之间转移数据的过程,这也是okio最重要的优点:

Stream读写数据与Okio读写数据的比较

public void readFileUseStream() {
    BufferedReader fileReader = null;
    try {
        fileReader = new BufferedReader(new FileReader(fileName));
        String content;
        while ((content = fileReader.readLine()) != null) {
            System.out.println("readFileUseStream line:"+content);
        }
        fileReader.close();
    } catch (EOFException e) {
        throw new RuntimeException(e);
    } catch (FileNotFoundException e) {
        throw new RuntimeException(e);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

public void readFileUseOkIo() {
    try {
        BufferedSource source = Okio.buffer(Okio.source(new File(fileName)));
        String content;
        while ((content = source.readUtf8Line()) != null) {
            System.out.println("readFileUseOkIo content:"+content);
        }
        source.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

可以看出

1.数据流读写数据,需要用到多个Reader类,而Okio就只有Source的概念。

2.okio在数据流之间传输数据显得更为高效,因为okio在流之间传输数据只需要移动Buffer,而流之间传输数据,每一次传输,都需要进行一次数据的拷贝。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/443351.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

『pyqt5 从0基础开始项目实战』12. 实现多线程循环检测数据的开始和停止(保姆级图文)

目录 最终效果导包和框架代码 main.py避免重复执行开始与停止事件表格更新事件 scheduler.py初始化开始线程停止线程在线程列表中删除线程TaskThread新建线程StopThread停止线程完整代码main.pythreads.pyscheduler.py 总结 欢迎关注 『pyqt5 从0基础开始项目实战』 专栏&#…

C. Anadi and Domino(思维 + 枚举)

Problem - C - Codeforces 阿纳迪有一副多米诺骨牌。每张多米诺骨牌都有两个部分&#xff0c;每个部分都包含一些小点。对于每一个a和b&#xff0c;如1≤a≤b≤6&#xff0c;1≤a≤b≤6&#xff0c;正好有一块多米诺骨牌的一半是a点&#xff0c;另一半是b点。这组多米诺骨牌正好…

手动实现 Tomcat 底层机制+ 自己设Servlet

目录 手动实现 Tomcat 底层机制 自己设Servlet 完成小案例 运行效果 此项目用maven至于怎么配置在下一篇文章 创建cal.html CalServlet.java web.xml WebUtils 问题: Tomcat 整体架构分析 测试分析&#xff1a; 抓包情况 手动实现 Tomcat 底层机制 自己设计 Serv…

十个高质量工具网站推荐,AI自动抠图换背景,任意背景自动融合

AI 背景更换是一种利用生成式人工智能创建新图像背景的软件工具。与传统方法需要移除原有的背景并更换新的不同&#xff0c;AI背景生成器使用先进的算法生成与前景完美融合的全新背景。这项技术彻底改变了图像编辑的方式&#xff0c;为设计提供了更多的创造自由和灵活性。 特点…

数据结构--B树、B+树

数据结构--B树、B树 1. 什么是B树2.建立B树的要求3.什么是B树4.Mysql里面为什么使用B树作为索引结构&#xff1f; 1. 什么是B树 B树是一种数据结构&#xff0c;用于在硬盘或其他非易失性存储介质上快速存储和访问大量数据。它是一种平衡树&#xff0c;其每个节点可以存储多个键…

zabbix SNMP traps 监控案例

目标 根据H3C网络设备 发送 SNMP trap 信息进行网络端口的告警。 具体过程 继上次配置的trap 方式进行监控一个案例。 其中log数据中的内容是&#xff1a; 20230330.163810 ZBXTRAP 192.168.21.148 UDP: [192.168.21.148]:52289->[172.18.18.2]:1162 DISMAN-EVENT-MIB::…

Keil5软件安装方法(兼容stm32与c51方法)

目录 一、下载软件包 二、安装软件 1、安装C51v960a.exe (1&#xff09;右键以管理员权限运行程序 &#xff08;2&#xff09;开始安装软件 &#xff08;3&#xff09;勾选协议 &#xff08;4&#xff09;选择安装路径 &#xff08;5&#xff09;填写名字与邮箱 &#xff0…

我国元宇宙行业分析:政策、技术、资金助推行业探索多元化应用场景

1.元宇宙行业概述、特征及产业链图解 元宇宙是人类运用数字技术构建的&#xff0c;由现实世界映射或超越现实世界&#xff0c;可与现实世界交互的虚拟世界&#xff0c;具备新型社会体系的数字生活空间&#xff0c;主要具有沉浸式体验、开放性、虚拟身份、不断演化、知识互动、…

c/c++:指针,指针定义和使用,指针大小4字节,野指针,空指针*p=NULL

c/c:指针&#xff0c;指针定义和使用&#xff0c;指针大小4字节&#xff0c;野指针&#xff0c;空指针*pNULL 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;此时学会c的话&#xff0c; 我所知道的周边的会c的同学&#xf…

VMware开机自启虚拟机系统

一、前提 wmware开机自启&#xff0c;安装完毕wmware不用管&#xff0c;默认该软件以及相关服务就是开机自启准备waware虚拟机&#xff08;一般都linux&#xff0c;我用centos7&#xff0c;你随意&#xff09; 二、脚本 脚本命令如下&#xff0c;等待30秒&#xff08;给服务自启…

NXP公司K20+PF8100实现硬件窗口看门狗

Kinetis K20 72 MHz MCU系列为中等性能的Kinetis产品组合提供了可扩展的入门级产品&#xff0c;具有差异化的集成&#xff0c;配备高精度模拟集成和灵活的低功耗功能。其相关资源可在NXP的官网获得。 PF81/PF82为PMIC系列专为高性能处理应用而设计&#xff0c;如娱乐中控、车载…

阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小黄&#xff0c;独角兽企业的Java开发工程师&#xff0c;CSDN博客专家&#xff0c;阿里云专家博主&#x1f4d5;系列专栏&#xff1a;Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙…

【数据结构】七大排序之快速排序详解(挖坑法快排,非递归快排,二路快排,三路快排)

目录 1.快速排序核心思路 2.挖坑法快速排序&#xff08;递归&#xff09; 2.1步骤 2.2代码&#xff08;详细注释&#xff09; 3.非递归快排&#xff08;用栈实现快速排序&#xff09; 3.1思路 3.2代码 4.二路快排 4.1思路 4.2代码 5.三路快排 5.1思路 5.2代码 1.快速…

大白话chatGPT及其原理之快速理解篇

大白话chatGPT及其原理之快速理解篇 从GPT名字理解chatGPTchatGPT三步曲 声明&#xff1a;本文为原创&#xff0c;未经同意请勿转载&#xff0c;感谢配合&#x1f604; chatGPT今年年初的时候是非常火爆的&#xff0c;现在也有很多相关的应用和插件。当然现在也有很多新的技术出…

老宋 带你五分钟搞懂vue

Vue 1.1 什么是框架 任何编程语言在最初的时候都是没有框架的&#xff0c;后来随着在实际开发过程中不断总结『经验』&#xff0c;积累『最佳实践』&#xff0c;慢慢的人们发现很多『特定场景』下的『特定问题』总是可以『套用固定解决方案』。于是有人把成熟的『固定解决方案…

袋鼠云春季生长大会圆满落幕,带来数实融合下的新产品、新方案、新实践

4月20日&#xff0c;以“数实融合&#xff0c;韧性生长”为主题的袋鼠云春季生长大会圆满落幕。 在春季生长大会中&#xff0c;袋鼠云带来了数实融合趋势下的最新行业沉淀、最佳实践经验和行业前瞻性的产品发布。从大数据基础软件“数栈”、到低代码数字孪生世界“易知微”&…

离散数学-考纲版-01-命题逻辑

文章目录 1. 命题逻辑的等值演算与推理演算参考1.1 命题1.2 常用联结词1.3 命题公式命题公式的分类-重言式-矛盾式-可满足式等价关系式-逻辑等价 logically equivalent 1.4 命题的等值演算与推理基本等价式逻辑蕴涵重言式 logically implication重言蕴涵推到归结法 1.5 命题公式…

log4j2日志简单使用

log4j2日志使用 1、log4j2介绍 Apache Log4j2是对Log4j的升级版&#xff0c; log4j2借鉴了logback的一些优秀的设计&#xff0c;并且修复了一些问题&#xff0c;因此带来了一些重大的提升&#xff0c;主要有&#xff1a; 1、异常处理&#xff1a;在logback中&#xff0c;Appe…

Makefile通用模板

工程目录 假如我们有以下目录结构&#xff1a; . ├── inc │ ├── add.h │ └── sub.h ├── main.c └── src├── add.c└── sub.c文件中的内容如下&#xff1a; //main.c #include <stdio.h> #include "add.h" #include "sub.h&q…

Mysql 学习(六)Mysql的数据目录

数据库中数据的存放 Mysql中 InnoDB 和 MyISAM 这样的存储引擎都是把数据存储到磁盘上的&#xff0c;而我们把这种存放到磁盘上的东西叫做文件系统&#xff0c;当我们想读取对应数据的时候&#xff0c;就会把数据从文件系统上加载&#xff0c;并且处理返回给我们&#xff0c;当…