Okhttp IO 之 Segment & SegmentPool

看本文前,我希望你对链表的操作有个基本的认识,否则你会看得比较痛苦,因为我不会解析链表的操作。

Segment

要想搞明白 okio 的运作机制,Segment 是首先要弄清楚的,Segment 是用作 okio包下的 BufferSegmentPool 的结点。

参数说明

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

        // 底层字节数组
        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}. */
        boolean owner;

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

        /** Previous segment in a circularly-linked list. */
        Segment prev;
    }
  1. byte[] data 很明显代表底层字节数组,用来存储字节。
  2. int pos 代表从这个 Segment 读取时候的起始位置
  3. int limit 代表向这个 Segment 写数据时候的起始位置
  4. boolean shared 代表当前 Segment 是否是共享的。如何决定共享呢?

    • 如果使用不带参数的构造函数创建,那么底层数组就是自己创建的,创建出来的 Segment 就是不共享的。

          Segment() {
              this.data = new byte[SIZE];
              this.owner = true;
              this.shared = false;
          }
    • 如果使用另外一个 Segment 当作参数来创建新的 Segment,那么这这两个 Segment 都是共享的,共享的就是底层数组

          Segment(Segment shareFrom) {
              this(shareFrom.data, shareFrom.pos, shareFrom.limit);
              shareFrom.shared = true;
          }
      
          Segment(byte[] data, int pos, int limit) {
              this.data = data;
              this.pos = pos;
              this.limit = limit;
              this.owner = false;
              this.shared = true;
          }
  5. boolean owner 代表当前 Segment 是否是底层数组的拥有者。底层数组的第一个创建者就是拥有者,也就是调用无参构造方法创建的才是底层数组的拥有者。
  6. Segment nextSegment prev 代表当前 Segment 的前驱结点和后继结点。因此使用 Segment 可以构造双链表。

形成链表

Segment 因为有了前驱结点指针和后继结点指针,因此能形成链表。

它帮助 okioBuffer 类形成循环双链表,现在来说明下是如何形成循环双链表的。

Buffer 创建头结点的时候,就会让它形成一个循环链表

    // 截取的 Buffer 类的 writableSegment() 方法中的代码
    if (head == null) {
        head = SegmentPool.take(); 
        return head.next = head.prev = head;
    }

如图 HEAD
这里写图片描述

Buffer 读数据的时候,如果循环链表最后一个结点的空间不够的时候,就会从 SegmentPool 中重新获取一个 Segment,然后加入到链表的尾部

    // 截取的 Buffer 类的 writableSegment() 方法中的代码
    Segment tail = head.prev;
    if(tail.limit +minimumCapacity >Segment.SIZE ||!tail.owner){
        tail = tail.push(SegmentPool.take());
    }

它会调用 Segmentpush 来形成循环双向链表

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

如图
这里写图片描述

Segment还能帮助 SegmentPool 形成一个单链表。 当一个 SegmentBuffer 的循环双向链表上移除的时候,就会调用 SegmentPool.recycler() 回收

    // 可以容纳32个Segment
    static final long MAX_SIZE = 64 * 1024; // 64 KiB.

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

从实现可以看出,这个单链表是从后往前形成的。假如当前 SegmentPool 的链表没有结点,那么 next 就是 null,如图

这里写图片描述

当回收一个 Segment 后,如下图

这里写图片描述

从链表中移除

既然能形成链表,当然就能把结点从链表中移除,okioBuffer 类的 writeTo() 方法,当把一个 Segment 的数据完全写出后,就会把这个 Segment 从链表中移除

        // 截取自 Buffer.java 的 writeTo() 方法的代码片段
        if (s.pos == s.limit) {
            Segment toRecycle = s;
            head = s = toRecycle.pop();
            SegmentPool.recycle(toRecycle);
        }

Segment 类的 pop() 方法实现了移除的操作

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

假如现在的 Buffer 的双链表如图

这里写图片描述

现在移除 HEAD 指向的结点

这里写图片描述

Split()

Segmentsplit() 方法可以把当前 Segment 按照字节数分为两个 Segment

    public Segment split(int byteCount) {
        if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
        // 用当前Segment创建新的Segment
        Segment prefix = new Segment(this);
        // 调整新的Segment的limit位置
        prefix.limit = prefix.pos + byteCount;
        // 调整旧的Segment的pos位置
        pos += byteCount;
        // 把新的Segment放到prev结点后面,从而形成再次形成双链表
        prev.push(prefix);
        return prefix;
    }

这个功能是在 okioBuffer 中使用的,假如 Buffer 的循环双向链表功能如下图

这里写图片描述

现在把 HEAD 指向的分为两个后的效果图

这里写图片描述

虚线框中两个元素就是分离 HEAD 后的两个 Segment

compact()

compact() 方法我称之为合并 Segment,这个方法用来 Buffer 类和 Buffer 类之间传输数据,使用 compact() 可以节约内存。

当两个 Buffer 之间传输数据的时候,如果 Source Buffer 有两个 Segment,它们的填充底分别为 30%80%,我们把它表示为 [30%, 80%],而 Sink Buffer 也有两个 Segment,我们把它表示为 [100%, 40%]

Buffer 之间传输数据的原理是,把 Source Buffer 的当前结点移除,然后把这个移除的结点加入到 Sink Buffer 的链表中。现在把 Source BufferSegment 全部加入 Sink Buffer 的链表中,就会形成 [100%, 40%, 30%, 80%],而中间两个 Segment 的填充度都小于 50%,所以为了节约内存考虑,可以把这两个 Segment 给合并了,这就是 compact() 的作用。

Buffer 使用 compact() 代码片段如下

        // Buffer.java 的 write() 方法片段

        // 获取尾部结点
        Segment tail = head.prev;
        // 把新结点添加到尾部,然后 tail 重新指向尾部结点
        tail = tail.push(segmentToMove);
        // 合并(这个合并是有条件的,只是这里没有写出来)
        tail.compact();

现在看下 Segmentcompact() 的代码

    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 的字节数量。

第五行,计算了前驱结点可用字节数。

第六行,如果当前结点的字节数大于前驱结点可用的字节数,很显然就不能合并。

第七行,把当前结点的字节写到前驱结点中。

第八行,反当前结点从链表中移除。

第九行,回收当前结点。

现在关键就是怎么把当前字节写到前驱结点中,Segmentwrite() 方法如下

    // 直接向前驱结点底层数组后写
    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;
    }

理想的方式是直接把当前结点的底层数组直接复制到前驱结点底层数组的后面。 而有时空间是不够的,因为前驱结点的数据起始位置可能不是0的情况,这个时候就需要位移来满足空间要求。

SegmentPool

当我们完全掌握了 Segment 之后,基本上就可以完全掌握 SegmentPool

前面说过,SegmentPoolrecycle() 方法会形成一个单链表,而且结点的插入方式是往插入的。

那么现在看看如何从 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.
    }

如果 next 不为 null,就直接取出来,然后与链表断开。 如果 nextnull,就直接创建一个 Segment

发布了44 篇原创文章 · 获赞 30 · 访问量 400万+

猜你喜欢

转载自blog.csdn.net/zwlove5280/article/details/79806536