Okio 源码分析(二)

132 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

Buffer

Buffer 类是 Okio 中最核心并且最丰富的类了,前面分析发现最终的 Source 和 Sink 实现对象中,都是通过该类完成读写操作,而 Buffer 类同时实现了 BufferedSource 和 BufferedSink 接口,因此 Buffer 具备 Okio 中的读和写的所有方法

buffer中的写方法
internal inline fun Buffer.commonWrite(
  source: ByteArray,
  offset: Int,
  byteCount: Int
): Buffer {
  var offset = offset
  //检验参数的合法性
  checkOffsetAndCount(source.size.toLong(), offset.toLong(), byteCount.toLong())
 //计算source要写入的最后一个字节的index
  val limit = offset + byteCount
  while (offset < limit) {
    //获取循环链表尾部的一个segment
    val tail = writableSegment(1)
    //计算最多可写入的字节
    val toCopy = minOf(limit - offset, Segment.SIZE - tail.limit)
    //将source复制到data中
    source.copyInto(
      destination = tail.data,
      destinationOffset = tail.limit,
      startIndex = offset,
      endIndex = offset + toCopy
    )
    //调整写入的起始位置
    offset += toCopy
    //调整尾部segment的limit的位置
    tail.limit += toCopy
  }
  //调整buffer的size大小
  size += byteCount.toLong()
  return this
}

public actual fun ByteArray.copyInto(destination: ByteArray, destinationOffset: Int = 0, startIndex: Int = 0, endIndex: Int = size): ByteArray {
    System.arraycopy(this, startIndex, destination, destinationOffset, endIndex - startIndex)
    return destination
}

写操作内部是调用 System.arraycopy 进行字节数组的复制,这里是写到 tail 对象,也就是循环链表的链尾 Segment 对象当中,而且这里会不断循环的获取链尾Segment对象进行写入

writableSegment最后会进入commonWritableSegment这个方法中

internal inline fun Buffer.commonWritableSegment(minimumCapacity: Int): Segment {
  require(minimumCapacity >= 1 && minimumCapacity <= Segment.SIZE) { "unexpected capacity" }
  //如果头指针为null,则直接在segmentpool中取出一个
  if (head == null) {
    val result = SegmentPool.take() // Acquire a first segment.
    head = result
    result.prev = result
    result.next = result
    return result
  }
  //获取前驱节点
  var tail = head!!.prev
  if (tail!!.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
   // 从SegmentPool中获取一个Segment,插入到循环双链表当前结点的后面
    tail = tail.push(SegmentPool.take()) // Append a new empty segment to fill up.
  }
  return tail
}

这里有个 head 对象,就是 Segment 链表的头结点的引用,这个方法中可以看到如果写的时候头结点head为空,则会调用 SegmentPool.take() 方法从Segment池中获取一个 Segment缓存对象,并以此形成一个双向链表的初始节点:

image.png

这时头结点和尾节点其实是同一个节点,然后取得 head.prev 也就是 tail 尾节点返回,但是如果此时 tail 能写的字节数限制超过了 8k 或者尾节点不是 data 的拥有者,就会调用tail.push(SegmentPool.take()); 也就是再调用一次 SegmentPool.take() 取到 Segment 池中下一个 Segment. 通过 tail. push() 方法插入到循环链表的尾部。

image.png

此时插入的节点会作为新的tail节点返回,下一次获取尾节点的时候就会取到它,每当 tail 进行 push 一次,就会将新 push 的节点作为新的尾节点:

image.png

buffer的读操作
internal inline fun Buffer.commonRead(sink: ByteArray, offset: Int, byteCount: Int): Int {
  checkOffsetAndCount(sink.size.toLong(), offset.toLong(), byteCount.toLong())
  //获取表头
  val s = head ?: return -1
  //计算最多可写入的字节
  val toCopy = minOf(byteCount, s.limit - s.pos)
  //将数据拷贝到链头的data字节数组当中
  s.data.copyInto(
    destination = sink, destinationOffset = offset, startIndex = s.pos, endIndex = s.pos + toCopy
  )
  //调整链头的data数组的起始postion和Buffer的size
  s.pos += toCopy
  size -= toCopy.toLong()
  //pos等于limit的时候,从循环链表中移除该Segment并从SegmentPool中回收复用
  if (s.pos == s.limit) {
    head = s.pop()
    SegmentPool.recycle(s)
  }

  return toCopy
}

读操作内部也是调用 System.arraycopy 进行字节数组的复制,这里是直接对 head 头结点进行读取,也就是说 Buffer 在每次读数据的时候都是从链表的头部进行读取的,如果读取的头结点的 pos 等于 limit, 这里就会调用 s.pop() 将头节点从链表中删除,并返回下一个节点作为新的头结点引用,然后将删除的节点通过 SegmentPool.recycle(s) 进行回收复用

image.png

Buffer 中读的过程就是不断取头结点的过程,而写的过程就是不断取尾节点的过程。

Buffer 之间的数据交换
internal inline fun Buffer.commonWrite(source: Buffer, byteCount: Long) {
  var byteCount = byteCount

  require(source !== this) { "source == this" }
  checkOffsetAndCount(source.size, 0, byteCount)

  while (byteCount > 0L) {
    // 如果 Source Buffer 的头结点可用字节数大于要写出的字节数
    if (byteCount < source.head!!.limit - source.head!!.pos) {
      //取到当前buffer的尾节点
      val tail = if (head != null) head!!.prev else null
      // 如果尾部结点有足够空间可以写数据,并且这个结点是底层数组的拥有者
      if (tail != null && tail.owner &&
        byteCount + tail.limit - (if (tail.shared) 0 else tail.pos) <= Segment.SIZE
      ) {
        //source头结点的数据写入到当前尾节点中,然后就直接结束返回了
        source.head!!.writeTo(tail, byteCount.toInt())
        source.size -= byteCount
        size += byteCount
        return
      } else {
        //如果尾节点空间不足或者不是持有者,这时就需要把 Source Buffer 的头结点分割为两个 Segment,然后将source的头指针更新为分割后的第一个Segment
        source.head = source.head!!.split(byteCount.toInt())
      }
    }

    //从 Source Buffer 的链表中移除头结点, 并加入到当前Buffer的链尾
    val segmentToMove = source.head
    val movedByteCount = (segmentToMove!!.limit - segmentToMove.pos).toLong()
    //移除操作,并移动更新source中的head
    source.head = segmentToMove.pop()
    // 如果当前buffer的头结点为 null,则头结点直接指向source的头结点,初始化双向链表
    if (head == null) {
      head = segmentToMove
      segmentToMove.prev = segmentToMove
      segmentToMove.next = segmentToMove.prev
    } else {
      //否则就把Source Buffer的 head 加入到当前Buffer的链尾
      var tail = head!!.prev
      tail = tail!!.push(segmentToMove)
      tail.compact()//尾节点尝试合并,如果合并成功,则尾节点会被SegmentPool回收掉
    }
    source.size -= movedByteCount
    size += movedByteCount
    byteCount -= movedByteCount
  }
}

将字节数据从 source buffer 的头节点复制到当前buffer的尾节点中,这里主要需要平衡两个相互冲突的目标:CPU 和 内存

  • 不要浪费 CPU(即不要复制全部的数据)。

    复制大量数据代价昂贵。相反,我们更喜欢将整个段从一个缓冲区重新分配到另一个缓冲区。

  • 不要浪费内存。

    Segment作为一个不可变量,缓冲区中除了头节点和尾节点的片段以外,相邻的片段,至少应该保证 50% 以上的数据负载量(指的是 Segment 中的data数据, Okio 认为 data 数据量在 50% 以上才算是被有效利用的)。由于头结点中需要读取消耗字节数据,而尾节点中需要写入产生字节数据,因此头结点和尾节点是不能保持不变性的。