携手创作,共同成长!这是我参与「掘金日新计划 · 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缓存对象,并以此形成一个双向链表的初始节点:
这时头结点和尾节点其实是同一个节点,然后取得 head.prev 也就是 tail 尾节点返回,但是如果此时 tail 能写的字节数限制超过了 8k 或者尾节点不是 data 的拥有者,就会调用tail.push(SegmentPool.take()); 也就是再调用一次 SegmentPool.take() 取到 Segment 池中下一个 Segment. 通过 tail. push() 方法插入到循环链表的尾部。
此时插入的节点会作为新的tail节点返回,下一次获取尾节点的时候就会取到它,每当 tail 进行 push 一次,就会将新 push 的节点作为新的尾节点:
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) 进行回收复用
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% 以上才算是被有效利用的)。由于头结点中需要读取消耗字节数据,而尾节点中需要写入产生字节数据,因此头结点和尾节点是不能保持不变性的。