Okio源码解析

本文深入探讨Okio库的特性,包括API简化、缓存优化、超时处理。Okio是对Java原生IO的封装,通过统一的初始化方式简化流程,优化了缓存操作,如Segment链表和SegmentPool。此外,Okio提供了同步和异步超时处理,增强了流的操作效率。文章详细介绍了读写流程和Buffer操作,并解析了Timeout机制的工作原理。
摘要由CSDN通过智能技术生成

Okio是对java原生io的封装,旨在简化api同时优化io操作性能。接下来我会从下面几个方面介绍

  1. Okio特性概述
  2. 读写流程源码查看
  3. Buffer精华操作
  4. Timeout超时处理

1. Okio特性概述

java已经提供了很多io实现类供我们在不同场景使用,而Okio并不是一种新的io,而是对原生io的一次封装,为的是解决原生io的一些缺陷,下面我们介绍Okio的特性

1.1 api简化

我们知道java的io相关的类非常多,有针对字节和字符的输入输出接口,有实现缓存的Bufferedxxx,以及各种子类实现比如文件的(FileInputStream和FileOutputStream),数据的(DataInputStream和DataOutputStream),对象的(ObjectInputStream和ObjectOutputStream)等等,针对不同的场景我们需要使用不同的类,是非常复杂的。

而Okio简化了这一流程,统一通过Okio这个工厂类初始化,内部是通过重载方法根据传入的参数不同初始化不同的流。

举个板栗

        File file = new File(Environment.getExternalStorageState(), "test");
        Okio.buffer(Okio.sink(file)).writeString("aaaa", Util.UTF_8).close();

由于传入的参数是File,内部是通过FileOutputStream进行io写操作,并且支持链式操作。

1.2 缓存优化

原生的io缓存比较粗暴,Okio在这方面做了很多优化。

以BufferedOutputStream为例,是利用一个长度为8192的byte数组缓存,当要写入数据长度大于缓存最大容量时跳过缓存直接进行io写操作,当写入数据长度大于缓存数组剩余容量的时候先把缓存写入输出流,再将数据写入缓存,其他情况直接写入缓存。

然后原生的输入输出流之间的buffer是没有办法直接建立联系的,输入流中的数据转移到输出流中是:输入buf -> 临时byte数组 -> 输出buf,经历两次拷贝

        //原生
        File file = new File(Environment.getExternalStorageState(), "test");
        try {
   
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
            int count = -1;
            byte[] array = new byte[1024];//临时byte数组
            while ((count = bis.read(array)) != -1) {
   
                bos.write(array, 0, count);
            }
            bis.close();
            bos.close();
        } catch (FileNotFoundException e) {
   
            e.printStackTrace();
        } catch (IOException e) {
   
            e.printStackTrace();
        }

okio的话则是用一个Buffer对象去负责缓存,内部维护了一个Segment双向链表,而这个Segment则是真正的数据缓存载体,内部利用一个长度为8192的byte数组去缓存,当要写入的数据超过8192的时候则会创建多个Segment对象在链表中有序存储。当Segment使用完毕的时候又会利用SegmentPool进行回收,减少Segment对象创建。

对于输入流流到输出流有专门的优化,直接将输入流buffer中segment数据转移到输出流的buffer中只有一次数据的操作,相比原生粗暴的buffer做了很多优化。(而实际操作是,如果是整段的segment数据转移则是直接修改指针指到输出流的buffer上,如果只转移输入流Segment部分数据则根据输出Segment能不能装得下,装得下的话则进行数据拷贝,否则拆分输入流Segment为两个然后将要转移数据的Segment指针指向输出流Buffer,总之Okio在这块做了很多优化,这个后面会细说)

1.3 超时操作

原生进行io操作的时候是阻塞式的,没有超时的处理,除非发生异常才会停止,okio增加了超时处理,推出了Timeout机制,提供了同步超时和异步超时处理。

同步超时Timeout:是在每次进行读写前检测是否超时,如果超时则抛出异常,那如果检测完之后操作阻塞了很久是没法停止的,只有等到下一次操作的时候才会抛出异常停止操作。

异步超时AsyncTimeout:是在每次要进行读写操作的时候创建一个AsymcTimeout对象,然后通过一个链表存储,根据即将超时时间排序,快要超时的排在链表头部,然后启动一个Watchdog线程进行检测,优先从链表头部取出AsyncTimeout判断是否超时,超时了的话则调用AsyncTimeout#timeout()方法。okio给用socket创建流提供了默认实现,timeout的时候直接关闭Socket。

1.4 相关类介绍

  • Source:Okio对输入流的抽象接口,提供了read方法将数据读到buffer中
  • Sink:Okio对输出流的抽象接口,提供了write方法将数据写到buffer中
  • BufferedSource和BufferedSink:是Okio对Buffer的接口抽象,分别继承Source和Sink
  • RealBufferedSource:BufferedSource实现类,读操作都是由该类来完成,可以通过Okio工厂获取
  • RealBufferedSink:BufferedSink实现类,写操作都是由该类来完成,可以通过Okio工厂获取
  • Okio:okio的工厂来用来获取buffer实现类和流的实现类
  • Segment:Buffer中存储数据的载体,内部通过一个8K的byte数组存储数据
  • SegmentPool:管理Segment创建和回收,最多存储64K的数据也就是8个Segment,当池中存在Segment的时候会复用减少对象的创建

以读写文件为例,读是先文件内容读到buffer中,然后再从buffer中获取数据,写是先写数据到buffer中,然后将将buffer中的数据写入文件,而buffe中数据是通过一个个Segment存储的,Segment则是通过SegmentPool创建和回收,每个类各司其职。

2. 读写流程源码

2.1 读流程

		File file = new File(Environment.getExternalStorageState(), "test");
    try {
   
         BufferedSource source = Okio.buffer(Okio.source(file));
         byte[] array = new byte[1024];
         source.read(array);
         source.close();
     } catch (Exception e) {
   
         e.printStackTrace();
     }

根据上面的例子我们分几段查看源码

  1. Okio.source()
  2. Okio.buffer()
  3. BufferedSource.read()
2.1.1 Okio.source()
  //Okio#source
	public static Source source(File file) throws FileNotFoundException {
   
    if (file == null) throw new IllegalArgumentException("file == null");
    return source(new FileInputStream(file));
  }

可以看到由于我们传入的是file所以内部实际是通过FileInputStream读文件

  public static Source source(InputStream in) {
   
    return source(in, new Timeout());
  }

  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 tail = sink.writableSegment(1);//从buffer中拿到一个可写的Segment
          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);//byteCount是要读取的数据总量,Segment.SIZE - tail.limit是Segment可以装的数据量,取最小值
          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);//然后通过FileInputStream将文件数据读到segment的data中
          if (bytesRead == -1) return -1;
          tail.limit += bytesRead;
          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 + ")";
      }
    };
  }

上面这段就是创建了一个Source实现类,read的话则是先获取buffer中可写的segment,然后用FileInputStream将文件的数据读到segment中。对于Segment和Buffer在下面会说到

2.1.2 Okio.Buffer()
  //Okio#buffer
	public static BufferedSource buffer(Source source) {
   
    return new RealBufferedSource(source);
  }

由于我们传入的是Source所以会创建一个RealBufferedSource实例,Okio也都是通过它来做读操作,我们先来看下这个类

final class RealBufferedSource implements BufferedSource {
   
  public final Buffer buffer = new Buffer();//buffer对象
  public final Source source;//Source实现类

  RealBufferedSource(Source source) {
   
    if (source == null) throw new NullPointerException("source == null");
    this.source = source;
  }
  
  @Override 
  public int readInt() throws IOException {
   
    require(4);//最终通过source#read()将数据读到Buffer
    return buffer.readInt();//再从Buffer读取数据
  }
  
  @Override 
  public int read(byte[] sink, int offset, int byteCount) throws IOException {
   
    checkOffsetAndCount(sink.length, offset, byteCount);

    if (buffer.size == 0) {
   
      long read = source.read(buffer, Segment.SIZE);//先将数据读到Buffer
      if (read == -1) return -1;
    }

    int toRead = (int) Math.min(byteCount, buffer.size);
    return buffer.read(sink, offset, toRead);//再从Buffer读取数据
  }
  
  @Override 
  public String readString(Charset charset) throws IOException {
   
    if (charset == null) throw new IllegalArgumentException("charset == null");

    buffer.writeAll(source);//最终通过source#read()将数据读到Buffer
    return buffer.readString(charset);//再从Buffer读取数据
  }
  
  //后面省略了很多个read方法
}

可以看到它先将Okio.buffer(Okio.source(file))传入的Source实现类通过source成员变量存储,然后创建了一个Buffer对象用作缓冲区,而read方法都是一个流程通过source#read()方法将数据写入缓冲区,然后再从缓冲区中读取数据。接下来我们在看下缓冲区Buffer对象

public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel {
   
  @Nullable Segment head;//缓存数据的载体
  long size;//当前缓冲区的数据量单位字节
  public Buffer(
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值