OKio源码分析(1)six sy007 情感导师

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”);
}

if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
throw new InterruptedIOException(“deadline reached”);
}
}
}

根据代码分析在每次调用read()都会调用timeout#throwIfReached(),结合Timeout类中定义,当调用deadlineNanoTime()设置截止时间,hasDeadline,deadlineNanoTime会被赋值。即throwIfReached()的调用才会起到检查超时作用,也就是同步检测超时机制就是根据时间的流逝来判断是否超时。

4.2 异步检测

异步检测Okio用在对Socket输入流的读取和输出流的写入检测,这里仅以输入流检测为例进行说明。先对异步检测整体设计描述,让我们对整体上有一个
宏观上的认识;不至于在分析源码时抓不住重点,最后再对代码实现进行详细分析。

异步检测整体设计如下:

  • 结构上使用单链表作为检测超时的结构,将超时时间封装到结点中,按照超时时间的升序插入到链表中。也就是马上要过期的结点为头结点的下个结点。而头结点在这里起到了看门狗的作用。所以头节点保持不变。

  • 当开始从socket#inputStream中读取时,启动一个监视线程(Watchdog)。不断的获取头结点的下个结点即被监视的结点并判断是否为空,为空则等待60S,60S后如果还为空则退出监视器;不为空则取出该结点存储的超时时间判断是否超时。如果没超时则等待该结点存储的超时时间,时间到后或者被链表插入操作唤醒,则会走一遍流程;如果超时了则删除该结点,并关闭socket。

  • 整个过程如果没有发生超时,则在读取完后删除被监视的结点。直到监视线程wait()等待设置的时间后发现没有需要监视的结点了,然后退出整个监视线程。或者还在wait()中时,又read()了一次,即链表中添加了新的被监视结点。这时wait被唤醒,唤醒后开始监视新的结点。

下面对代码进行分析,从以Socket创建Source开始

//Okio.source(socket)
public static Source source(Socket socket) throws IOException {
//第一步 创建AsyncTimeout并包装socket,包装是为了在超时是调用timedOut()来关闭socket。AsyncTimeout是Timeout的子类
AsyncTimeout timeout = timeout(socket);
//第二步 创建source并包装socket.getInputStream()
Source source = source(socket.getInputStream(), timeout);
//第三步 创建source并包装第二步的source,也就是当外部调用source.read时,其实调用的是第二步的read,
而第三步的包装是为了在第二步read时多加一层监视
return timeout.source(source);
}

//创建AsyncTimeout并包装socket,AsyncTimeout是Timeout的子类
private static AsyncTimeout timeout(final Socket socket) {
return new AsyncTimeout() {
@Override protected IOException newTimeoutException(@Nullable IOException cause) {

}

@Override protected void timedOut() {
try {
socket.close();
} catch (Exception e) {

}
}
};

}
//第二步 创建source并包装socket.getInputStream()
private static Source source(final InputStream in, final Timeout timeout) {

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

try {

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

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

};
}

//第三步 创建source并包装第二步的source,也就是当外部调用source.read时,
//其实调用的是第二步的read,
//而第三步的包装是为了在read时多加一层超时检测
public final Source source(final Source source) {
return new Source() {
@Override public long read(Buffer sink, long byteCount) throws IOException {
boolean throwOnTimeout = false;
//在enter方法内部启动超时检测
enter();
try {
long result = source.read(sink, byteCount);
throwOnTimeout = true;
return result;
} catch (IOException e) {
throw exit(e);
} finally {
//read执行完毕删除被监视的结点
exit(throwOnTimeout);
}
}

@Override public void close() throws IOException {
boolean throwOnTimeout = false;
try {
source.close();
throwOnTimeout = true;
} catch (IOException e) {
throw exit(e);
} finally {
exit(throwOnTimeout);
}
}

@Override public Timeout timeout() {
return AsyncTimeout.this;
}

};
}

public final void enter() {
if (inQueue) throw new IllegalStateException(“Unbalanced enter/exit”);
long timeoutNanos = timeoutNanos();
boolean hasDeadline = hasDeadline();
if (timeoutNanos == 0 && !hasDeadline) {
return;
}
inQueue = true;
scheduleTimeout(this, timeoutNanos, hasDeadline);
}

private static synchronized void scheduleTimeout(
AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
if (head == null) {
head = new AsyncTimeout();
//启动监视器
new Watchdog().start();
}
long now = System.nanoTime();

node.timeoutAt = now + timeoutNanos;
//按照超时时间升序插入到链表中,头结点后的结点就是即将超时的节点
//还有多长时间就超时了
long remainingNanos = node.remainingNanos(now);
for (AsyncTimeout prev = head; true; prev = prev.next) {
if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
node.next = prev.next;
prev.next = node;
if (prev == head) {
//如果是插入到头节点后,那么唤醒监视器
AsyncTimeout.class.notify();
}
break;
}
}
}

private static final class Watchdog extends Thread {
Watchdog() {
super(“Okio Watchdog”);
setDaemon(true);
}

public void run() {
while (true) {
try {
AsyncTimeout timedOut;
synchronized (AsyncTimeout.class) {
timedOut = awaitTimeout();
if (timedOut == null) continue;
if (timedOut == head) {
head = null;
return;
}
}
//表示读取超时关闭socket
timedOut.timedOut();
} catch (InterruptedException ignored) {
}
}
}
}

static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException {
//根据头结点(看门狗)获取被监视的结点
AsyncTimeout node = head.next;
//如果不存在被监视的结点则等待IDLE_TIMEOUT_MILLIS秒(60s)。
//等待后还是不存在并且时间已经超过了60s,则返回头节点(看门狗)然后退出监视器。
//等待后不为空即存在被监视的结点,则返回null,继续下次循环,循环后node!=null。
if (node == null) {
long startNanos = System.nanoTime();

最后

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

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
轻大家的负担。**

[外链图片转存中…(img-9fRS87nj-1715713825561)]

[外链图片转存中…(img-HxYzFeA7-1715713825563)]

[外链图片转存中…(img-JaReS7tm-1715713825564)]

[外链图片转存中…(img-suEo9SIt-1715713825565)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 28
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值