okio源码解析「建议收藏」

okio源码解析「建议收藏」1、为什么要学习okio源码?a)okio是安卓大神JakeWharton之作,大神之作必须是值得学习的。b)okio简单易用,高效。okio是对Javaio、nio的简洁封装,原生的Javaio采用装饰者模式,使用的时候非常繁琐,而相同的操作okio只需短短几行代码就可以搞定,当然除了简单易用之外,okio还是一个非常高效的io库,显著的节省CPU和Memory资源。c)okio

大家好,又见面了,我是你们的朋友全栈君。

1、为什么要学习okio源码?

a)okio是安卓大神JakeWharton之作,大神之作必须是值得学习的。

b)okio简单易用,高效。okio是对Java io、nio的简洁封装,原生的Java io采用装饰者模式,使用的时候非常繁琐,而相同的操作okio只需短短几行代码就可以搞定,当然除了简单易用之外,okio还是一个非常高效的io库,显著的节省CPU和Memory资源。

c)okio是okhttp的io组件。现在okhttp已经被Google采纳,作为Android默认的通信组件,这么牛的io组件难道不值得一学吗?

2、概述

2.1 整体结构

okio源码解析「建议收藏」

上图是okio的整体结构,可以看到整个okio分为四个重要部分,sink(输出,可以理解为是Java中的OutputStream的代理,最终通过OutputStream将byte写入文件等)、Source(输入,可以理解为是Java中的InputStream的代理,最终通过InputStream读取字节)、Timeout是okio中加入的超时机制、SegmentPool是okio中的Segment池,和一般的池的作用是一样的,可以避免重复的创建对象,节省资源。Segment是okio缓存的基本单位。

2.2 整体流程

okio源码解析「建议收藏」

在解析源码之前,一张简明的流程图可以让我们事半功倍,不明白流程就直接扎到源码中,额,这太跟自己过不去了。

注意上述缓存结构中有一个head头结点,没错,Buffer中保存了head节点,因此可以通过head节点完成对整个双端链表的读和写操作。

首先解析一下几个比较重要的概念:

a)source:okio通过source子类读取数据,source子类是对原生Java io/nio的封装,最终还是通过Java InputStream的相关方法读取数据。

b)Buffer:数据缓存可以有效提升性能,缓存的操作主要是通过Buffer类来完成,Buffer类主要包含一系列的readxxx和writexxx方法用于读缓存和写缓存。

c)缓存的数据结构:okio中缓存是一个双端链表的数据结构,Segment是缓存的基本单位,Segment包含了一个Byte数组,这样整个链表既具备了数组又具备了链表的优点,同时为了避免不必要的对象创建,使用了Segment池。

d)Sink:和Source一样,Sink子类也是对Java 原生io/nio的封装,最终通过OutputStream完成数据写入。

以上各交互都是通过操作byte数组进行的。

3、okio的简单使用

 private void okio_write() {
        File file = new File(getExternalCacheDir(), "okio.txt");
        boolean isCreate = false;
        if (!file.exists()) {
            try {
                file.createNewFile();
                isCreate = true;
            } catch (IOException e) {
                e.printStackTrace();
                isCreate = false;
            }
        } else
            isCreate = true;
        if (isCreate) {
            BufferedSink bufferedSink = null;
            try {
                bufferedSink = Okio.buffer(Okio.sink(file));
                bufferedSink.writeUtf8("okio 写入测试");
                bufferedSink.flush();//一定要flush,flush的作用是将Flushes this output stream and forces any buffered output bytes to be written out. 
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (bufferedSink != null) {
                    try {
                        bufferedSink.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private void okio_read() {
        File file = new File(getExternalCacheDir(), "okio.txt");
        BufferedSource bufferedSource = null;
        try {
            bufferedSource = Okio.buffer(Okio.source(file));
            String content = bufferedSource.readUtf8();
            Toast.makeText(OkioActivity.this, content, Toast.LENGTH_SHORT).show();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedSource != null) {
                try {
                    bufferedSource.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
okio的使用非常简单,上述标红的代码就是okio关于读和写的核心代码,是不是很简单

4、源码解析

sink和source中,我们最常使用到的子类是RealBufferedSink&RealBufferedSource,我们最终的读写操作也是通过这个子类完成的,因此解析主要针对这两个子类,其他子类自行品尝。先看一下两个子类的主要构成
okio源码解析「建议收藏」
okio源码解析「建议收藏」

RealBufferedSink&RealBufferedSource的主要功能是通过Buffer(缓存)和Sink或者Source的内部类完成的。
下面的源码解析中会涉及到Segment里面具体的变量等,暂时现将其当作一个黑盒子,只知道他是一个保存数据的Node即可。
4.1、RealBufferedSource之读<注意注释的内容>
@Override public String readString(Charset charset) throws IOException {
    if (charset == null) throw new IllegalArgumentException("charset == null");

    buffer.writeAll(source);//写入缓存
    return buffer.readString(charset);//将缓存内容读取出来
  }

4.1.1、写入缓存

@Override public long writeAll(Source source) throws IOException {
    if (source == null) throw new IllegalArgumentException("source == null");
    long totalBytesRead = 0;
    for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {
      totalBytesRead += readCount;
    }
    return totalBytesRead;
  }
Source代表一个InputStream,通过一个for循环,不断的读取InputStream中的Segment.SIze个byte,并将其保存到缓存中。保存至缓存的过程就发生在read的过程中。

如下所示:

  private static Source source(final InputStream in, final Timeout timeout) {    if (in == null) throw new IllegalArgumentException("in == null");    if (timeout == null) throw new IllegalArgumentException("timeout == null");    return new Source() {      @Override public long read(Buffer sink, long byteCount) throws IOException {        if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);        if (byteCount == 0) return 0;        try {          timeout.throwIfReached();//加入了超时机制:超时或中断抛异常          Segment tail = sink.writableSegment(1);//获取一个Segment,这个Segment来自SegmentPool或者新创建的Segment          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);//从输入流中读取数据byte数据到tail缓存起来,tail是尾节点,数据的插入在尾节点处开始,数据的读取则在头结点处开始。          if (bytesRead == -1) return -1;          tail.limit += bytesRead;//修改Segment的limit          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 + ")";      }    };  }
a)如果超时则会抛出异常。timeout.throwIfReached();
b)未超时,则申请一个用于写入缓存的Segment,该Segment来自于SegmentPool或者重新生成的一个新的Segment,该Segment会被加入到Buffer的tail尾节点中;
c)从inputStream中不断读取数据到tail节点的data数组中缓存起来;
d)完成Segment.Size长度的数据读取,修改状态值,返回已读字节数;

4.1.2、读取缓存内容

  @Override public String readString(long byteCount, Charset charset) throws EOFException {
    checkOffsetAndCount(size, 0, byteCount);
    if (charset == null) throw new IllegalArgumentException("charset == null");
    if (byteCount > Integer.MAX_VALUE) {
      throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
    }
    if (byteCount == 0) return "";

    Segment s = head;
    if (s.pos + byteCount > s.limit) {
      // If the string spans multiple segments, delegate to readBytes().
      return new String(readByteArray(byteCount), charset);
    }

    String result = new String(s.data, s.pos, (int) byteCount, charset);//从缓存中读取,生成需要的内容
    s.pos += byteCount;
    size -= byteCount;

    if (s.pos == s.limit) {//pos是segment中有效数据的开始索引,limit是segment中有效数据的结束索引,如果pos和limit相同,则说明这个Segment读取完成,应读取下一个Segment了
      head = s.pop();
      SegmentPool.recycle(s);//丢入SegmentPool,留待复用
    }

    return result;
  }

读取缓存的内容实际就是遍历双端链表的过程,Buffer中保存了双端链表的头结点,因此通过该头节点,我们可以完成整个链表的遍历。被遍历过的Segment会被放入SegmentPool中,留待复用。

4.2、RealBufferedSink之写(与RealBufferSource过程基本类似,不再详细分析)

 @Override public BufferedSink writeString(String string, Charset charset) throws IOException {    if (closed) throw new IllegalStateException("closed");    buffer.writeString(string, charset);//写入缓存    return emitCompleteSegments();//调用sink,完成io写  }

4.2.1、写入缓存

最终是调用如下方法完成写入缓存操作
@Override public Buffer write(byte[] source, int offset, int byteCount) {
    if (source == null) throw new IllegalArgumentException("source == null");
    checkOffsetAndCount(source.length, offset, byteCount);

    int limit = offset + byteCount;
    while (offset < limit) {
      Segment tail = writableSegment(1);//获取一个Segment

      int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
      System.arraycopy(source, offset, tail.data, tail.limit, toCopy);//将待写入内容的byte数组缓存到Buffer中

      offset += toCopy;
      tail.limit += toCopy;
    }

    size += byteCount;
    return this;
  }

读取缓存,写入文件

 @Override public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    long byteCount = buffer.completeSegmentByteCount();
    if (byteCount > 0) sink.write(buffer, byteCount);//最终调用了okio.sink(...);生成的Sink子类的write方法,完成io写入文件操作
    return this;
  }

继续看一下Sink子类的代码

  private static Sink sink(final OutputStream out, final Timeout timeout) {
    if (out == null) throw new IllegalArgumentException("out == null");
    if (timeout == null) throw new IllegalArgumentException("timeout == null");

    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);
          out.write(head.data, head.pos, toCopy);//将buffer中的byte数组通过OutputStream写入文件

          head.pos += toCopy;
          byteCount -= toCopy;
          source.size -= toCopy;

          if (head.pos == head.limit) {//依次遍历Buffer中的双端链表中的Segment节点
            source.head = head.pop();
            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 + ")";
      }
    };
  }

4.3、Segment

okio源码解析「建议收藏」

Segment是缓存的基本单元,通过byte数组保存数据,通过pos和limit标识有效数据的开始和结束索引,通过split和compat方法完成segment的分裂和合并(一般情况下我们用不到),这是一个优化策略。
下面解析一下Segment源码,首先看一下Segment的Api:
okio源码解析「建议收藏」

关于变量的含义,该部分第一张图已经说的很清楚了。重点看一下各个方法。
push、pop是操作链表的基本方法,不再详述。重点看split和Compat。这两个方法是Segement的优化方法。
public Segment split(int byteCount) {
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix;

    // We have two competing performance goals:
    //  - Avoid copying data. We accomplish this by sharing segments.
    //  - Avoid short shared segments. These are bad for performance because they are readonly and
    //    may lead to long chains of short segments.
    // To balance these goals we only share segments when the copy will be large.
    if (byteCount >= SHARE_MINIMUM) {
      prefix = new Segment(this);
    } else {
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    prev.push(prefix);
    return prefix;
  }

该方法的含义是将双链表的头结点分割成两个Segment,第一个Segment包含从[pos,pos+byteCount]的数据,第二个Segment包含从[pos+byteSegment,limit]的数据。这个方法在将一部分segment从一个Buffer转移到另一个Buffer的时候很有用。

此处有一点很有意思,就是如果byteCount的大小大于SHARE_MINIMUM,则两个Segment共享byte[] data 数组。否则,从Segment中取。
  public void compact() {
    if (prev == this) throw new IllegalStateException();
    if (!prev.owner) return; // Cannot compact: prev isn't writable.
    int byteCount = limit - pos;
    int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
    if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
    writeTo(prev, byteCount);
    pop();
    SegmentPool.recycle(this);
  }

如果tail节点和tail的前一个节点保存的数据量都小于Segment,SIZE/2,则合并两个Segment。


 关于:超时机制及其他,可参考一下博文:
1、OKIO源码分析<Segment的设计智慧>

http://blog.csdn.net/gpwner/article/details/65656341
2、用轻和快定义优雅,Okio框架解析
http://www.jianshu.com/p/ea3ef6d7f01b

3、大概是最完全的Okio源码解析文章
http://www.jianshu.com/p/f033a64539a1
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/137725.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 如何和女生聊天不尬聊_女孩说和我聊天是尬聊

    如何和女生聊天不尬聊_女孩说和我聊天是尬聊大家好呀,我是辣条。写这篇文章的灵感来源于之前和朋友的聊天,真的无力吐槽了,想发适合的表情包怼回去却发现收藏的表情包就那几个,就想着是不是可以爬取一些表情包,再也不用尬聊了。先给大家看看我遇到的聊天最尬的场面:斗图吧图片采集抓取目标工具使用重点内容学习项目思路分析整理需求简易源码分享抓取目标网站:斗图吧工具使用开发环境:win10、python3.7开发工具:pycharm、Chrome工具包:requests、etree重点内容学习1.Q队列储存数据信息2.py多线程使用方法

  • getrealpath()_成语解释1000个

    getrealpath()_成语解释1000个getRealPath详细解释今天在获取路径的时候突然发现request中也有getRealPath这个方法,最后查了查文档,说是request.getRealPath(“”)不推荐使用,已摈弃。getServlet().getServletContext().getRealPath(“/”);可以取代上者,都是取得应用绝对路径。比如,有个servlet叫UploadServlet,它部署在tomcat下面以后的绝对路径如下:“C:\ProgramFiles\apache-tomcat-8.

  • Altium_Protel99SE的使用

    Altium_Protel99SE的使用Protel99SE的使用

  • eWebEditor漏洞分析

    eWebEditor漏洞分析现在eWebEditor在线编辑器用户越来越多,危害就越来越大。首先介绍编辑器的一些默认特征:   默认登陆admin_login.asp   默认数据库db/ewebeditor.mdb   默认帐号admin密码admin或admin888   在baidu/google搜索inurl:ewebeditor   几万的站起码有几千个是具有默认特征的,那么试一下默认后台   htt…

  • hbuilderx 打包_下载hbuilder的方法

    hbuilderx 打包_下载hbuilder的方法下载地址:https://www.dcloud.io/hbuilderx.html1.新建项目2.选择图标3.选择启动图片4.设置配置文件,下面的代码主要是去掉了默认的导航栏和退出时不要显示反馈信息{“global”:{“webviewParameter”:{“titleNView”:{“autoBackButton”:true,”bac

    2022年10月31日
  • 卓见杯”第五届CCPC河南省赛参赛感悟

    卓见杯”第五届CCPC河南省赛参赛感悟

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号