Android | 彻底理解 Okio 之源码篇

Okio 2.X 及其以上的版本的源码为 kotlin 语言编写。本文讲解的Okio源码版本为 1.17.2,仍为 Java 语言。

okio库的类结构

okio 主要的接口和类

okio接口和类的说明

名称类型描述
Source接口定义了输入流的几个基本方法
BufferedSource接口继承Source接口,新增了一系列readXxx方法
RealBufferedSource实现了BufferedSource接口
Sink接口定义了输出流的几个基本方法
BufferedSink接口继承Sink接口,新增了一系列writeXxx方法
RealBufferedSink实现了BufferedSink接口
Buffer同时实现了BufferedSource和BufferedSink接口。被RealBufferedSource和RealBufferedSink所持有,是读取和写入操作的真正实现类。
  1. readXxx系列方法是从缓冲区读出数据的方法。
  2. writeXxx系列方法是向缓冲区写入数据的方法。

okio读取文件

使用 okio 来读取文件非常的简单,只需要简单的几步。

  1. 调用Okio.source方法获得Source对象
  2. 调用Okio.buffer方法获得BufferedSource对象。因为BufferedSource是个接口,它里面定义了一系列的readXxx方法,可以用来方便的读取输入流的内容。
public void readFile() {
    try {
        FileInputStream fis = new FileInputStream("test.txt");
        okio.Source source = Okio.source(fis);
        BufferedSource bs = Okio.buffer(source);
        String res = bs.readUtf8();
        System.out.println(res);
    } catch (Exception e){
        e.printStackTrace();
    }
}

Okio.source 方法

Okio.source重写了read方法,并返回一个Source对象。所以当我们调用Source对象的read(Buffer sink, long byteCount)方法时,其实是在调用该处重写的方法。read方法会从输入流进行一次读取操作,将数据读取到尾部的Segment中。

  private static Source source(final InputStream in, final Timeout timeout) {
    if (in == null) throw new IllegalArgumentException("in == null");
    if (timeout == null) throw new IllegalArgumentException("timeout == null");

    return new Source() {
      @Override public long read(Buffer sink, long byteCount) throws IOException {
        if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
        if (byteCount == 0) return 0;
        try {
          // 判断是否中断这次的读取操作
          timeout.throwIfReached();
         // 获取双链表尾部的 Segment
          Segment tail = sink.writableSegment(1);
         // 从输入流最多读取 maxToCopy 个字节
          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
         // 从输入流读取数据到 Segment
          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
         // 到达输入流尾部
          if (bytesRead == -1) return -1;
         // 更新 tail 的 limit
          tail.limit += bytesRead;
         // 更新 sink 的 size 值
          sink.size += bytesRead;
          return bytesRead;
        } catch (AssertionError e) {
          if (isAndroidGetsocknameError(e)) throw new IOException(e);
          throw e;
        }
      }

      @Override public void close() throws IOException {
        in.close();
      }

      @Override public Timeout timeout() {
        return timeout;
      }

      @Override public String toString() {
        return "source(" + in + ")";
      }
    };
  }

  1. read 方法首先会调用timeout.throwIfReached(),这个方法是Okio中的同步超时检测。它的作用有两个,一是检查当前线程是否中断,二是判断即将开始的读取操作是否在已经到达了截止时间,以上有任何一个条件不满足,将会抛出异常中断此次操作。比如我们将上面读取文件的代码设置一下读取操作需要在未来的1ms内完成。这意味着接下来的readUtf8操作,必须要在未来的1ms内完成,否则抛出异常。
public void readFile() {
    try {
        FileInputStream fis = new FileInputStream("test.txt");
        okio.Source source = Okio.source(fis);
        BufferedSource bs = Okio.buffer(source);
        // 设置超时时间为 1ms
        source.timeout().deadline(1, TimeUnit.MILLISECONDS);
        String res = bs.readUtf8();
        System.out.println(res);
    } catch (Exception e){
        e.printStackTrace();
    }
}

上面代码将会抛出如下异常。由于throwIfReached是在每次读取数据之前调用并且与数据读取在同一个线程,所以如果读取操作阻塞,则无法及时抛出异常。

java.io.InterruptedIOException: deadline reached
	at okio.Timeout.throwIfReached(Timeout.kt:102)
	at okio.InputStreamSource.read(JvmOkio.kt:87)
	at okio.Buffer.writeAll(Buffer.kt:1642)
	at okio.RealBufferedSource.readUtf8(RealBufferedSource.kt:297)

又或者在读取操作之前中断了线程,也会抛出同样的异常,如下代码。

public void readFile() {
    Thread thread = new Thread(){
        @Override
        public void run() {
            try {
                FileInputStream fis = new FileInputStream("test.txt");
                okio.Source source = Okio.source(fis);
                BufferedSource bs = Okio.buffer(source);
                // 中断当前线程
                interrupt();
                String res = bs.readUtf8();
                System.out.println(res);
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    };
    thread.start();

    try {
        thread.join();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

这里简单介绍了Okio的同步超时机制,而异步超时机制,这里就不做介绍了。

  1. read方法接着会将数据读取到双链表最尾部的Segment中,关于Segment是啥,这里暂时理解成它是一个存放数据的容器就行了。后面会详细介绍。

Okio.buffer 方法

Okio.buffer方法的看起来就简单多了,直接实例化了一个RealBufferedSource对象返回。 RealBufferedSource实现了BufferedSource接口,所以会有一系列的readXxx方法。注意此处传入了Source对象,所以在RealBufferedSource中调用source对象的read方法,是在调用上面重写过的read方法!

  public static BufferedSource buffer(Source source) {
    return new RealBufferedSource(source);
  }

readUtf8() 方法

RealBufferedSource实现了BufferedSource接口,所以调用readUtf8()方法来读取字符串时候,其实调用的是RealBufferedSource的readUtf8()方法。下面是readUtf8()方法的源码。

  @Override public String readUtf8() throws IOException {
    buffer.writeAll(source);
    return buffer.readUtf8();
  }

  1. buffer.writeAll(source)会将数据写入BufferSegment中,来看看这个方法的实现。我们发现,这里会循环的调用source.read方法,上面我们说过,调用source对象的read方法,是在调用上面重写过的read方法!所以writeAll方法的任务就是将所有的数据写入到一个或多个Segment中(一个Segment的最大容量是8kb,如果数据量大,一个Segment可能读取不了这么多)。
  @Override public long writeAll(Source source) throws IOException {
    if (source == null) throw new IllegalArgumentException("source == null");
    long totalBytesRead = 0;
    for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {
      totalBytesRead += readCount;
    }
    return totalBytesRead;
  }

  1. buffer.readUtf8()会将存储在Segment中的数据读出,转化为字符串。若一个Segment的数据被读完且它是非共享的,那么这个Segment将会被回收。

总结

使用Okio来读取输入流的数据,Okio首先会将所有的数据读取到Buffer类的一个或多个Segment中,当我们想要获取这些数据的时候,再从Segment中读出来Buffer这个类是整个Okio框架的灵魂所在,它实现了BufferedSource, BufferedSink接口,最终的读写操作都会交给它来完成。而RealBufferedSourceRealBufferedSink更像是中间人,负责把读写任务交给Buffer

有读者到这就会问了,使用Okio来读取数据并没有看到明显的优势,就是在API调用上精简了一些。其实不然,Okio天然的设计了Segment作为数据的缓冲区。同时Segment是可以回收和复用的,这就减少了内存的消耗,提高了内存的利用率。考虑一种双流操作,先读取输入流的内容再写入到输出流。传统的操作首先要将输入流缓冲区的数据拷贝到一个字节数组中,然后再将字节数组的内容拷贝到输出流缓冲区,这中间存在不同缓冲区的数据拷贝操作。而对于Okio来说,在不同缓冲区移动数据,只需移动Segment的引用,而非拷贝字节数组。

Okio双流操作

Okio的优点在于设计了Segment,而双流操作最能体现出这种天然的优势。下面代码首先从test.txt中读取文件内容,然后写入test2.txt中。

public void readAndWrite() {
    try {
        FileInputStream fis = new FileInputStream("test.txt");
        Source source = Okio.source(fis);
        BufferedSource bSource = Okio.buffer(source);

        FileOutputStream fos = new FileOutputStream("test2.txt");
        Sink sink = Okio.sink(fos);
        BufferedSink bSink = Okio.buffer(sink);

        while (!bSource.exhausted()){
            // 不停的从 test.txt 中读取数据并写入到 test2.txt
            bSource.read(bSink.buffer(), 8*1024);
            // 将输出流缓冲区的数据完全写入到文件中
            bSink.emit();
        }

        bSource.close();
        bSink.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

特别注意上面read最终会调用到Buffer类的write(Buffer source, long byteCount)方法,这个方法可以说是Buffer类最重要的方法。当将一个Buffer缓冲区的数据写入到另一个Buffer缓冲区,并不会拷贝字节,而是移动Segment的引用。除此之外,该方法还使用了Segment的分割与合并操作,将内存利用最大化。正如该方法的注释所言“while balancing two conflicting goals: don’t waste CPU and don’t waste memory.”(同时平衡两个相互冲突的目标:不浪费CPU和不浪费内存。)

在下文分析Buffer类的设计时,会详细介绍这个方法的源码。

Segment类的设计

Okio将Java类库中的输入输出流做了封装,让我们能很方便的使用这些API来完成文件的读写操作,这是Okio的一个优点。但是仅仅从API封装调用的角度,不能体现出一个框架的优势所在。Okio最精妙的地方是它设计了数据缓冲区Segment

Segment的特点

  1. Segment是一个循环双链表,有前驱(prev)和后继节点(next)
  2. 一个Segment可以存储的最大数据量是8kb(8192=8*1024)
  3. Segment有两种状态,分别是可共享和不可共享,由shared字段来区分(本质上是data字节数组是否共享)。
  4. 一个Segment如果是共享的,那么只有data字节数组的宿主Segment能对它进行修改。由owner字段来区分当前Segment是不是data字节数组的宿主。
  5. 一个Segment如果是共享的,那么这个Segment将不可以被回收,data字节数组也不可以被非宿主的Segment所修改。

Segment成员变量

  /** 一个Segment的容量 8kb */
  static final int SIZE = 8192;

  /** data可共享阈值,小于这个值则使用 System.arraycopy 拷贝,不共享*/
  static final int SHARE_MINIMUM = 1024;

  final byte[] data;

  /** 读数据的起始位 */
  int pos;

  /** 写数据的起始位 */
  int limit;

  /** data字节数组是否共享. */
  boolean shared;

  /** 当前Segment是否为data字节数组的宿主Segment,与shared互斥 */
  boolean owner;

  /** 后继节点 */
  Segment next;

  /** 前驱节点 */
  Segment prev;

Segment成员方法

Segment sharedCopy()

Segment unsharedCopy()

Segment pop()

Segment push(Segment segment)

Segment split(int byteCount)

void compact()

void writeTo(Segment sink, int byteCount)

sharedCopy 共享拷贝

sharedCopy是共享拷贝的意思,该方法会将shared字段改为true,然后实例化一个新的Segment返回。新的Segment会与当前Segment共享data字节数组(本质上是都持有data数组的引用),新返回的Segment并不是data字节数组的宿主Segment,所以它不能对data数组进行修改操作。同样,当一个Segment被标记为共享状态的时候,不能够被回收。

  final Segment sharedCopy() {
    shared = true;
    return new Segment(data, pos, limit, true, false);
  }

unsharedCopy 非共享拷贝

unsharedCopy非共享拷贝,该方法对data字节数组进行深拷贝,返回的Segment完完全全是一个新的对象。

  final Segment unsharedCopy() {
    return new Segment(data.clone(), pos, limit, false, true);
  }

pop 将当前 Segment 从双链表中移除

pop方法可以将当前的Segment从它所在的双链表中移除,并返回它的后继节点(下一个节点)。若链表中只有一个节点(只有当前节点),则将当前节点移除后返回null。

  public final @Nullable Segment pop() {
    Segment result = next != this ? next : null;
    prev.next = next;
    next.prev = prev;
    next = null;
    prev = null;
    return result;
  }

pop方法涉及到循环双链表删除节点的操作,这里详细介绍下。 当链表中只有当前Segment,调用pop方法后,结构会发生如下变化,当前Segment不再会有指向它的引用,会在某个时刻被垃圾回收掉。

其实不论链表中有多少个节点,要删除哪一个节点。循环双链表中删除节点的操作都是一样的,只需将当前要删除节点的前一个节点的next引用指向到要删除节点的下一个节点,将当前要删除节点的后一个节点的pre引用指向到要删除节点的前一个节点。对应如下两行代码。

prev.next = next;
next.prev = prev;

然后将待删除节点的prevnext引用指向null,这样需要删除的节点就脱离了这个链表,等待垃圾回收。

next = null;
prev = null;

push 将一个 Segment 添加到当前 Segment 后面

push方法可以将一个Segment添加到当前Segment的后面,与上述链表节点的删除操作类似,也是改变prevnext引用的指向来实现的,这里就不再详细说明了。

  public final Segment push(Segment segment) {
    segment.prev = this;
    segment.next = next;
    next.prev = segment;
    next = segment;
    return segment;
  }

split 字节数组数据分割

split方法可以将当前Segment分割成两个Segment(实际上是将data字节数组的数据分成两部分)。传入的byteCount参数决定了分割后的第一个Segment含有多少个字节的数据。第一个Segment会有[pos+byteCount, limit)区间的数据,第二个Segment含有[pos, pos+byteCount)区间的数据,都是左闭右开区间。

  public final Segment split(int byteCount) {
    // byteCount 参数合法性校验,若要分割的字节数量 <=0 或 > 已有的数据量,则抛出异常
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix;

    // 从当前Segment分割出一个新的Segment(prefix)
    //1. 若要分割的字节数 >= SHARE_MINIMUM(1kb),则采用共享拷贝(拷贝引用)的方式
    //2. 若分割的字节数 < 1kb,则采用拷贝的方式(完全复制,新开辟内存空间)
    if (byteCount >= SHARE_MINIMUM) {
      prefix = sharedCopy();
    } else {
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    // 更新刚分割出来的Segment(prefix)的limit值, [pos, limit = (pos+byteCount))
    prefix.limit = prefix.pos + byteCount;
    // 更新当前Segment的pos值, [pos = (pos+byteCount), limit)
    pos += byteCount;
    // 将新分割出来的Segment(prefix)添加到当前Segment的后面
    prev.push(prefix);
    // 返回新分割出来的 Segment(prefix)
    return prefix;
  }

假设当前有一个Segment存储了2kb的数据,现在要分割出512b的数据(byteCount = 512),使用split方法分割的流程如下。

需要注意的是,若采用共享拷贝的方式,那当前Segment和分割出来的Segment共享同一个data字节数组(data数组内存空间一样),区别是poslimit的值会不同。若采用完全拷贝的方式,那么两个Segment就是完全独立的,即各自的data字节数组在不同的内存空间,不共享。split方法遵循了"大块数据移动引用,小块数据进行拷贝"的思想,平衡了CPU与内存的消耗。

writeTo Segment之间字节数组数据的移动

writeTo 方法可以将byteCount个字节数据从当前Segment移动到sink中去。

  public final void writeTo(Segment sink, int byteCount) {
    // sink 参数合法性校验,若sink非data的宿主Segemnt,则抛出异常。
    // 这说明获得数据的Segment必须是data的宿主,只有宿主Segment才能对data进行修改
    if (!sink.owner) throw new IllegalArgumentException();
	  
    // 若 sink 从 limit 开始写数据,剩余的容量不足以容纳 byteCount 个字节
    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();
      // (byteCount > SIZE - (sink.limit-sink.pos))
      // 即 sink 剩余的容量不能容纳 byteCount 个字节数据,抛出异常
      if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
      // 移动 sink 的数据,从 pos = 0 开始
      System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
      sink.limit -= sink.pos;
      sink.pos = 0;
    }

    // 拷贝数据到 sink
    System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
    // 更新 sink 的 limit 值
    sink.limit += byteCount;
    // 更新当前 Segment 的 pos 值
    pos += byteCount;
  }

从上面代码可以看出,writeTo方法可以将当前Segment的一部分数据移动到sink中。需要注意的是,若sinklimit位置开始写入数据,sink剩余的容量不足以容纳byteCount个字节,那么首先会将sink原有的数据移动到数组pos=0的位置,再从新的limit位置写。若足以容纳,则从直接从最初的limit位置开始写。

compact 字节数组数据的合并

compact方法可以将当前Segment与它的前驱Segment合并成一个Segment

  public final void compact() {
    // 若链表中只有一个Segment,无法合并。抛出异常
    if (prev == this) throw new IllegalStateException();
    // 若待合并的 prev 节点非宿主,无法进行合并操作
    if (!prev.owner) return; // Cannot compact: prev isn't writable.
    // 当前 Segment 存储的字节数
    int byteCount = limit - pos;
    // prev 剩余的容量,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.
    // 将当前 Segment 的数据移动到 prev
    writeTo(prev, byteCount);
    // 将当前 Segment 从链表中移除
    pop();
    // 回收当前的 Segment
    SegmentPool.recycle(this);
  }

Segment的回收与复用

前面我们多次提到,Okio为了节约内存资源,Segment可以回收和复用。当一个Segment中不再有数据的时候(数据被读过或被写入到输出流),会被回收。而当要使用Segment来保存数据的时候,就可以从“池子”中取出一个Segment来使用,而不是直接new。SegmentPool这个类提供了recycletake两个方法,分别对应于Segment的回收与复用。在SegmentPool中使用单链表结构来保存已回收的Segment。下面是该类的源码。

final class SegmentPool {
  // 池子里最多有 8 个 Segment
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  // 单链表的头结点
  static @Nullable Segment next;

  // 池子中所有Segment的字节总数
  static long byteCount;

  private SegmentPool() {
  }

  // Segment 复用,取单链表头结点
  static Segment take() {
    synchronized (SegmentPool.class) {
      if (next != null) {
        Segment result = next;
        next = result.next;
        result.next = null;
        byteCount -= Segment.SIZE;
        return result;
      }
    }
    return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
  }

  // Segment 回收,将其放到单链表头部
  static void recycle(Segment segment) {
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    if (segment.shared) return; // This segment cannot be recycled.
    synchronized (SegmentPool.class) {
      if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
}

本质上takerecycle方法涉及单链表节点的删除和添加操作,若需要Segment,则调用take。若要回收某个Segment,则调用recycle

Buffer类的设计

Buffer类实现了BufferedSourceBufferedSink接口,最终数据的读取和写入操作都会交给这个类。

Buffer成员变量

  1. head是循环双链表的头结点,每次读数据的时候,从这个头结点开始读。因为是循环双链表,尾结点就是head.prev,每次写数据,从尾结点开始写。
  2. sizeSegment链表中保存的字节总数。当size==0时,表明该Buffer缓冲区已经没有数据。
  @Nullable Segment head;
  long size;

Buffer成员方法

write 缓冲区之间的数据移动

回顾下Okio的双流操作。在两个缓冲区之间移动数据,是不会拷贝字节的,而是移动Segment的引用。write(Buffer source, long byteCount)方法可以将source缓冲区byteCount个字节移动到当前缓冲区。现在详细分析下write(Buffer source, long byteCount)方法的源码,它到底是如果做到的。

  @Override public void write(Buffer source, long byteCount) {
    // 参数合法性校验
    if (source == null) throw new IllegalArgumentException("source == null");
    if (source == this) throw new IllegalArgumentException("source == this");
    checkOffsetAndCount(source.size, 0, byteCount);

    // 当 byteCount > 0
    while (byteCount > 0) {
      // Is a prefix of the source's head segment all that we need to move?
      // 若 byteCount 个字节数据存在于 source 的头部 Segment
      if (byteCount < (source.head.limit - source.head.pos)) {
        Segment tail = head != null ? head.prev : null;
        // 若当前缓冲区尾部的 Segment 不为 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.
          // 直接将 source缓冲区 头部 Segment 的数据移动到当前缓冲区尾部的 Segment
          source.head.writeTo(tail, (int) byteCount);
          // 更新 source 缓冲区的 size
          source.size -= byteCount;
          // 更新当前缓冲区的 size
          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 为 null || 无法容纳 byteCount 个字节
          // 将 source 缓冲区头部的 Segment 的 byteCount 个字节分割出来
          source.head = source.head.split((int) byteCount);
        }
      }

      // Remove the source's head segment and append it to our tail.
      // source 缓冲区头部节点
      Segment segmentToMove = source.head;
      // source 缓冲区头部节点的字节数
      long movedByteCount = segmentToMove.limit - segmentToMove.pos;
      // 将 source 缓冲区头部节点从双链表中移除,并返回它的下一个节点
      source.head = segmentToMove.pop();
      // 若当前缓冲区头部节点为 null
      if (head == null) {
        head = segmentToMove;
        head.next = head.prev = head;
      } else {
        // 若当前缓冲区头部节点不为 null,将 source 缓冲区头部节点添加到当前缓冲区尾部
        Segment tail = head.prev;
        tail = tail.push(segmentToMove);
        // 尝试合并
        tail.compact();
      }
      // 更新 source 缓冲区的 size
      source.size -= movedByteCount;
      // 更新当前缓冲区的 size
      size += movedByteCount;
      // 更新 byteCount
      byteCount -= movedByteCount;
    }
  }

从上面源码可以看出,将数据从一个缓冲区移动到另一个缓冲区,根据不同的情况会采取不同的移动策略。

  1. 若要移动的byteCount个字节存在于源缓冲区的头部Segment
  • 若目的缓冲区的尾部Segment能容纳byteCount个字节,则直接将源缓冲区头部SegmentbyteCount字节移动到目的缓冲区的尾部Segment,程序就结束了。这里采用的策略是拷贝字节,而非移动引用
  • 若目的缓冲区的尾部Segment不能容纳byteCount个字节,则将源缓冲区头部 Segment 的 byteCount 个字节分割(分割操作使用共享拷贝或者非共享拷贝)出来,生成一个新的Segment将其添加到目的缓冲区的尾部,之后尝试Segment合并操作。

上述代码进行第一次循环运行后,可能已经结束,可能进行下一次循环。简单来说,上述代码并不复杂。有两种数据移动的策略。

  1. 直接将源缓冲区头部SegmentbyteCount字节移动到目的缓冲区的尾部Segment。这种情况发生一次程序就结束了。这里是在拷贝字节数组。
  2. 将源缓冲区头部的Segment添加到目的缓冲的尾部。因为在循环内,这种情况可能进行多次。这里是在移动Segment的引用。

经过上述源码的讲解,想必大家对Okio有了更进一步的认识。Okio中最精妙的设计当Segment所属。在缓冲区之间移动大块数据,是在移动Segment的引用。而移动小块数据,是在拷贝字节。“不浪费CPU和不浪费内存”。

作者:程序员小北
原文链接:https://juejin.cn/post/7196741149343367205

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Okio是一个强大的Java I/O库,用于处理输入和输出流的操作。要下载Okio的jar文件,可以按照以下步骤进行: 1. 在网上的Maven仓库或者其它可靠的软件下载网站上搜索"Okio jar"。 2. 找到适合你的项目的Okio jar文件版本。可以根据你的项目需求选择稳定版本或者最新版本。 3. 点击下载链接并选择合适的下载目录。 4. 下载完成后,将jar文件移动到你的项目目录中的lib文件夹(如果没有lib文件夹,则可以创建一个新的lib文件夹)。 5. 打开你的IDE(如Eclipse、IntelliJ IDEA等),导入下载好的Okio jar文件到你的项目中。 6. 在你的代码中导入Okio库的类或者方法,你就可以开始使用Okio库了。 这些步骤将帮助你下载并且集成Okio jar到你的项目中,让你能够使用Okio库的强大功能。请确保下载和使用的是可靠的版本,并充分理解Okio库的使用方式和文档,以便更好地应用到你的项目中。 ### 回答2: OKIO是一个开源的轻量级IO库,用于在Java平台上进行高效的IO操作。你可以通过下载OKIO jar包来使用它。 首先,你需要找到OKIO的官方网站或是在Maven中央仓库中搜索OKIO。官方网站通常会提供OKIO的jar包下载链接。 一旦找到下载链接,你可以点击下载按钮来获取OKIO的jar包。下载完成后,你将得到一个以.jar结尾的文件。 接下来,将下载的jar包移动到你的Java项目中合适的位置。通常情况下,你可以将它放在项目的lib目录中。 然后,在你的项目中配置构建工具(例如Maven或Gradle)以引入OKIO的依赖。你可以在构建工具的配置文件中添加OKIO的依赖信息,例如Maven的pom.xml文件或Gradle的build.gradle文件。 在配置文件中添加OKIO的依赖信息后,保存文件并进行构建。构建工具将会自动下载并引入OKIO的jar包到你的项目中。 最后,你可以在你的Java代码中使用OKIO库了。可以通过导入OKIO的类来使用它提供的功能,例如读取和写入文件、缓冲区操作等。 总结来说,OKIO的jar包可以通过下载官方网站提供的链接或在构建工具中引入依赖来获取。下载完成后,将jar包放置在项目中合适的位置,并在配置文件中添加依赖信息。完成这些步骤后,你就可以在你的项目中使用OKIO库了。 ### 回答3: Okio是一个高效的Java I/O库,主要用于处理流和字节。要下载Okio JAR文件,可以按照以下步骤进行操作: 1. 打开浏览器并前往Maven仓库的网站(https://mvnrepository.com/)。 2. 在搜索框中输入“Okio”并点击搜索按钮。 3. 在搜索结果中找到最新版本的Okio库,通常以“okio”开头。单击库的版本号以进入详细信息页面。 4. 在详细信息页面中,您将看到有关该库的信息,包括依赖项和Gradle / Maven坐标。 5. 在坐标部分,您可以找到Gradle和Maven的引用代码。根据您的项目构建工具选择适合您的代码。 - 如果您使用Gradle构建项目,请将Gradle代码复制到项目的build.gradle文件中的dependencies部分。 - 如果您使用Maven构建项目,请将Maven代码复制到项目的pom.xml文件中的dependencies部分。 6. 复制引用代码后,保存并关闭文件。 7. 重新构建和编译项目,您的项目将自动下载并使用Okio JAR文件。 请注意,确保您的网络连接良好,以便从Maven仓库成功下载JAR文件。另外,如果您使用的是集成开发环境(IDE),如IntelliJ IDEA或Eclipse,您可以使用IDE的依赖管理工具来搜索并添加Okio库,这更加方便。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值