看本文前,我希望你对链表的操作有个基本的认识,否则你会看得比较痛苦,因为我不会解析链表的操作。
Segment
要想搞明白 okio
的运作机制,Segment
是首先要弄清楚的,Segment
是用作 okio
包下的 Buffer
和 SegmentPool
的结点。
参数说明
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;
}
byte[] data
很明显代表底层字节数组,用来存储字节。int pos
代表从这个Segment
读取时候的起始位置int limit
代表向这个Segment
写数据时候的起始位置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; }
boolean owner
代表当前Segment
是否是底层数组的拥有者。底层数组的第一个创建者就是拥有者,也就是调用无参构造方法创建的才是底层数组的拥有者。Segment next
和Segment prev
代表当前Segment
的前驱结点和后继结点。因此使用Segment
可以构造双链表。
形成链表
Segment
因为有了前驱结点指针和后继结点指针,因此能形成链表。
它帮助 okio
的 Buffer
类形成循环双链表,现在来说明下是如何形成循环双链表的。
当 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());
}
它会调用 Segment
的 push
来形成循环双向链表
public Segment push(Segment segment) {
segment.prev = this;
segment.next = next;
next.prev = segment;
next = segment;
return segment;
}
如图
Segment
还能帮助 SegmentPool
形成一个单链表。 当一个 Segment
从 Buffer
的循环双向链表上移除的时候,就会调用 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
后,如下图
从链表中移除
既然能形成链表,当然就能把结点从链表中移除,okio
的Buffer
类的 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()
Segment
的 split()
方法可以把当前 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;
}
这个功能是在 okio
的 Buffer
中使用的,假如 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 Buffer
的 Segment
全部加入 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();
现在看下 Segment
的 compact()
的代码
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
的字节数量。
第五行,计算了前驱结点可用字节数。
第六行,如果当前结点的字节数大于前驱结点可用的字节数,很显然就不能合并。
第七行,把当前结点的字节写到前驱结点中。
第八行,反当前结点从链表中移除。
第九行,回收当前结点。
现在关键就是怎么把当前字节写到前驱结点中,Segment
的write()
方法如下
// 直接向前驱结点底层数组后写
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
。
前面说过,SegmentPool
的 recycle()
方法会形成一个单链表,而且结点的插入方式是往插入的。
那么现在看看如何从 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
,就直接取出来,然后与链表断开。 如果 next
为 null
,就直接创建一个 Segment
。