Okio之Segment和SegmentPool

Segment

官方解释

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

成员变量

// Segment.java

// segment 数据字节数最大值为 8kb
static final int SIZE = 8192;

/* SHARE_MINIMUM 是调用 split() 时根据操作字节大小(byteCount)判断使用共享 segment 实现还是
   使用数据复制实现的标准 */
static final int SHARE_MINIMUM = 1024;

// 存放数据的字节数组
final byte[] data;

// 下一个可读字节的下标
int pos;

// 第一个可写字节的下标,即最后一个可读数据下标为 limit-1
int limit;

// 是否与其他 segment 持有同一个数组 data 对象
boolean shared;

// 是否为 data 的所有者
//  true  对 data 有读写的权限,可写的范围与 shared 有关
//  false 只读
boolean owner;

// 下一个 segment
Segment next;

// 上一个 segment
Segment prev;
  • 字节数组中有两个特殊区域,分别是已读区域 [pos, limit-1] 和可写区域 [limit, SIZE)
  • shared owner 共同作用限制了 Segment 的读写权限以及可写的范围,下文阅读 writeTo() 会介绍他们的作用.

构造方法

// Segment.java
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 ,data 大小是默认 8k, onwer 为 true, shared 为 false,此时 Segment 没有数据.

  • 有参构造可以构造自定义的 Segment,并设置 data pos limit shared owner 的值.


sharedCopy 和 unsharedCopy

// Segment.java

/**
 * 返回一个新的 segment 并与当前 segment 使用同一个 data 引用,相当于浅克隆
 */
Segment sharedCopy() {
  // 设置当前 segment 的 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 且当前 Segment 和复制品的 pos 和 limit 值相同,两个方法区别:

  • sharedCopy() 被调用时,当前 Segment 和复制得到的 Segment 会持有相同的数组对象 data ,所以两个 Segment 的 shared 都要设置为 true 且复制的 Segment 的 owner 值为 false.
  • unsharedCopy() 被调用时,复制的 Segment 持有的是当前 Segment 数组 data 的深克隆得到的对象,所以他是克隆对象 data 的持有者, owner 为 true 且 shared 为 false.

由此可见只有最初持有 data 的 Segment 是数组的持有者 owner 为 true,其他调用 sharedCopy 复制的 Segment 都为 false.


push

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

在当前 Segment 与上一个节点之间插入一个 Segment 并返回被插入的 Segment.


pop

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 本身就不在循环链表内就返回 null.


writeTo

// Segment.java
public void writeTo(Segment sink, int byteCount) {
  // 1
  if (!sink.owner) throw new IllegalArgumentException();
  // 2
  if (sink.limit + byteCount > SIZE) {
    // 2.1
    if (sink.shared) throw new IllegalArgumentException();
    // 2.2
    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;
}

读取当前 Segment 数据并写入目标 Segment (sink) 中.

  1. 如果 sink.owner 为 false 即 sink 的数据只读不写,直接抛异常.

当前数组 data 可以分为三个部分 [0, pos) [pos, limit) [limit, SIZE) ,第一个区域在非共享状态下是可回收区域,第二第三个就是之前说的可读和可写区域.

  1. 首先判断 sink 的可写区域是否足够:
  • true 如果足够直接从 sink 数组下标 limit 开始写入数据.
  • false 如果不足够可以通过移动数组中的可读数据到 [0,limit-pos) ,把第一个区域的空间回收到可写区域中,但实现该方法需要考虑 sink.shared 的值:
    • true sink 是共享状态(2.1)数据是不可以移动的只能抛异常.
    • false sink 不是共享状态数组数据可移动,但移动之前还需计算判断移动后的可写区域大小是否足够(2.2):
      • false 移动之后还是不够空间,抛异常.
      • true 移动后有足够空间,通过数组复制方式往 sink 中写入数据.

数据通过调用 System.arraycopy 写进目标 Segment 后还需要设置当前 pos 和目标 Segment 的 limit.

该方法还表明了 owner 与 shared 之间的联系:

  • owner = true && shared = true : 数组 [0, limit-1] 范围内只读不可写,可写范围 [limit, SIZE] ,写大小为 SIZE - limit.
  • owner = true && shared = false : 整个数组 data 都是可读可写的,可以把可读数据在数组中随意移动,可写大小为 SIZE - (limit - pos).
  • owner = false 时 shared 只能是 true, Segment 持有的数据不可以做任何修改,只读不写.

split

// Segment.java
public Segment split(int byteCount) {
  // byteCount 不可以 <= 0 || byteCount 必须大于有效数据的大小,不然没必要拆分
  if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
  Segment prefix;

  // 两个性能指标:
  // - 避免复制操作.可以使用共享 segment 达到目的.
  // - 避免共享数据量小的 segment,这样会在链表中出现一串数据量小的 segment 且他们都是只读,会影响性能.
  // 为了得到平衡,只会在复制操作代价足够大的时候才使用共享 segment
  if (byteCount >= SHARE_MINIMUM) {
    prefix = sharedCopy();
  } else {
    prefix = SegmentPool.take();
    System.arraycopy(data, pos, prefix.data, 0, byteCount);
  }

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

split() 以 byteCount 为数据大小分界线,把当前 Segment 一分为二.

从数据内容角度看是把下标区间 [pos, limit) 分为 [pos, pos+byteCount) 和 [pos+byteCount, limit).

分割的实现有两种方法:

  • 调用 sharedCopy 构建新的 Segment 且两个 Segment 共享同一个数组 data 对象,然后设置两个 Segment 的 pos 和 limit.
  • 从缓存池中获取 Segment 并用 System.arraycopy 复制数据,然后设置两个 Segment 的 pos 和 limit.

优点:

  • share 方案可以减少 System.arraycopy 的调用提高了性能,共享 Segment 持有同一个数组 data 对象减少内存消耗.

缺点:

  • share 方案中的 Segment 且非数组持有者都只是可读不可写的,即使是数组持有者可写范围也受到限制,当循环链表中存在大量共享状态且数据量小的 Segment 的时候,这些 Segment 对象会占用过多内存资源.
  • 数组复制方案涉及到底层方法,占用 CPU 资源,操作的字节数越大时性能损耗越明显.

所以 Segment 规定当操作数据大小小于 1k 时用数据复制方案,超过 1k 用共享方案.


compact

// Segment.java
public void compact() {
  if (prev == this) throw new IllegalStateException();
  // 当 prev 是只读的时候不可以合并
  if (!prev.owner) return; 
  // 操作字节数就是当前 segment 的数据大小
  int byteCount = limit - pos;
  // 根据是否共享状态计算 prev 的可写范围大小:
  //    true  共享状态可写范围是 [limit, SIZE),
  //    false 非共享可写范围是 [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 数据到上一个 Segment 中,可以减少循环链表的节点数且尽可能地保证所有节点的数据占用率在 50% 以上.

  • 当前 Segment 的上一个 Segment 最大可写大小 >= 当前 Segment 数据大小的时候,合并这两个 Segment 中的数据到上一个 Segment 中并把当前 Segment 从循环链表中移除然后添加到缓存池.
  • 通常是由链表中的尾结点 tail 调用该方法.

SegmentPool

// SegmentPool.java
/**
 * 无用 Segment 的集合,防止被 GC 和零填充
 * 缓存池是静态单例的保证了线程安全
 */
final class SegmentPool {
  // 缓存池最大容量 64kb
  static final long MAX_SIZE = 64 * 1024; 

  // 指向单链表中下一个节点,就是单链表的头结点
  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;
        // 减去一个 Segment 容量
        byteCount -= Segment.SIZE;
        return result;
      }
    }
    return new Segment(); 
  }

  // 回收一个 Segment
  static void recycle(Segment segment) {
    // segment 必须不存在上一个节点和下一个节点的引用,否则报错
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    // 如果该 segment 是被共享状态,不可以被回收
    if (segment.shared) return; 
    synchronized (SegmentPool.class) {
      // 缓存池已满,return
      if (byteCount + Segment.SIZE > MAX_SIZE) return;
      // byteCount 添加一个 segment 的容量
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
}
  • SegmentPool 通过一个单链表实现,缓存最大容量是 64kb 个(有点多).
  • SegmentPool 不会回收共享状态的 Segment.
  • SegmentPool 只回收指向上一个节点和下一个节点都为 null 的 Segment.
  • SegmentPool 中获取的 Segment 可能保留着上次使用时的数据.

你可能感兴趣的:(Okio之Segment和SegmentPool)