前言
前篇《玩一玩OkHttp缓存源码》分析OkHttp缓存源码的时候涉及到缓存的读写,而OkHttp底层采用Okio实现,所以特地写了一篇文章介绍Okio,也算自己的一个总结。我们现在主要知道它是怎么一个工作过程,至于架构之美有点难度,即使你总结出Okio的架构,运用自如,举一反三还是很难。很多优秀框架亦是如此,最重要的还是创意。
分析
废话不多说,老规矩,从实际到理论,不太喜欢上来就直接架构分析,所以跟着我的思路一边阅读文章一边阅读源码更佳。可能一开始比较懵逼,但是过了一遍再过一遍时就很清晰了。由于读和写是一个反过程,因此我们这里只以写为例。
数据写入
如果我们想在一个文件里写一点内容,那么可以这样做:
1 | new Thread(() -> { |
OK,我们一步步分析,首先我们得知道要写入的文件test,这个比较好理解。然后把文件test传入了Okio的静态方法sink。不太懂?那就看源码,源码是最好的老师。
1 | //Okio |
即使特例Socket我们看代码也会调用sink(final OutputStream out, final Timeout timeout)。只不过timeout再包装一次,Socket我们先不讲,放在超时机制一起讲。
那么最终的问题都指向两个点:
- Sink是什么玩意
- sink(final OutputStream out, final Timeout timeout)干了什么事
我们一个一个来解决,从我们出发点是写入不难猜出Sink是关于输出的。我们具体看看是怎么回事:
1 | /** |
我们现在大致知道Sink是一个写入的接口。同时引入一个新的概念Buffer。暂且记下。
我们再来看看第二个问题:
1 | //Okio |
除了write方法相对好理解,而write方法中Buffer卡主了我们,从头到尾都在操作Buffer。看来不理解是不行了,简单推理一下,write是我们返回Sink对象的一个方法,那么拥有这个Sink对象的类肯定会调用write方法。我们再回到用例。
1 | BufferedSink bufferedSink = Okio.buffer(Okio.sink(test)); |
刚刚我们简单过了一下Okio的sink方法,显然返回的Sink对象以Okio的buffer方法参数身份传入。
1 | //Okio |
首先分析下返回的是BufferedSink,了解下整体结构。
1 | public interface BufferedSink extends Sink, WritableByteChannel { |
表示看注释也是懵逼的,我们回到buffer方法,然后看具体实现类。
1 | final class RealBufferedSink implements BufferedSink { |
我们看到我们想要的Buffer,进去参观一下。
1 | public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel { |
Buffer实现了BufferedSource和BufferedSink,从我们分析来看BufferedSink的api拥有一堆的write方法,那么猜测是负责写入的类,反之BufferedSource就是负责读取的类,同时实现。那么我们这里大胆猜测Buffer这个类是真正的幕后操手且同时支持读写操作,这样设计应该是为了避免逻辑的重复吧,毕竟读写只是一个反过程。
既然实现这个两接口,那下面必然是一堆实现方法,我们先不看。到目前为止我们已知信息还是很少,因为我们只分析了类的创建,那么只能了解类的结构和一些注释信息,最终还是要回到例子,分析具体的写操作。
1 | bufferedSink.writeUtf8("不知道写啥"); |
从我们上面的分析可知,BufferedSink的实现类是RealBufferedSink,那么我们直接看RealBufferedSink的writeUtf8方法。
1 | // RealBufferedSink |
果不其然,buffer是幕后操手。OK,那么我们去看看buffer的writeUtf8方法。
1 | // Buffer |
靠,真的是一环接一环啊,貌似不知道Segment是没法玩了,还记得我们在Buffer成员变量记下的Segment吗?不管怎么样,我们必须知道这玩意是干什么的。
1 | //buffer的一段? |
从Segment的数据结构,我们可以了解到Segment的是一个双向链表结构,其次它允许数据共享,最后它可以防止数据的污染而可以设定不允许写入。这是我们现在的已知信息,我们再回到原来的代码处。
1 | // Buffer |
继续追踪writableSegment方法。
1 | // Buffer |
呵呵,还玩不玩?不过这个看名字就懂啥意思,应该是Segment池,联想一下我们熟悉的线程池。我们进去看看。
1 | final class SegmentPool { |
SegmentPool相对较为简单,我们很快就了解了。OK,我们再回过头来看,writableSegment方法。
1 | // Buffer |
OK,这里我们可能不太明白的就是Segment是如何插入的,索性把取出代码一起分析了,毕竟是成对的。
1 | // Segment |
我们再往上翻,回到写入字节的方法。
1 | // Buffer |
貌似结束了?好像不是,到这里毕竟只是写入Buffer。而真正拥有流对象的是Sink。我们再往回退,回到RealBufferedSink的writeUtf8方法。
1 | // RealBufferedSink |
buffer的writeUtf8方法,我们已经玩的很6了,buffer的主要作用就是把数据存储在自己的head循环双向链表中。我们再来看看emitCompleteSegments方法。
1 | // RealBufferedSink |
那么只剩下最后一句了sink.write(buffer, byteCount),还记得sink的来历吗?不记得没关系,我们再回顾一遍。
1 | BufferedSink bufferedSink = Okio.buffer(Okio.sink(test)); |
可能认真看的同学比较厌恶这样的做法,体谅一下。OK,最终还是回到最初卡主我们的地方。
1 | // Okio$Sink |
到这里差不多了,剩下的flush或者说close底层都是调输出流的对应方法,代码我们也看过了。
我们这里简单总结下我们所分析的okio优点:
- ByteString让我们byte[]与String优雅转换,同时提供了很多字符处理工具
- 数据处理采用链表Segment,扩容简单,SegmentPool支持复用,大大减少了OOM
- Api简洁易用
除此之外,在内存方面做得更优秀,Segment提供压缩和共享功能。其次很重要的一点okio支持超时机制,这一点下一节讲。最后它还提供了很多扩展类,例如GzipSink。我先把Segment的压缩和共享和大家说一说。
这两种功能现在放在写入一个Buffer的时候。具体方法解析:
1 | // Segment 压缩 |
算了,算了,再给大家讲解一下一个Segment写入到另一个Segment。
1 | public final void writeTo(Segment sink, int byteCount) { |
关于数据写入到这里就结束了,关于写入的核心逻辑我们都已经分析,读取只不过是个反过程,我相信你没有问题。
超时机制
超时机制也是Okio的一大亮点,同时它分为同步和异步超时。
同步超时
我们先整理下关于同步超时的线索。
1 | // Okio |
根据我们已知线索说明BufferedSink会调用sink的timeout方法进行操作。但如果看得仔细的话,会发现BufferedSink接口其实是继承Sink接口的,而Sink接口是有timeout方法,不出意外的话BufferedSink实现这个方法并把sink的timeout传入。
1 | // BufferedSink |
简单的推理。然后就没然后了,应该BufferedSink中压根没用,只是单纯实现这个接口。那么,按照这个推理Buffer肯定也实现了这个方法。
1 | // Buffer |
其实也然并卵。单单只是实现这个方法,不然报错,Do you know?
那么只剩下这个点了。
1 | // Okio$Sink |
说了半天,我们还没了解Timeout这个类。
1 | public class Timeout { |
通过刚才那分析,我们的例子永远都不会超时。。。也就是说并没有默认的超时机制。
这里补充一点关于System的nanoTime方法。该方法返回一个精确的纳秒级别的时间戳,但并不同System.currentTimeMillis()是一个相对某个时刻的绝对时间,因此该方法多用于计算时间差。
Timeout还有一个方法是waitUntilNotified,内部用的是Object的wait,再结合方法名,不难猜出是干嘛的。
异步超时
还记得我们篇头说的Socket吗?我们说好的要和超时机制一起讲的,那么来吧。
1 | // Okio |
OK,我们首先还是去了解一下AsyncTimeout。
1 | public class AsyncTimeout extends Timeout { |
简单了解了AsyncTimeout的数据结构,然后我们再去看timeout方法。
1 | // Okio |
行吧,直接new了一个,很粗暴。到此,我们仍然不太了解这个类的作用,因为压根没用到它,那么接下这句应该是关键:
1 | // Okio#sink(Socket socket) |
先来看一下enter方法。
1 | // AsyncTimeout |
OK,接下来就是安排我们的超时规则。
1 | // AsyncTimeout |
这里并没有我们想要的超时规则,那么规则应该是在Watchdog中。
1 | private static final class Watchdog extends Thread { |
我们不太明白timedOut是怎么得到的,得到的timedOut代表什么意思,整个逻辑还是较为模糊。
1 | static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException { |
关于进入超时规则以及Watchdog的运行规则我相信你已经搞清楚了,如果还没搞清楚,我真的无能为力了,这么详细的注释。
这里再说一下exit的逻辑,其实比较简单。代码讲解就一起上了。
1 | final IOException exit(IOException cause) throws IOException { |
到这里,超时机制已经讲解完成了。从源码分析,我们知道默认其实并没有超时机制,我们需要自己去制定,但你得了解它的运行机制。
图解
整体结构
异步超时流程图
粗略的流程图,具体细节最好细读源码讲解。
总结
没啥好说的,square出品,必属精品。关于Okio的优点文中已经总结过了。我非常怀疑square这些大佬是不是都是处女座。比如Retrofit框架,简洁但不简单,牛皮,牛皮。