Android Okhttp之Okio解析

本文介绍了Okio库在Android应用中的应用,详细解析了其简单易用的文件读写方法及内存优化策略。通过静态方法创建缓冲池和缓冲源,实现代理操作并优化内存和CPU使用,展示了Okio库如何提升应用程序性能。

背景

网络在Android中的重要性那是自然的,Android的HttpUrlconnection和Apach的HttpClientokio已经不能够满足Android的需求,Okio作为okhttp的io组件整个项目的规模不是很大,大神代码的质量也很高,是学习源码的良好素材,也为学习okhhtp奠定一些基础。

okio简单使用

1.1 okio的几种使用
代码开源项目,作者的测试源代码,使用了junit;

public final class OkioTest {
  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
  /**一.简单的文件读写*/
  @Test public void readWriteFile() throws Exception {
  //1.文件
    File file = temporaryFolder.newFile();
  //2.构建写缓冲池
    BufferedSink sink = Okio.buffer(Okio.sink(file));
    //3.向缓冲池写入文本
    sink.writeUtf8("Hello, java.io file!");
    //4.关闭缓冲池
    sink.close();
    /****************割*************************/
   //1.构建读文件缓冲源
    BufferedSource source =       Okio.buffer(Okio.source(file));
    //2.读文件
    source.readUtf8();
    //3.关闭缓冲源
    source.close();
  }
/****************二.文件内容的追*************/
  @Test public void appendFile() throws Exception {
    File file = temporaryFolder.newFile();
    //1.将文件读入,并构建写缓冲池
    BufferedSink sink = Okio.buffer(Okio.appendingSink(file));
    //2.追加文本
    sink.writeUtf8("Hello, ");
    //3.关闭
    sink.close();
    assertTrue(file.exists());
    assertEquals(7, file.length());
    //4.再次追加文本,需要重新构建缓冲池对象
    sink = Okio.buffer(Okio.appendingSink(file));
    //5.追加文本
    sink.writeUtf8("java.io file!");
    //6.关闭缓冲池
    sink.close();
  }
/**************三.通过路径来读写文件****************/
  @Test public void readWritePath() throws Exception {
    Path path = temporaryFolder.newFile().toPath();
   //1.构建写缓冲池
    BufferedSink sink = Okio.buffer(Okio.sink(path));
    //2.写缓冲
    sink.writeUtf8("Hello, java.nio file!");
    //3.关闭缓冲
    sink.close();

   //1.构建读缓冲源
    BufferedSource source = Okio.buffer(Okio.source(path));
    //2.读文本
   source.readUtf8();
   //3.关闭缓冲源
    source.close();
  }
    /********四,写buffer********************/
    //在okio中buffer是一个很重要的对象,在后面我们在详细介绍。
  @Test public void sinkFromOutputStream() throws Exception {
  //1.构建buffer对象
    Buffer data = new Buffer();
    //2.向缓冲中写入文本
    data.writeUtf8("a");
    //3.可以连续追加,类似StringBuffer
    data.writeUtf8(repeat('b', 9998));
    data.writeUtf8("c");

  //4.构建字节数组流对象
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    //5.构建写缓冲池
    Sink sink = Okio.sink(out);
    //6.向池中写入buffer
    sink.write(data, 3);
  }
     /**********五.读buffer***********/
  @Test public void sourceFromInputStream() 
  throws Exception {
  //1.构建字节数组流
    InputStream in = new ByteArrayInputStream(
        ("a" + repeat('b', Segment.SIZE * 2) + "c").getBytes(UTF_8));

    // Source: ab...bc
    //2.缓冲源
    Source source = Okio.source(in);
    //3.buffer
    Buffer sink = new Buffer();
   //4.将数据读入buffer
    sink.readUtf8(3);
  }
}

1.2okio的优势
可以看到,okio的文件读写操作,使用起来是很简单的,减少了很多io操作的基本代码,并且对内存和cpu使用做了优化(代码直观当然是看不出来的,okio作者在buffer类中有详细的说明,后面我们在分析这个类);

okio框架结构与源码分析

1.Okio框架分析
Okio的简单使用,无非下面几个步骤:
a.构建缓冲池、缓冲源对象
b.读、写
c.关闭缓冲对象;

构建缓冲对象

可以看到,直接我我们接触的就是这个类:Okio。Okio,通过:

  public static BufferedSource buffer(Source source) {
throw new IllegalArgumentException("source == null");
    return new RealBufferedSource(source);
  }

  public static BufferedSink buffer(Sink sink) {
    if (sink == null) 
    throw new IllegalArgumentException("sink == null");
    return new RealBufferedSink(sink);
  }

这两个static方法,返回 读、写池:BufferedSource 、BufferedSink ;Source、Sink ,这两种参数从哪里来?*看看Okio下面两个static方法*:

  public static Source source(File file) throws FileNotFoundException {
throw new IllegalArgumentException("file == null");
    return source(new FileInputStream(file));
  }
  /** Returns a sink that writes to {@code file}. */
  public static Sink sink(File file) throws FileNotFoundException {
    if (file == null) 
    throw new IllegalArgumentException("file == null");
    return sink(new FileOutputStream(file));
  }

读、写操作

可以从代码中看到,其实真正的完成读写的类是:RealBufferedSource、RealBufferedSink
切到RealBufferedSink中

final class RealBufferedSink implements BufferedSink {...}
final class RealBufferedSource implements BufferedSource {...}

下面以写操作为例,来查看源码:

  @Override public BufferedSink writeUtf8(String string, int beginIndex, int endIndex)
      throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.writeUtf8(string, beginIndex, endIndex);
    return emitCompleteSegments();
  }

可以看到这里,有一个成员变量buffer,

public final Buffer buffer = new Buffer();

和emitCompleteSegments()完成写的提交;

@Override public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    long byteCount = buffer.completeSegmentByteCount();//buffer来提交
/***sink这一行代码才是真正的io操作。
之前所有的writeXXX()方法,是将,数据保存在链表中,
在这一步中,将数据从链表中不出来;
注意,这里的sink对象,我们看看他是从哪里来的***/
    return this;
  }

在Okio.中buffer(XXX,X)来得到缓冲源、池对象,这里传入了一个Sink对象,这里的Sink就是我们emitCompleteSegments()中真正来完成IO操作的类:

public static BufferedSink buffer(Sink sink) {
    if (sink == null)
     throw new IllegalArgumentException("sink == null");
    return new RealBufferedSink(sink);
  }

通过前面的源码分析,我们知道,真正的缓冲池是:RealBufferedSink。

RealBufferedSink(Sink sink) {
throw new IllegalArgumentException("sink == null");
    this.sink = sink;
  }

这个Sink ,是Okio中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() {
    /*看到没有,NEW  了一个Sink对象,并且实现了下面的几个方法,这就是RealBufferedSink 中emitCompleteSegments() 里,正真的Io操作的实现,过程有些复杂。下面我们贴一张流程图,来展示这个写的过程。*/
      @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);

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

          if (head.pos == head.limit) {
            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 + ")";
      }
    };
  }

Okio写操作流程图

这里写图片描述

OkIO高效在哪里?

在前面我们说,okio对内存和计算量最了优化。所有的写数据操作,其实是:先保存在Segment这里双向链表的用其中,由Buffer对象来直接操作它,最后真正写的时候,由Sink的实现来完成。下面就来分析,Buffer操作链表的这部分代码

public final class Buffer implements BufferedSource, BufferedSink, Cloneable {
.....}

看看,buffer的:writeUtf8(string, beginIndex, endIndex);

@Override public Buffer writeUtf8(String string, int beginIndex, int endIndex) {
    if (string == null) throw new IllegalArgumentException("string == null");
    if (beginIndex < 0) throw new IllegalAccessError("beginIndex < 0: " + beginIndex);
    if (endIndex < beginIndex) {
      throw new IllegalArgumentException("endIndex < beginIndex: " + endIndex + " < " + beginIndex);
    }
    if (endIndex > string.length()) {
      throw new IllegalArgumentException(
          "endIndex > string.length: " + endIndex + " > " + string.length());
    }

    // Transcode a UTF-16 Java String to UTF-8 bytes.
    for (int i = beginIndex; i < endIndex;) {
      int c = string.charAt(i);

      if (c < 0x80) {
        Segment tail = writableSegment(1);
        byte[] data = tail.data;
        int segmentOffset = tail.limit - i;
        int runLimit = Math.min(endIndex, Segment.SIZE - segmentOffset);

        // Emit a 7-bit character with 1 byte.
        data[segmentOffset + i++] = (byte) c; // 0xxxxxxx

        // Fast-path contiguous runs of ASCII characters. This is ugly, but yields a ~4x performance
        // improvement over independent calls to writeByte().
        while (i < runLimit) {
          c = string.charAt(i);
          if (c >= 0x80) break;
          data[segmentOffset + i++] = (byte) c; // 0xxxxxxx
        }

        int runSize = i + segmentOffset - tail.limit; // Equivalent to i - (previous i).
        tail.limit += runSize;
        size += runSize;

      } else if (c < 0x800) {
        // Emit a 11-bit character with 2 bytes.
        writeByte(c >>  6        | 0xc0); // 110xxxxx
        writeByte(c       & 0x3f | 0x80); // 10xxxxxx
        i++;

      } else if (c < 0xd800 || c > 0xdfff) {
        // Emit a 16-bit character with 3 bytes.
        writeByte(c >> 12        | 0xe0); // 1110xxxx
        writeByte(c >>  6 & 0x3f | 0x80); // 10xxxxxx
        writeByte(c       & 0x3f | 0x80); // 10xxxxxx
        i++;

      } else {
        // c is a surrogate. Make sure it is a high surrogate & that its successor is a low
        // surrogate. If not, the UTF-16 is invalid, in which case we emit a replacement character.
        int low = i + 1 < endIndex ? string.charAt(i + 1) : 0;
        if (c > 0xdbff || low < 0xdc00 || low > 0xdfff) {
          writeByte('?');
          i++;
          continue;
        }

        // UTF-16 high surrogate: 110110xxxxxxxxxx (10 bits)
        // UTF-16 low surrogate:  110111yyyyyyyyyy (10 bits)
        // Unicode code point:    00010000000000000000 + xxxxxxxxxxyyyyyyyyyy (21 bits)
        int codePoint = 0x010000 + ((c & ~0xd800) << 10 | low & ~0xdc00);

        // Emit a 21-bit character with 4 bytes.
        writeByte(codePoint >> 18        | 0xf0); // 11110xxx
        writeByte(codePoint >> 12 & 0x3f | 0x80); // 10xxxxxx
        writeByte(codePoint >>  6 & 0x3f | 0x80); // 10xxyyyy
        writeByte(codePoint       & 0x3f | 0x80); // 10yyyyyy
        i += 2;
      }
    }

    return this;
  }
/**
1.文本是被保存到一个Segment 对象里的,Segment 是一个双向链表的数据结构;
2.根据字符的取值范围,划定一定长度的空间来保存字符
3.writeByte是这个写字符的具体实现;
注:双向链表的,插入和读取速度都是不错的,
比数组快。后续数据可追加,而不是使用数组的复制,
数组的复制,无形中增加了CUP的负担。根据字符的取值范围,
来划定空间保存临时数据,提高了内存的使用效率。
*/

切到这个writeByte()方法中来:

@Override public Buffer writeByte(int b) {
    Segment tail = writableSegment(1);//生成一个数据节点
    tail.data[tail.limit++] = (byte) b;//将节点,添加到链表尾部,链表长度++
    size += 1;
    return this;
  }

总结

1.整体架构与设计模式方面:
Okio,使用起来很简单,暴露给用户的就:Okio,BufferSink,BufferSource,Buffer这几个类,根据相面的源码分析,我们大致清楚了,Okio的类图关系如下;
这里写图片描述
可以看到,缓冲池、缓冲源、buffer、Segment都是缓存数据为服务,真正的Io,操作还是入上面流程图所描述由Sink、Source,new的那个对象实现;
要学习的是:
1..1源码作者,提供一个工具了类,OKio来处理,缓存和真正的读写与上图,处理缓存的类,完美解耦;
1.2从上图中看到,BufferSink、bufferSource 、buffer都继承了Sink和Source接口
是的缓存的具体业务,对应性的得到分解,调用时泾渭分明。
3.数据结构方面:
从这个项目中,可以看到,双向链表缓存数据,根据字符取值,给个节点分配不同长度空间来保存数据,节省了空间。双向链表的存取,俱佳,是支持OKio表现高效的核心;

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值