2024年安卓最全OKio源码分析(1)six sy007 情感导师,2024年最新自学Android

关于面试的充分准备

一些基础知识和理论肯定是要背的,要理解的背,用自己的语言总结一下背下来。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,我能明显感觉到国庆后多了很多高级职位,所以努力让自己成为高级工程师才是最重要的。

好了,希望对大家有所帮助。

接下来是整理的一些Android学习资料,有兴趣的朋友们可以关注下我免费领取方式

①Android开发核心知识点笔记

②对标“阿里 P7” 40W+年薪企业资深架构师成长学习路线图

③面试精品集锦汇总

④全套体系化高级架构视频

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

}

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 {

最后

以前一直是自己在网上东平西凑的找,找到的东西也是零零散散,很多时候都是看着看着就没了,时间浪费了,问题却还没得到解决,很让人抓狂。

后面我就自己整理了一套资料,还别说,真香!

资料有条理,有系统,还很全面,我不方便直接放出来,大家可以先看看有没有用得到的地方吧。

系列教程图片

2020Android复习资料汇总.png

flutter

NDK

设计思想开源框架

微信小程序

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

[外链图片转存中…(img-bADtTfaM-1715749501582)]

[外链图片转存中…(img-CWjLIk40-1715749501582)]

[外链图片转存中…(img-Y6CiKYsH-1715749501582)]

[外链图片转存中…(img-eIbpZoJv-1715749501582)]

[外链图片转存中…(img-ALZhkWHe-1715749501583)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Okio是一个强大的Java I/O库,用于处理输入和输出流的操作。要下载Okio的jar文件,可以按照以下步骤进行: 1. 在网上的Maven仓库或者其它可靠的软件下载网站上搜索"Okio jar"。 2. 找到适合你的项目的Okio jar文件版本。可以根据你的项目需求选择稳定版本或者最新版本。 3. 点击下载链接并选择合适的下载目录。 4. 下载完成后,将jar文件移动到你的项目目录中的lib文件夹(如果没有lib文件夹,则可以创建一个新的lib文件夹)。 5. 打开你的IDE(如Eclipse、IntelliJ IDEA等),导入下载好的Okio jar文件到你的项目中。 6. 在你的代码中导入Okio库的类或者方法,你就可以开始使用Okio库了。 这些步骤将帮助你下载并且集成Okio jar到你的项目中,让你能够使用Okio库的强大功能。请确保下载和使用的是可靠的版本,并充分理解Okio库的使用方式和文档,以便更好地应用到你的项目中。 ### 回答2: OKIO是一个开的轻量级IO库,用于在Java平台上进行高效的IO操作。你可以通过下载OKIO jar包来使用它。 首先,你需要找到OKIO的官方网站或是在Maven中央仓库中搜索OKIO。官方网站通常会提供OKIO的jar包下载链接。 一旦找到下载链接,你可以点击下载按钮来获取OKIO的jar包。下载完成后,你将得到一个以.jar结尾的文件。 接下来,将下载的jar包移动到你的Java项目中合适的位置。通常情况下,你可以将它放在项目的lib目录中。 然后,在你的项目中配置构建工具(例如Maven或Gradle)以引入OKIO的依赖。你可以在构建工具的配置文件中添加OKIO的依赖信息,例如Maven的pom.xml文件或Gradle的build.gradle文件。 在配置文件中添加OKIO的依赖信息后,保存文件并进行构建。构建工具将会自动下载并引入OKIO的jar包到你的项目中。 最后,你可以在你的Java代码中使用OKIO库了。可以通过导入OKIO的类来使用它提供的功能,例如读取和写入文件、缓冲区操作等。 总结来说,OKIO的jar包可以通过下载官方网站提供的链接或在构建工具中引入依赖来获取。下载完成后,将jar包放置在项目中合适的位置,并在配置文件中添加依赖信息。完成这些步骤后,你就可以在你的项目中使用OKIO库了。 ### 回答3: Okio是一个高效的Java I/O库,主要用于处理流和字节。要下载Okio JAR文件,可以按照以下步骤进行操作: 1. 打开浏览器并前往Maven仓库的网站(https://mvnrepository.com/)。 2. 在搜索框中输入“Okio”并点击搜索按钮。 3. 在搜索结果中找到最新版本的Okio库,通常以“okio”开头。单击库的版本号以进入详细信息页面。 4. 在详细信息页面中,您将看到有关该库的信息,包括依赖项和Gradle / Maven坐标。 5. 在坐标部分,您可以找到Gradle和Maven的引用代码。根据您的项目构建工具选择适合您的代码。 - 如果您使用Gradle构建项目,请将Gradle代码复制到项目的build.gradle文件中的dependencies部分。 - 如果您使用Maven构建项目,请将Maven代码复制到项目的pom.xml文件中的dependencies部分。 6. 复制引用代码后,保存并关闭文件。 7. 重新构建和编译项目,您的项目将自动下载并使用Okio JAR文件。 请注意,确保您的网络连接良好,以便从Maven仓库成功下载JAR文件。另外,如果您使用的是集成开发环境(IDE),如IntelliJ IDEA或Eclipse,您可以使用IDE的依赖管理工具来搜索并添加Okio库,这更加方便。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值