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)
blank

相关推荐

发表回复

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

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