Okio之Segment

作者: zYoung_Tang | 来源:发表于2019-09-27 16:29 被阅读0次

    官方解释 Segment

    • Segment 是 buffer 的切割部分.
    • 每个 buffer 中的 Segment 都是循环链表中的节点,持有上一个 Segment 和下一个 Segment 的引用.
    • 每个缓存池中的 Segment 都是单链表中的节点.
    • Segment 底层数组可以被 buffer 和字符串共享.当一个 Segment 是被共享状态时不可以被回收,其字节数据也不可以只读不可写.
      • 唯一例外的是当 owner 为 true 时,数据区间为 [limit,SIZE] 中可以做写操作.
    • 每个数组都有一个 Segment 是持有者.
    • Positions, limits, prev, next 不可以被共享.

    由于 Segment 的代码不是很多,所以直接贴代码和注释.

    // Segment.java
    final class Segment {
        // 定义了 segment 数据字节数最大值为 8kb
        static final int SIZE = 8192;
      
        // SHARE_MINIMUM 是调用 split() 时根据操作字节大小区分使用共享 segment 还是使用数据复制的标准
        static final int SHARE_MINIMUM = 1024;
      
        // 数据
        final byte[] data;
      
        // 标记下一个有效数据字节的下标
        int pos;
      
        // data 可写的下一个字节的下标,也是有效数据的最后一个字节下一个字节的下标
        int limit;
      
        // 是否与其他 segment 共享同一个数组 data,如果为 true data 中 [0,limit] 都是只读不可写的
        boolean shared;
      
        // 是否 data 的所有者,即与 shared 互斥,可以对 data 进行读写
        boolean owner;
      
        // 下一个 segment
        Segment next;
      
        // 上一个 segment
        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 并与当前 segment 使用同一个 data 引用
         */
        Segment sharedCopy() {
          // 设置 shared 为 true 可以防止 segment 被回收
          shared = true;
          return new Segment(data, pos, limit, true, false);
        }
      
        /** 
         * 返回一个新的 segment , data 与当前 segment.data 的克隆
         */
        Segment unsharedCopy() {
          return new Segment(data.clone(), pos, limit, false, true);
        }
      
        /**
         * 把当前 segment 从循环链表中移除,如果移除后链表为空,就返回 null
         */
        public @Nullable Segment pop() {
          // 如果当前 segment 下一个节点就是指向它自己,那么链表只有一个 segment,result 为 null,
          // 且下面两行代码的执行毫无意义.
          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 拆分成两部分,数据范围分别是 [pos, pos+byteCount] [pos+byteCount, limit]
         */
        public Segment split(int byteCount) {
          // byteCount 不可以 <= 0 || byteCount 必须大于有效数据的大小,不然没必要拆分
          if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
          Segment prefix;
      
          // 两个性能指标:
          // - 避免复制操作.可以使用共享 segment 达到目的.
          // - 避免共享数据量小的 segment,这样会在链表中出现一串数据量小的 segment 且他们都是只读,会影响性能.
          // 为了得到平衡,只会在复制操作代价足够大的时候才使用共享 segment
    
          // 当 byteCount 大于 SHARE_MINIMUM 的时候使用共享 segment 操作,
          // 小于 SHARE_MINIMUM 的时候使用数据复制操作
          if (byteCount >= SHARE_MINIMUM) {
            prefix = sharedCopy();
          } else {
            // 从缓存池中获取 segment
            prefix = SegmentPool.take();
            // 把当前 segment 中区间为 [pos,byteCount] 的数据复制到 prefix.data [0,byteCOunt] 中
            System.arraycopy(data, pos, prefix.data, 0, byteCount);
          }
      
          // 设置 prefix 的 limit
          prefix.limit = prefix.pos + byteCount;
          // 当前 segment 的 pos 向后移动 byteCount
          pos += byteCount;
          // 把 prefix 添加到链表中且为当前 segment 的上一个节点
          prev.push(prefix);
          return prefix;
        }
      
        /**
         * 调用该方法,可以在当前节点与它的上一个节点有效数据字节数相加小于 Segemnt.SIZE 的时候
         * 合并成一个 segment,然后回收当前 segment.通常是由尾结点 tail 调用该方法.
         */
        public void compact() {
          if (prev == this) throw new IllegalStateException();
          // 当 prev 是只读的时候不可以合并
          if (!prev.owner) return; 
          // 操作字节数就是当前 segment 的数据大小
          int byteCount = limit - pos;
          // 计算 prev 的可写范围大小
          // 如果 prev 是被共享的 segment,它的可写范围是 [limit,SIZE],不是共享的话范围是 [0,pos],[limit,SIZE]
          int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
          // 如果 prev 可写大小小于当前 segment 数据大小,就不可以合并了
          if (byteCount > availableByteCount) return; 
          // 把当前 segment 数据写进 prev 中
          writeTo(prev, byteCount);
          // 把当前 segment 从循环链表中移除
          pop();
          // 回收当前 segment
          SegmentPool.recycle(this);
        }
      
        // 把当前 segment 数据中的前 byteCount 个字符写进另一个 segment sink 中
        public void writeTo(Segment sink, int byteCount) {
          // 如果 sink 是只读的抛异常
          if (!sink.owner) throw new IllegalArgumentException();
          // 如果忽略 sink 数据区间 [0,pos] 的大小,只拿区间 [limit,SIZE] 与 byteCount 作对比的话,
          // 大于 byteCount 的话可以直接把数据写进 sink 数据区间 [limit, limit+byteCount] 中
          // 小于 byteCount 的话要先把 sink 数据往前移动到 [0,limit-pos] 中,再把数据写进 sink 中
          if (sink.limit + byteCount > SIZE) {
            // 如果 sink 是被共享的 segment ,可以在 limit 之后即区间[0, limit] 都是只读不可写的,所以抛异常
            if (sink.shared) throw new IllegalArgumentException();
            // 如果 sink 可写空间大小小于 byteCount ,抛异常
            if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
            // 移动 sink 数据到 [0,limit-pos] 中
            System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
            // 移动后调整 sink.limit 和 sink.pos
            sink.limit -= sink.pos;
            sink.pos = 0;
          }
          
          // 把当前 segment 的数据写 byteCount 字节到 sink 中
          System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
          // 调整 sink.limit 和当前 pos
          sink.limit += byteCount;
          pos += byteCount;
        }
    }  
    

    总结

    • Segment 是作为 buffer 用来切割数据的存在,通过循环链表的方式把分散的数据连在一起组成 buffer 中的数据.
    • Segment 中存储数据最大容量是 8kb
    • Segment 的 split() 中为了减少调用 System.arraycopy 带来的 CPU 损耗,操作字节数大于 1kb 时用共享 segment 的方式代替复制操作.
    • Segment 的 compact() 可以把两个数据占用率小于 50% 且连续的 segment 合并成一个,减少内存消耗.

    相关文章

      网友评论

        本文标题:Okio之Segment

        本文链接:https://www.haomeiwen.com/subject/spkauctx.html