OKio源码分析(1)six sy007 情感导师,2024年最新PHP计算机毕设源码

fun copy(inputStream: InputStream, dest: File): Boolean {
val source = Okio.buffer(Okio.source(inputStream))
val sink = Okio.buffer(Okio.sink(dest))
val buffer = Buffer()
return try {
var length = source.read(buffer, 8192L)
while (-1L != length) {
sink.write(buffer, length)
sink.flush()
length = source.read(buffer, 8192L)
}
true
} catch (e: Exception) {
e.printStackTrace()
false
} finally {
source.close()
sink.close()
}
}

Okio.source(inputStream)实现了对InputStream的包装,将InputStream包装在Source对象中并返回。

private static Source source(final InputStream in, final Timeout timeout) {

return new Source() {
@Override public long read(Buffer sink, long byteCount) throws IOException {

if (byteCount == 0) return 0;
try {
//超时检查
timeout.throwIfReached();
//从SegmentPool中获取Segment
Segment tail = sink.writableSegment(1);
//根据Segment中可用大小计算最大可以往Segment中写多少字节
int maxToCopy = (int)
Math.min(byteCount, Segment.SIZE - tail.limit);
//从inputStream中将数据写到Segment中
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
//如果读完则返回
if (bytesRead == -1) return -1;
//追加已经写入的数据量,用于下次将数据从limit位置开始写入,也就是limit之前都是写入的数据
tail.limit += bytesRead;
//修正buffer中存储的字节数量
sink.size += bytesRead;
return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}

@Override public void close() throws IOException {
in.close();
}
@Override public Timeout timeout() {
return timeout;
}
@Override public String toString() {
return “source(” + in + “)”;
}
};
}

//Buffer#writableSegment
Segment writableSegment(int minimumCapacity) {

//1
if (head == null) {
head = SegmentPool.take();
return head.next = head.prev = head;
}

//2
Segment tail = head.prev;
if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
tail = tail.push(SegmentPool.take());
}
return tail;
}

  1. 如果链表头为空,则从SegmentPool中获取新节点并指向head结点返回。

  2. 因为是双向循环列表所以head.prev始终获取的是尾结点(当链表长度为1时指向自己),如果tail结点存储数据的容量已满或者tail.owner为false(即该sement不能追加写入)则从SementPool中获取新节点插入到该结点尾部,并返回新节点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Okio.buffer(Okio.source(src))等价于Okio.buffer(source),将source包装在RealBufferedSource(source)内并返回。

val source = Okio.buffer(Okio.source(src))
source.read(buffer,8192L)

执行read((buffer,8192L)),实际是调用的RealBufferedSource.read(buffer,byteCount)。而RealBufferedSource.read(buffer,byteCount)内部又会调用被包装的Source的read(buffer,length)。即我们上面分析过的读数据代码。

//RealBufferedSource#read(buffer,8192L)
public long read(Buffer sink, long byteCount) throws IOException {
if (buffer.size == 0) {
//调用被包装的Source。
//即从InputStream中读取Segment.SIZE个字节写入到buffer中
long read =
source.read(buffer,Segment.SIZE);
//如果未读到数据则返回
if (read == -1) return -1;
}
//读到数据,此时数据保存在buffer中,接下来将buffer写入到sink中。即将一个缓冲区写到另一个缓冲区。
long toRead = Math.min(byteCount, buffer.size);
return buffer.read(sink, toRead);
}

//Buffer#read(sink,toRead)
public long read(Buffer sink, long byteCount) {
if (size == 0) return -1L;
if (byteCount > size) byteCount = size;
//Okio高效的地方就是buffer#write()的实现,后面会详细分析。这里先理解为将buffer中的数据写入到外部传递进来的sink中
sink.write(this, byteCount);
return byteCount;
}

Okio.sink(dest)将File转换为OutPutStream,然后包装在Sink对象中。

public static Sink sink(OutputStream out) {
return sink(out, new Timeout());
}

private static Sink sink(final OutputStream out, final Timeout timeout) {

return new Sink() {
@Override public void write(Buffer source, long byteCount) throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {
//超时检测
timeout.throwIfReached();
//获取链表头结点
Segment head = source.head;
//计算一次可以读多少字节
int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
//从head中读取数据写入到OutPutStream中
out.write(head.data, head.pos, toCopy);
//修正head读到哪个位置,下次继续从pos位置开始读
head.pos += toCopy;
//递减直到byteCount=0退出循环即表示本次写完
byteCount -= toCopy;
//修正buffer中存储的字节大小
source.size -= toCopy;
//如果该Segment已经读完
if (head.pos == head.limit) {
//从链表中删除head并将head的下个结点赋值给head
source.head = head.pop();
//回收head结点
SegmentPool.recycle(head);
}
}
}
@Override public void flush() throws IOException {
out.flush();
}
@Override public void close() throws IOException {
out.close();
}
@Override public Timeout timeout() {
return timeout;
}
@Override public String toString() {
return “sink(” + out + “)”;
}
};
}

Okio.buffer(Okio.sink(dest))等价于Okio.buffer(Sink),将Sink包装在RealBufferedSink(Sink)内并返回。

val sink = Okio.buffer(Okio.sink(dest))
sink.write(buffer, length)
sink.flush()

执行write(buffer, length)和flush()其实调用的是RealBufferedSink的write(buffer,length)和flush()。RealBufferedSink的write(buffer,length)和flush()最终会调用被包装的Sink的write(buffer,length)和flush()。

//RealBufferedSink#write(Buffer source, long byteCount)
public void write(Buffer source, long byteCount)
throws IOException {
//将source中的数据写入到buffer中,Okio高效的地方就是buffer#write的实现
buffer.write(source, byteCount);
emitCompleteSegments();
}

public BufferedSink emitCompleteSegments() throws IOException {
long byteCount = buffer.completeSegmentByteCount();
//检查缓冲区是否被写满,写满则将数据写入到OutPutStream中。未写满则等到下次写满或调用flush或close时将数据写入到OutPutStream中,起到一个缓冲作用。
if (byteCount > 0) {
//调用被包装的Sink#write(buffer, byteCount)
//将buffer中的数据写入到OutPutStream中
sink.write(buffer, byteCount);
}
return this;
}

public long completeSegmentByteCount() {
long result = size;
if (result == 0) return 0;
Segment tail = head.prev;
if (tail.limit < Segment.SIZE && tail.owner) {
result -= tail.limit - tail.pos;
}
return result;
}

至此Okio的输入到输出基本流程已分析完。根据源码分析可知Okio就是对Java io的一个封装和优化,底层还是使用的InputStream和OutputStream。既然和Java io底层使用一样方式读和写,那么它优势体现在哪里呢?有人可能会说他体现在api的简洁上,结构清晰,链式编程,调用方便。说的对,这算是它的优势,而这优势并不能说服我抛弃Java io而使用它,其实你也可以基于java io封装一套链式编程。它和直接使用Java io的最大优势并不api的简洁上,而是io流拷贝的效率上以及对内存的复用上,下节中会详细介绍。

3.Okio为什么比直接使用Java io更有优势

上节中我们提到Okio和直接使用Java io的最大优势并不api的简洁上,而是io流拷贝的效率上以及对内存的复用上。在说这两个优势之前我们先看看直接使用Java io和Okio从输入到输出都经过哪些步骤。

  • Java io

InputStream --> BufferedInputStream --> 临时byte数组 --> BufferedOutPutStream --> OutPutStream

由此可见Java io从输入到输出流程中出现了临时byte数组。意味着从BufferedInputStream->临时byte数组拷贝一次数据,从临时byte数组->BufferedOutPutStream再拷贝一次数据。

  • Okio

InputStream --> inBuffer --> 临时buffer --> outBuffer --> OutPutStream

看起来中间部分和Java io步骤一样。实则不然,Java io我们刚才说过经历两次拷贝,而Okio中间部分inBuffer ->临时buffer->outBuffer 其实不完全是数据的拷贝。在分析buffer->buffer时我们会详细描述为什么不完全是数据拷贝。buffer->buffer 定义在 Buffer#write(Buffer source, long byteCount) 方法中。根据wirte方法注释看出Okio在实现 buffer->buffer 有两个指标。

此处引入该博主对注释的翻译

  • 不要浪费CPU

    不要浪费CPU即不要到处复制数据,从将整个Segments从一个缓冲区重新分配到另一个缓冲区。

  • 不要浪费内存

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

  • 在缓冲区之间移动片段

    在将一个缓冲区写入另一个缓冲区时,我们更喜欢重新分配整个段,将字节复制到最紧凑的形式。假设我们有一个缓冲区,其中的片段负载为[91%,61%],如果我们要在这上面附加一个负载量为[72%]的单一片段,这样将产生的结果为[91%,61%,72%]。这期间不会进行任何的字节复制操作。(即空间换时间,牺牲内存,提供速度)

    再假设,我们有一个缓冲区负载量为:[100%,2%],并且我们希望将其附加到一个负载量为[99%,3%]的缓冲区中。这个操作将产生以下部分:[100%、2%、99%、3%],也就是说,我们不会花时间去复制字节来提高内存的使用效率,如变成[100%,100%,4%]这样。(即这种情况下Okio不会采取时间换空间的策略,因为太浪费CPU)

    在合并缓冲区时,当相邻缓冲区的合并级别不超过100%时,我们将压缩相邻缓冲区。例如,当我们在[100%,40%]基础上附加[30%,80%]时,结果将会是[100%,70%,80%]。(也就是中间相邻的负载为40%和30%的两个Segment将会被合并为一个负载为70%的Segment)

  • 分割片段

    有时我们只想将source buffer中的一部分写入到sink buffer当中,例如,给定一个sink为 [51%,91%],现在我们想要将一个source为[92%,82%]的前30%写入到这个sink buffer当中。为了简化,我们首先将source buffer转换为等效缓冲区[30%,62%,82%](即拆分Segment),然后移动source的头结点Segment即可,最终生成sink[51%,91%,30%]和source[62%,82%]。

根据上面注释的定义,我们可知在进行buffer数据转移时,根据不同策略执行不同操作以达到CPU和内存之间的平衡,那么来看下buffer转移的代码实现。

//Buffer#write
public void write(Buffer source, long byteCount) {
while (byteCount > 0) {
//如果复制的数据量比原缓冲区已有数据量小
if (byteCount < (source.head.limit - source.head.pos)) {
//获取目标缓冲区尾结点
Segment tail = head != null ? head.prev : null;
//如果目标缓冲区尾结点不为空,并且是数据拥有者即可以追加数据并且目标缓冲区可以存下该数据
if (tail != null && tail.owner
&& (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
//如将[10%]追加到[20%],直接拷贝。最终结果[30%]
//将原缓冲区拷贝到目标缓冲区
source.head.writeTo(tail, (int) byteCount);
source.size -= byteCount;
size += byteCount;
return;
} else {
//如果目标缓冲区尾结点为空即目标缓冲区为空缓冲区 或者不为空但是的空间不足,或者不是持有者,这时就需要把原缓冲区的头结点分割为两个Segment,
//然后将原缓冲区的头指针更新为分割后的第一个Segment, 如[92%, 82%]变成[30%, 62%, 82%]这样
source.head = source.head.split((int) byteCount);
}
}
// 从原缓冲区的链表中移除头结点, 并加入到目标缓冲区的尾结点
Segment segmentToMove = source.head;
long movedByteCount = segmentToMove.limit - segmentToMove.pos;
source.head = segmentToMove.pop();
//如果目标缓冲区为空,则创建链表并将原缓冲区的链表头结点赋值给目标缓冲区结点的头结点
if (head == null) {
head = segmentToMove;
head.next = head.prev = head;
} else {
//目标缓冲区不为空,则向目标缓冲区链表追加原缓冲区结点的头结点。并尝试合并,如[60%,20%]追加[10%]。那么目标缓冲区结点为[60%,20%,10%]。然后合并后为[60%,30%]。
//合并成功回收多余结点以节省空间
Segment tail = head.prev;
tail = tail.push(segmentToMove);
tail.compact();
}
source.size -= movedByteCount;
size += movedByteCount;
byteCount -= movedByteCount;
}
}

根据上面源码分析以及注释来回答为什么Okio比Java io高效。

Java中读写数据一般为了高效我们引入BufferedInputStream和BufferedOutPutStream。这里以BufferedInputStream读写磁盘文件为例分析。在BufferedInputStream中当一次读取的字节数大于缓冲区大小会摒弃缓冲区,直接从磁盘中读取。
如果一次读取的字节数小于缓冲区大小,则先从磁盘中读取缓冲区大小个字节(BufferedInputStream中默认定义为8k)。然后每次从缓冲区读取设置的读取数量。直到缓冲区读完。然后再从磁盘中读取…直到整个磁盘数据读完

而Okio读取时不管你读取的字节长度是否大于缓冲区大小。直接读取8k数据到缓冲区,然后根据你设置的读取大小和当前缓冲区已有数据大小做比较取最小值来进行数据转移。

举个例子

比如数据16K,读取一次到临时变量:

读取大小设置为4k

  • Okio经历0次拷贝,inBuffer->临时buffer,只是分割inBuffer数据,将分割后的数据赋值给临时buffer,只是指针的修改

  • 而Java io读取一次到临时变量经历1次拷贝,即buffer->临时byte数组。

读取大小设置为8k

  • Okio经历0次拷贝,inBuffer->临时buffer,只是指针的修改

  • 而Java io读取一次到临时变量经历0次拷贝,因为大于等于缓冲区大小则直接从磁盘读取即InputStream->临时byte数组。

读取大小设置为16k

  • Okio经历0次拷贝,inBuffer->临时buffer,只是指针的
    修改。但是经历两次read即经历两次指针修改。

  • 而Java io读取一次到临时变量经历0次拷贝,因为大于等于缓冲区大小则直接从磁盘读取即InputStream->临时byte数组。经历一次read,但是浪费内存。

从上面举例说明中可以看出Okio在CPU和内存做了很好的权衡,超过8k就只读8k,减少一次性加载到内存的数据。
没超过8k,数据的复制也只是修改链表指针。

小结:
Okio比直接使用Java io高效得益于它底层对缓冲区的实现结构,将数据的缓冲区定义为链表结构是为了更好从缓冲区到缓冲区数据的移动,即不浪费CPU(不到处复制数据),在内存方面它引入SegmentPool来复用Segment。毕竟直接开辟一个8k的byte[]还是很浪费的。以及对缓冲区链表结点的数据进行压缩处理减少不必要的内存开销。

4.Okio的超时检测

超时机制分为同步检测和异步检测机制,先从简单的开始。
下面以读取数据检测超时为例进行说明。

4.1 同步检测

通过前面分析,调用read()时其实调用了RealBufferedSource#read()。而RealBufferedSource#read()又会调用被包装的Source,即Okio#source()创建的Source的read()。

//Okio#source()返回的Source
private static Source source(final InputStream in, final Timeout timeout) {

return new Source() {
public long read(Buffer sink, long byteCount) throws IOException {

try {
//超时检测
timeout.throwIfReached();

int bytesRead = in.read(tail.data, tail.limit, maxToCopy);

return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}

};
}

//Timeout
public class Timeout {
private boolean hasDeadline;
private long deadlineNanoTime;
private long timeoutNanos;

public Timeout() {
}

public Timeout deadlineNanoTime(long deadlineNanoTime) {
this.hasDeadline = true;
this.deadlineNanoTime = deadlineNanoTime;
return this;
}

public void throwIfReached() throws IOException {
if (Thread.interrupted()) {
throw new InterruptedIOException(“thread interrupted”);
}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数同学面临毕业设计项目选题时,很多人都会感到无从下手,尤其是对于计算机专业的学生来说,选择一个合适的题目尤为重要。因为毕业设计不仅是我们在大学四年学习的一个总结,更是展示自己能力的重要机会。

因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
img
img
img

既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!

由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频

如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
img

学习的一个总结,更是展示自己能力的重要机会。**

因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
[外链图片转存中…(img-ke2qZYeV-1712529613654)]
[外链图片转存中…(img-B8zIbWAt-1712529613654)]
[外链图片转存中…(img-63Hj7ZSO-1712529613654)]

既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!

由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频

如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
[外链图片转存中…(img-BeuOtovB-1712529613654)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值