请选择 进入手机版 | 继续访问电脑版

Okio好在哪里?

[复制链接]
为你演绎 发表于 2021-1-1 18:32:55 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题



/   本日科技快讯   /


9月17日,在支付宝开放日运动中,支付宝小步调宣布将与新浪微博在场景、产物以及平台三大层面实现全面互通。微博副总裁田利英体现,微博与支付宝小步调的互通将打造一个多端融合的生态,可以重新勾画用户内容消费服务场景,在完成资源整合的同时毗连更多的服务与优质内容,实现流量更高效地变现。


/   作者简介   /


本篇文章来自老司机MxsQ的投稿,分享了对Okio的明白,相信会对各人有所资助!同时也感谢作者贡献的出色文章。


MxsQ的博客地点:
  https://www.jianshu.com/u/9cf1f31e1d09


/   前言   /


与许多Android小同伴一样,打仗到Okio也是在打仗Okhttp之后。在Okhttp中,每个请求通过拦截链处置惩罚,而Okio则在CallServerInterceptor中,对创建起毗连的请求举行读写。刚好自己对Java原生IO也不熟,就两个一起学了。本篇文章分为三个部门,第一部门先容IO,第二部门扼要先容Java中的IO,第三部门先容Okio。熟悉的部门自行跳过。


/   什么是IO   /


步调与运行时数据在内存中驻留,由CPU负责执行,涉及到数据互换的地方,如磁盘、网络等,就需要IO接口。IO中涉及到输入流 Input Stream 与输出流 Output Stream的概念,用来表达数据从一端,到达另一端的过程。


Input Stream 与 Output Stream 可以以内存作为参照标准,加载到内存的,是输入流,而从内存中输出到别的地方,如磁盘、网络的,则称为输出流。好比File存于磁盘中,步调获取File数据用来举行别的操作,这是将数据读入内存中的过程,所以为输入流。


反之,步调将各种信息生存入File中,是将数据读出内存的过程,所以为输出流;再好比,网络操作,请求从客户端来到服务端,也就是数据从客户端到达了服务端,那么对于客户端,是输出流,对服务端,是输入流,响应则相反。如图:





IO原理





用户态:对于操作系统而言,JVM只是一个用户进程(应用步调),处于用户态空间中,处于用户态空间的进程是不能只能操作底层的硬件(磁盘/网卡)。


系统调用:区别于用户进程调用,系统调用时操作系统级别的api,好比java IO的读取数据过程(使用缓冲区),用户步调发起读操作,导致“syscall read”系统调用,就会把数据搬入到一个buffer中;用户发起写操作,导致“syscal write”系统调用,将会把一个buffer中的数据搬出去(发送到网络中 or 写入到磁盘文件)。


内核态:用户态的进程要访问磁盘/网卡(也就是操作IO),必须通过系统调用,从用户态切换到内核态(中断,trap),才能完成。


局部性原理:操作系统在访问磁盘时,由于局部性原理,操作系统不会每次只读取一个字节(代价太大),而是借助硬件直接存储器存取(DMA)一次性读取一片(一个大概若干个磁盘块)数据。因此,就需要有一个“中间缓冲区”——即内核缓冲区。先把数据从磁盘读到内核缓冲区中,然后再把数据从内核缓冲区搬到用户缓冲区。


用户态于内核态的转化时耗时操作,甚至可能比所要执行的函数执行时间还长,应用步调举行IO操作时,应只管淘汰转换操作。而且由于局部性原理,操作系统度读取数据是整片读取的,假设一片的数据为4096字节,那么0~4096字节范围内的数据,对于操作系统来说,读取时间差异是可以忽略不计的。因此,缓冲区的是为了办理速度不匹配问题。


/   Java原生IO   /


Java步调自然要遵守并使用IO的特点。在Java里,输入流为InputStream的子类,输出流为OutputStream的子类,而且详细的读操作read()与写操作write(),均有详细场景下的详细子类来实现。而涉及到IO操作,就抛不开BufferedInputStream和BufferedOutputStream,前者对应输入流,后者对应输出流,这两个类是流上缓冲区的实现。


假设要将一些自定义的数据写入文件中,那构建出的输出流可能如下:


  
  1. [/code]  new DataOutputStream(new BufferedOutputStream(new FileOutputStream("filePath")));
  2.   
  3. 此中DataOutputStream功能为转译,将数据转换成对应字节,BufferedOutputStream为缓冲,FileOutputStream则为详细输出实现,也就是调用下层API的上层触发点。实际上,输入流与输出流雷同,流的构造涉及装饰模式,这样可以把想要的功能拼装起来。
  4. IO操作涉及到的类有许多,不一一先容,主要看BufferedInputStream与BufferedOutputStream如何实现缓冲功能。
  5. 输入流缓冲 BufferedInputStream
  6. BufferedInputStream的读取操作有:
  7. [list]
  8. [*]read():读取下一个字节
  9. [*]read(byte b[], int off, int len):读取一段数据到b[]中
  10. [/list]
  11. 看读取一段数据,读取下一字节的API自然能明白:
  12.    [code]
复制代码
 // 默认的缓冲区存储数据巨细
    private static int DEFAULT_BUFFER_SIZE = 8192;
    // 存储缓冲区数据
    protected volatile byte buf[];
    // 当前缓冲区的有效数据 = count - pos
    protected int count;
    // 当前缓冲区读的索引位置,在pos前的数据是无效数据
    protected int pos;
    // 当前缓冲区的标志位置,需要共同 mark() 和 reset()使用
    // mark()将pos位置索引到到markpos
    // reset() 将pos值重置为markpos,当再次read()数据时,会从mark()标志的位置开始读取数据
    protected int markpos = -1;
    // 缓冲区可标志位置的最大值
    protected int marklimit;

    public synchronized int read(byte b[], int off, int len)
        throws IOException
    {
        // 获取buf,在流关闭情况下buf被释放
        getBufIfOpen();
        // 查抄要获取的数据(假设有),b[]是否内存得下
        if ((off | len | (off + len) | (b.length - (off + len))) = buf.length) {
            // 要写出的数据大于缓冲区的容量,也不消缓冲区战略

            // 先将缓冲区数据写出
            flushBuffer();
            // 再直接通过输出流out直接将数据写出
            out.write(b, off, len);
            return;
        }
        if (len > buf.length - count) {
            // 要写出的数据大于缓冲区还可写入的容量,将缓冲区数据写出
            flushBuffer();
        }
        // 将要写出的数据写入到缓冲区
        System.arraycopy(b, off, buf, count, len);
        // 更新缓冲区已添加的数据容量
        count += len;
    }

  

当数据大于缓冲区容量时,不使用缓冲战略的原因和与分析写入流雷同,都是尽可能少的举行系统调度,输出流缓冲写出过程可用下图体现。



   
  



flushBuffer()就比力简朴了,触发out输出流写出数据。


  
  1. [/code]   private void flushBuffer() throws IOException {
  2.         if (count > 0) {
  3.             out.write(buf, 0, count);
  4.             count = 0;
  5.         }
  6.     }
  7.   
  8. /   IO缓冲小结   /
  9. IO缓冲区的存在,淘汰了系统调用。也就是说,如果缓冲区能满意读入/写出需求,则不需要举行系统调用,维护系统读写数据的习惯。
  10. 从上面学习的内容来看,不管是读入照旧写出,缓冲区的存在一定涉及copy的过程,而如果涉及双流操作,好比从一个输入流读入,再写入到一个输出流,那么这种情况下,在缓冲存在的情况下,数据走向是:
  11. [list]
  12. [*]-> 从输入流读出到缓冲区
  13. [*]-> 从输入流缓冲区copy到 b[]
  14. [*]-> 将 b[] copy 到输出流缓冲区
  15. [*]-> 输出流缓冲区读出数据到输出流
  16. [/list]     
  17. 上面情况存在冗余copy操作,Okio应运而生。
  18. /   Okio实现   /
  19. 在Okio里,办理了双流操作时,中间数据 b[] 存在冗余拷贝的问题。虽然这不能概括Okio的优点,但却是足够亮眼以及核心的优点。
  20. Okio可以通过:
  21.    
  22.     [code]
复制代码
 implementation("com.squareup.okio:okio:2.4.0")

  

引入,如果已引入Okttp3大概Retrofit,则无需再引入。


Okio使用Segment来作为数据存储手段。Segment 实际上也是对 byte[] 举行封装,再通过各种属性来记载各种状态。在互换时,如果可以,将Segment整体作为数据传授前言,这样就没有详细数据的copy过程,而是互换了对应的Segment引用。Segment的数据结构如下:


  
  1. [/code]  final class Segment {
  2.   // 默认容量
  3.   static final int SIZE = 8192;
  4.   // 最小分享数据量
  5.   static final int SHARE_MINIMUM = 1024;
  6.   // 存储详细数据的数组
  7.   final byte[] data;
  8.   // 有效数据索引起始位置
  9.   int pos;
  10.   // 有效数据索引结束位置
  11.   int limit;
  12.   // 指示Segment是否为共享状态
  13.   boolean shared;
  14.   // 指示当前Segment是否为数据拥有者,与shared互斥
  15.   // 默认构造函数的Segment owner为true,当把数据分享
  16.   // 出去时,被分享的Segment的owner标志为false
  17.   boolean owner;
  18.   // 指向下一个Segment
  19.   Segment next;
  20.   // 指向前一个Segment
  21.   Segment prev;
  22. }
  23.   
  24. 除了用来存储详细数据的byte[]数据外,以 pos ~ limit 来标志有效的数据范围。Segment被涉及成可以被分割,在将Segment分割成两个Segment时,就会举行数据分享,纵然用相同的byte[] 数组,只不过 pos ~ limit 标志差异罢了。在分享否,就需要区分个Segment是owner,哪个Segment是shared,这样,就需要对应的标志举行标志。也不难看出,Segment可以采取双向链表结构举行毗连。这里不妨先看看Segment的分割函数split()。
  25. /   Segment分割   /
  26.    [code]
复制代码
 public Segment split(int byteCount) {
    // byteCount体现要分割出去的数据巨细
    // 如果byteCount大于Segment拥有的有效数据巨细,抛出异常
    if (byteCount  limit - pos) throw new IllegalArgumentException();
    Segment prefix;

    if (byteCount >= SHARE_MINIMUM) {
      // 大于分割阀值 1024,举行数据共享
      // 这个情况 prefix.pos = this.pos
      prefix = new Segment(this);
    } else {
      // 小于分割阀值,从缓存池里调换Segment,将所需数据copy到
      // 新的Segment中,这里就没有使用到共享
      // 这个情况 prefix.pos = 0;
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    // 更新当前Segment.pos与新的Segment.limit
    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    // 将Segment加入到当前Segment节点的后面
    prev.push(prefix);
    return prefix;
  }

  

上面的代码形貌情况可以用下图体现





分割操作视byteCount巨细,有差异选择。byteCount大于阀值时,新建Segment,并与当前的Segment共享byte[]数据,此中,当前Segment的的索引范围为[pos + byteCount] ~ [limit],新的Segmetn索引范围为[pos] ~ [pos + byteCount] ; byteCount小于阀值时,则通过copy操作,将所需数据搬运到新的Segment。


/   Segment缓存池   /


slipt()操作中可以看到缓存池SegmentPool的身影。与大多数缓存池一样,SegmentPool制止的内存的重新分配。SegmentPool存储的巨细为 64 * 1024, Semgent数据存储巨细为 8192,因此最多存下8个Segment。


SegmentPool复用IO操作中分配到的内存,也是得益于Segment的设计,当涉及到多流操作时,效果显着。


取操作为 take(),采取操作为 recycle() ,存储方式为单向链表,这里不多说。


/   Okio中的脚色   /


说了那么多,在看看看Okio中涉及的到脚色


Source和Sink


对应IO中的输入流和输出流,Source的实现类实现需read(Buffer sink, long byteCount) throws IOException; Sink的实现类实现write(Buffer source, long byteCount)。不难猜测,在Okio中以Buffer作为操作前言,可以发挥它的最大优势。


BufferedSource 和 BufferedSink


对应IO中输入流缓冲和输出流缓冲,提供对外的API举行读写操作。


Okio


入口类,工厂类,提供source方法可以得到一个Source输入流,提供sink方法得到一个Sink输出流。两种方法可担当的入参都可为 File、Socket、InputStream / OutputStream。对每个对应的方法举行检察,Okio并没有改变各种Java 输入输出流的对应装饰对象的构造,在构造上,对于涉及到的上面说到的入参,构造起来比力方便。也能看出,Okio并没有筹划改变底层的IO方式,旨在弥补原声IO框架上的不敷。


Segment


这一部门开篇已现对Segment举行了先容。除了先容都的内容外,Segment可以以单链、双链的方式存储。提供了pop()将自己从链中删除,返回下一节点;push()将一个Segment加在自己后面,这两个对于链表的操作不做深入。既然提供了split()方法举行分割,自然也提供了compact()方法Segment举行归并,前提是用来做归并的Segment的剩余容量装得下,也不做深入。


SegmentPoll


复用Segment,前面说过,不赘述。RealBufferedSource,RealBufferdSink为BufferedSource 和 BufferedSink的实现类。


Buffer


Okio使用了Segment作为数据存储的方式,自然要提供对应的缓冲方式来操作Segment,Segment在Buffer中以双向链表形式存在。Buffer则负责此项事务。Buffer也实现了BufferedSource和BufferedSink,这是因在使用Okio提供的输入/输出缓冲时,都需要举行缓冲处置惩罚,均由Buffer来处置惩罚,这样使API对应。


TimeOut


提供超时功能,希望IO能在一定时间内举行完毕,否则视为异常。分两种情况,同步超时和异步超时。




  • 同步超时:在每次读写中判断超时条件,因为处于同步方法,因此当IO发生阻塞时,不能及时响应。
  • 异步超时:用单独的线程监控超时条件,如果IO发生阻塞,而且检测到超时,抛出IO异常,阻塞终止。
  • 这部门也不做深入。


/   缓冲实现   /


假设使用Okio复制一个文件,那么实例代码可能是这样的


  
  1. [/code]     /**
  2.              * 构造带缓冲的输入流
  3.              */
  4.             Source source = null;
  5.             BufferedSource bufferedSource = null;
  6.             source = Okio.source(new File("yourFilePath"));
  7.             bufferedSource = Okio.buffer(source);
  8.             /**
  9.              * 构造带缓冲的输出流
  10.              */
  11.             Sink sink = null;
  12.             BufferedSink bufferedSink = null;
  13.             sink = Okio.sink(new File("yourSaveFilePath"));
  14.             sink = Okio.buffer(sink);
  15.             int bufferSize = 8 * 1024; // 8kb
  16.             // 复制文件
  17.             while (!bufferedSource.exhausted()){
  18.                 // 从输入流读取数据到输出流缓冲
  19.                 bufferedSource.read(
  20.                         bufferedSink.buffer(),
  21.                         bufferSize
  22.                         );
  23.                 // 输出流缓冲写出
  24.                 bufferedSink.emit();
  25.             }
  26.             source.close();
  27.             sink.close();
  28.   
  29. 上面代码中,Okio.source() 和 Okio.sink() , Source 接收的输入流为 FileInputStream, Sink接收输出流为FileOutputStream。Okio.buffer 和 Okio.sink分别返回 RealBufferedSource, 和 RealBufferedSink,Buffer作为这两个类的成员变量存在,在实例化时初始化,这部门代码不贴出。主要看 RealBufferedSource.read()。
  30.    [code]
复制代码

 @Override
 public long read(Buffer sink, long byteCount) throws IOException {
    // 用来接收数据的Buffer 不能为空
    if (sink == null) throw new IllegalArgumentException("sink == null");
    // 读取数据不能为负数
    if (byteCount 
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )