Okio源码分析

1 概述

Okio是一个对java.io和java.nio进行补充的库,使数据访问,保存和处理变得更容易。

Okio的主要功能是围绕着ByteString 和 Buffer 两个类展开的:
1> ByteString是一个immutable的字节序列。在java中,String代表的是字符串,ByteString和String很相似,只不过是用来处理字节串的,同时也提供了常用的操作,比如对数据进行十六进制(hex)、base64UTF-8 格式的编码和解码,equals、substring等操作。

2> Buffer是一个mutable的字节序列。 和ArrayList类似,不需要提前设置缓冲区大小。读取数据和写入数据和队列类似,从它的head读取数据,往它的tail写入数据,而且不用考虑容量、位置等因素。

java.io设计的一个优雅部分是如何将stream分层以进行加密和压缩等转换。 Okio包括自己的stream类型,称为Source和Sink,和InputStream和OutputStream的工作方式类似,但有一些关键的区别:
1> Timeouts: 提供对底层I/O访问的超时机制。
2> Source和Sink的API非常简洁,易于实现。
3> 虽然Source和Sink的只提供了三个方法,但是BufferedSource和BufferedSink接口提供了更丰富的方法(比如针对不同类型的read和write方法),以应对更加复杂的场景。
4> 不在区分byte stream和char stream,它们都是数据,可以按照任意类型进行读写。

2 Segment和SegmentPool

Segment的源码不到200行,直接通过源码来理解Segment的实现原理也是很简单的,首先来看一下Segment中的所有的字段:

/** Segment可以保存的最大字节数 */
static final int SIZE = 8192;

/** Segment被共享时最小的字节数 */
static final int SHARE_MINIMUM = 1024;

/** Segment中保存数据的字节数组 */
final byte[] data;

/** 字节数组data中被当前Segment实例使用的区间的第一个字节的下标 */
int pos;

/** 字节数组data中被当前Segment实例使用的区间之后的第一个字节的下标 */
int limit;

/** 代表字节数组data是否被 >=2 个Segment实例共用*/
boolean shared;

/** 代表字节数组data中最后一段被使用的区间是不是被当前Segment实例占有*/
boolean owner;

/** 当前Segment实例的后置节点 */
Segment next;

/** 当前Segment实例的前置节点 */
Segment prev;

shared、owner的作用:
在向Segment中写入数据时,首先用owner判断当前Segment实例对应的数据区间(字节数组data被使用的区间)之后是否可以写入数据,接着用shared判断当前Segment实例对应的数据区间之前是否可以写入数据,体现在了Segment的writeTo方法中。

接下来依次分析Segment中的方法:

  /**
   * 从循环双向链表中移除当前Segment实例,返回当前Segment实例的后置节点。
   */
  public @Nullable Segment pop() {
    Segment result = next != this ? next : null;
    prev.next = next;
    next.prev = prev;
    next = null;
    prev = null;
    return result;
  }

  /**
   * 在循环双向链表中的当前Segment实例之后插入segment实例,返回被插入的segment实例。
   */
  public Segment push(Segment segment) {
    segment.prev = this;
    segment.next = next;
    next.prev = segment;
    next = segment;
    return segment;
  }

上面的两个方法相信大家一看就明白了,就不再赘叙了。

  /**
   * 将当前Segment实例中的字节数组data进行分割,从而得到两个Segment实例. 
   * 字节数组data中[pos..pos+byteCount)区间的数据属于第一个segment. 
   * [pos+byteCount..limit)区间的数据属于第二个segment.
   *
   * 返回第一个segment.
   */
  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 = new Segment(this);
    } else {
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    prev.push(prefix);
    return prefix;
  }

  /**
   * 当当前Segment实例的前置节点中的空闲空间可以容纳当前Segment实例中的数据. 
   * 则将当前Segment实例中的数据拷贝到前置节点中并且将当前Segment实例回收到SegmentPool中。
   */
  public void compact() {
    if (prev == this) throw new IllegalStateException();
    if (!prev.owner) return; // Cannot compact: prev isn't writable.
    int byteCount = limit - pos;
    int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
    if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
    writeTo(prev, byteCount);
    pop();
    SegmentPool.recycle(this);
  }

  /** 将当前Segment实例中的前byteCount个字节的数据复制放到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;
  }

上面的注释已经非常清晰了,这里就不再解释了。

上面的compact方法中用到了SegmentPool.recycle(this)来回收Segment实例,那下面就来讲解SegmentPool类,该类的存在就是为了避免GC churn(高频率的创建和回收Segment实例会导致GC churn)和zero-fill(创建Segment实例时字节数组data需要zero-fill),SegmentPool实例中用一个单向的链表来保存回收的Segment实例,首先来看看Segment的源代码:

/**
 * 用于保存被回收的Segment实例,该类的存在就是为了避免GC churn和zero-fill
 * SegmentPool实例是线程安全的静态单例
 */
final class SegmentPool {
  /** SegmentPool实例中保存的最大字节数,因此SegmentPool中最多保存8个Segment实例 */
  // TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments?
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  /** SegmentPool实例中是通过单向非循环的链表来保存数据的,next代表链表中的第一个Segment实例 */
  static @Nullable Segment next;

  /** SegmentPool实例中的字节总数. */
  static long byteCount;

  private SegmentPool() {
  }

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

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

是不是很简单,一共也不到70行,一共提供了两个方法:
Segment take():从SegmentPool实例中获取被回收的Segment实例,如果SegmentPool实例是空的,则创建一个Segment实例返回。
void recycle(Segment segment):回收segment实例。

3 Buffer

Buffer内部使用Segment的双向链表来保存数据,Segment内部使用字节数组保存数据。 将数据从一个Buffer移动到另一个Buffer时,会通过转让Segment的所有权,而不用拷贝数据,从而节省性能上的开销。下面通过一张图来描述一下Buffer中双向循环链表和SegmentPool单向非循环链表:


左边对应Buffer,右边对应SegmentPool

下面通过一张类图来整体的描述一下Buffer:


为了更加清晰的理解上图,就需要简单的了解一下装饰者模式
1> 定义:Attach additional responsibilities to an object dynamically keeping the same interface.Decorators provide a flexible alternative to subclassing for extending functionality. (动态的给一个对象添加额外的职责。就增加功能来说,装饰者模式相比生成子类更加灵活。)
2> 装饰者模式通用类图

说明一下类图中的四个角色:
Component抽象组件:Component是一个接口或者是抽象类,在装饰者模式中,必然有一个最基本、最核心、最原始的接口或抽象类充当Component抽象组件。对应于在Okio框架中的BufferedSource和BufferedSink接口。
ConcreteComponent具体组件:对Component抽象组件的实现,将要被装饰的类。对应于Okio框架中的Buffer。
Decorator装饰者:一般是一个抽象类,继承至Component抽象组件。一定拥有一个指向Component抽象组件的priavte字段。
ConcreteDecorator具体装饰者:对Decorator装饰者的实现,用来装饰ConcreteComponent具体组件。
在Okio框架中没有细分Decorator和ConcreteDecorator,只有两个具体装饰类RealBufferedSource和RealBufferedSink。

Source是用来对数据来源的封装,Sink是对数据消费的封装,在Okio工具类中,为Source提供了四种数据来源:Socket、InputStream、File和Path,同样为Sink提供了四种数据消费:Socket、OutputStream、File和Path,接下来针对Socket举例分析:

public void testSocket(Socket socket) {
    try {
        Source source = Okio.source(socket);
        BufferedSource bufferedSource = Okio.buffer(source);
        bufferedSource.timeout().timeout(500, TimeUnit.MILLISECONDS);
        String data = bufferedSource.readString(Charset.forName("UTF-8"));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

上面就是使用Okio框架读数据的过程,下面我们就来看看源码中是如何实现的:

   // BufferedSource的方法:
  @Override public String readString(Charset charset) throws IOException {
    if (charset == null) throw new IllegalArgumentException("charset == null");

    buffer.writeAll(source);
    return buffer.readString(charset);
  }

  // Buffered的方法:
  @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;
  }

  @Override public String readString(Charset charset) {
    try {
      return readString(size, charset);
    } catch (EOFException e) {
      throw new AssertionError(e);
    }
  }

  @Override public String readString(long byteCount, Charset charset) throws EOFException {
    checkOffsetAndCount(size, 0, byteCount);
    if (charset == null) throw new IllegalArgumentException("charset == null");
    if (byteCount > Integer.MAX_VALUE) {
      throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
    }
    if (byteCount == 0) return "";

    Segment s = head;
    if (s.pos + byteCount > s.limit) {
      // If the string spans multiple segments, delegate to readBytes().
      return new String(readByteArray(byteCount), charset);
    }

    String result = new String(s.data, s.pos, (int) byteCount, charset);
    s.pos += byteCount;
    size -= byteCount;

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

    return result;
  }

  // Okio的方法:
  /**
   * Returns a new source that buffers reads from {@code source}. The returned
   * source will perform bulk reads into its in-memory buffer. Use this wherever
   * you read a source to get an ergonomic and efficient access to data.
   */
  public static BufferedSource buffer(Source source) {
    return new RealBufferedSource(source);
  }

  /**
   * Returns a source that reads from {@code socket}. Prefer this over {@link
   * #source(InputStream)} because this method honors timeouts. When the socket
   * read times out, the socket is asynchronously closed by a watchdog thread.
   */
  public static Source source(Socket socket) throws IOException {
    if (socket == null) throw new IllegalArgumentException("socket == null");
    AsyncTimeout timeout = timeout(socket);
    Source source = source(socket.getInputStream(), timeout);
    return timeout.source(source);
  }

  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);
          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
          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 + ")";
      }
    };
  }

上面方法的流程可以概括如下:
1> 利用Source的public long read(Buffer sink, long byteCount)方法从Socket输入流中读取数据到Buffer实例中。
2> 接着调用Buffer的public String readString(Charset charset)方法将Buffer实例中的数据读取到String对象中并且返回。
通过Okio框架写数据的过程与读数据的过程类似,只不过过程相反,就不再赘叙了。

下面给出Okio框架读写String数据的流程图:


对于Okio框架读写其他类型数据也是类似的过程。

在上面的例子中还用到了TimeOut机制,其实Okio实现了两种超时机制:
1> TimeOut 同步超时机制
利用throwIfReached方法在数据读取过程中轮询判断是否超时。
2> AsyncTimeout 异步超时机制
由于通过Socket来读写数据会阻塞线程,所以用的是异步超时机制。
有兴趣的同学可以自己阅读源码来分析超时机制。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,809评论 1 304
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,651评论 1 257
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 95,178评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,241评论 0 181
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 49,047评论 1 259
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,899评论 1 178
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,503评论 2 274
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,249评论 0 168
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,125评论 6 235
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,605评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,368评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,723评论 1 232
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,285评论 0 32
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,190评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,634评论 3 209
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,651评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,052评论 0 167
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,638评论 2 232
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,760评论 2 237

推荐阅读更多精彩内容