2024年最新OKio源码分析(1)six sy007 情感导师(2),2024-2024历年华为跳动Android面试真题解析

学习分享

在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了

很多朋友不是没有资料,大多都是有几十上百个G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘

如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。

2021最新上万页的大厂面试真题

七大模块学习资料:如NDK模块开发、Android框架体系架构…

只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。

这份体系学习笔记,适应人群:
**第一,**学习知识比较碎片化,没有合理的学习路线与进阶方向。
**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。
**第三,**到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。

由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示 。

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

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

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

Okio中几个重要的类介绍

  • ByteString 是不可变的字节序列。对于字符数据,最基本的就是String。而ByteString就像是String的兄弟一般,它使得将二进制数据作为一个变量值变得容易。这个类很聪明:它知道如何将自己编码和解码为十六进制、base64和utf-8。

  • Segment Segment在Okio中作为数据缓冲的载体,一个Segment的数据缓冲大小为8192,即8k。每一个Segment都有前驱和后继结点,也就是说Sement是一个双向链表链表,准确的来说是一个双向循环链表。读取数据从Segment头结点读取写数据从Segment尾结点写。Okio中引入池的概念也就是源码中SegmentPool的实现。SegmentPool负责Segment创建和销毁,SegmentPool最大可以缓存8个Segment。

  • Buffer 是一个可变的字节序列。像Arraylist一样。得益于它的底层由Segment实现因此你不需要预先设置缓冲区的大小,
    当你将数据从一个缓冲区移动到另一个缓冲区时,它会重新分配Segment的持有关系,而不是跨Segment复制数据。其中Buffer实现了BufferedSource和BufferedSink,同时具读写功能。

  • Sources 类似于java中的InputStream,Source作为Okio中读取数据的顶层接口只提供了简单的api

long read(Buffer sink, long byteCount) throws IOException;
Timeout timeout();
void close() throws IOException;

更多读取api由它的子接口BufferedSource提供,实现类为RealBufferdSource,底层InputStream->Buffer,然后基于Buffer的读取。

  • Sink 类似于java中的OutPutStream,Sink作为Okio中写入数据的顶层接口也只提供了简单的api

void write(Buffer source, long byteCount) throws IOException;
void flush() throws IOException;
Timeout timeout();
void close() throws IOException;

更多写入api由它的子接口BufferedSink提供,
实现类为RealBufferedSink,底层将数据写入到Buffer,再由Buffer写入到OutPutStream中。

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

这里省略了GzipSource,GzipSink,HashingSink,HashingSource…等其他实现Source和Sink的类,只关注主流程。

根据前面介绍和UML图得知,数据的读写在RealBufferedSource和RealBufferedSink中实现

2.Okio读写流程

作为一个简单切入点,这里提供一段Okio实现的输入流写入到指定文件的代码。

/***

  • 将字节输入流写入到指定文件中
  • @return true 写入成功,false 写入失败
    */
    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的超时检测

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

最后

最后这里放上我这段时间复习的资料,这个资料也是偶然一位朋友分享给我的,里面包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

还有 高级架构技术进阶脑图、高级进阶架构资料 帮助大家学习提升进阶,也可以分享给身边好友一起学习。

一起互勉~

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

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

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

度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

还有 高级架构技术进阶脑图、高级进阶架构资料 帮助大家学习提升进阶,也可以分享给身边好友一起学习。

[外链图片转存中…(img-HmkJw47D-1715143311057)]

[外链图片转存中…(img-Wkf7bvZA-1715143311057)]

一起互勉~

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值