第3章 IO

Okio笔记

一、基本认识

Okio库是一个由square公司开发的,它补充了java.io和java.nio的不足,以便能够更加方便,快速地访问、存储和处理数据。而OkHttp的底层也使用该库作为支持。而在开发中,使用该库可以大大的带来方便。

Okio中有两个关键的接口,SinkSource,这两个接口都继承了Closeable接口;而Sink可以简单的看做OutputStream,Source可以简单的看做InputStream,这两个接口都是支持读写超时设置的。

Okio相关类

它们各自有一个支持缓冲区的子类接口,BufferedSinkBufferedSource,而BufferedSink有一个实现类RealBufferedSink,BufferedSource有一个实现类RealBufferedSource;此外,Sink和Source它门还各自有一个支持gzip压缩的实现类GzipSinkGzipSource;一个具有委托功能的抽象类ForwardingSinkForwardingSource;还有一个实现类便是InflaterSourceDeflaterSink,这两个类主要用于压缩,为GzipSink和GzipSource服务;

BufferedSink中定义了一系列写入缓存区的方法,比如write方法写byte数组,writeUtf8写字符串,还有一些列的writeByte,writeString,writeShort,writeInt,writeLong,writeDecimalLong等等方法。BufferedSource定义的方法和BufferedSink极为相似,只不过一个是写一个是读,基本上都是一一对应的,如readUtf8,readByte,readString,readShort,readInt等等等等。这两个接口中的方法有兴趣的点源码进去看就可以了。

二、简单使用

//简单的文件读写
public void readWriteFile() throws Exception {
    //1.文件
    File file = new File("resources/dest.txt");
    //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();
}
    
//文件内容的追加
public void appendFile() throws Exception {
    File file = new File("resources/dest.txt");
    //1.将文件读入,并构建写缓冲池
    BufferedSink sink = Okio.buffer(Okio.appendingSink(file));
    //2.追加文本
    sink.writeUtf8("Hello, ");
    //3.关闭
    sink.close();

    //4.再次追加文本,需要重新构建缓冲池对象
    sink = Okio.buffer(Okio.appendingSink(file));
    //5.追加文本
    sink.writeUtf8("java.io file!");
    //6.关闭缓冲池
    sink.close();
}

//通过路径来读写文件
public void readWritePath() throws Exception {
    Path path = new File("resources/dest.txt").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是一个很重要的对象,在后面我们在详细介绍。
public void sinkFromOutputStream() throws Exception {
    //1.构建buffer对象
    Buffer data = new Buffer();
    //2.向缓冲中写入文本
    data.writeUtf8("a");
    //3.可以连续追加,类似StringBuffer
    data.writeUtf8("c");

    //4.构建字节数组流对象
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    //5.构建写缓冲池
    Sink sink = Okio.sink(out);
    //6.向池中写入buffer
    sink.write(data, 2);
}

//读Buffer
public void sourceFromInputStream() throws Exception {
    //1.构建字节数组流
    InputStream in = new ByteArrayInputStream(
        ("a"  + "c").getBytes(UTF_8));
    // Source: ac
    //2.缓冲源
    Source source = Okio.source(in);
    //3.buffer
    Buffer sink = new Buffer();
    //4.将数据读入buffer
    sink.readUtf8(2);
}

//Gzip功能
public static void gzipTest(String[] args) {
    Sink sink = null;
    BufferedSink bufferedSink = null;
    GzipSink gzipSink = null;
    try {
        File dest = new File("resources/gzip.txt");
        sink = Okio.sink(dest);
        gzipSink = new GzipSink(sink);
        bufferedSink = Okio.buffer(gzipSink);
        bufferedSink.writeUtf8("android vs ios");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        closeQuietly(bufferedSink);
    }
        
    Source source = null;
    BufferedSource bufferedSource = null;
    GzipSource gzipSource = null;
    try {
        File file = new File("resources/gzip.txt");
        source = Okio.source(file);
        gzipSource = new GzipSource(source);
        bufferedSource = Okio.buffer(gzipSource);
        String content = bufferedSource.readUtf8();
        System.out.println(content);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        closeQuietly(bufferedSource);
    }
}

public static void closeQuietly(Closeable closeable) {
    if (closeable != null) {
        try {
            closeable.close();
        } catch (RuntimeException rethrown) {
            throw rethrown;
        } catch (Exception ignored) {
        }
    }
}

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

此外还有一个ByteString类,这个类可以用来做各种变化,它将byte转会为String,而这个String可以是utf8的值,也可以是base64后的值,也可以是md5的值,也可以是sha256的值,总之就是各种变化,最后取得你想要的值。

三、Okio框架结构与源码分析

Okio框架分析

Okio的简单使用,无非下面几个步骤:
a. 构建对象
b. 读、写
c. 关闭缓冲对象

构建缓冲对象

通过Okio的buffer(Source source)buffer(Sink sink)方法,构建缓冲对象

public static BufferedSource buffer(Source source) {
    return new RealBufferedSource(source);
}
  
public static BufferedSink buffer(Sink sink) {
    return new RealBufferedSink(sink);
}

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

public static Sink sink(File file) throws FileNotFoundException {
    if (file == null) throw new IllegalArgumentException("file == null");
    return sink(new FileOutputStream(file));
}
  
public static Source source(File file) throws FileNotFoundException {
    if (file == null) throw new IllegalArgumentException("file == null");
    return source(new FileInputStream(file));
}

类似的方法还有

  • sink(File file) Sink
  • sink(OutputStream out) Sink
  • sink(Path path, OpenOption... options) Sink
  • sink(Socket socket) Sink
  • source(File file) Source
  • source(InputStream in) Source
  • source(Path path, OpenOption... options) Source
  • source(Socket socket) Source

以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对象,这个对象就是emitCompleteSegments方法中的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);//真正完成写操作!

          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 + ")";
      }
    };
  }
读、写操作

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

@Override 
public BufferedSink writeUtf8(String string, int beginIndex, int endIndex)
      throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.writeUtf8(string, beginIndex, endIndex);//写入到buffer中
    return emitCompleteSegments();//将buffer中的内容写入到sink成员变量中去,然后将自身返回
}

可以看到这里,有一个成员变量buffer,并调用了其writeUtf8方法,源码如下:

@Override 
public Buffer writeUtf8(String string, int beginIndex, int endIndex) {
    ...
    // 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) {
        ...
      }
    }
    return this;
  }
  
  @Override 
  public Buffer writeByte(int b) {
    //返回一个可写的Segment,可以使用的capacity至少为1(一个Segment的总大小为8KB),若当前Segment已经写满了,则会新建一个Segment返回
    Segment tail = writableSegment(1);
    tail.data[tail.limit++] = (byte) b;//将入参放到Segment中
    size += 1;//总长度+1
    return this;
  }

这一切的背后都是一个叫做Buffer的类在支持着缓冲区,Buffer是BufferedSink和BufferedSource的实现类,因此它既可以用来读数据,也可以用来写数据,其内部使用了一个Segment和SegmentPool,维持着一个链表,其循环利用的机制和Android中Message的利用机制是一模一样的。

final class SegmentPool {//这是一个链表,Segment是其元素,总大小为8KB
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  static Segment next;//指向链表的下一个元素

  static long byteCount;

  private SegmentPool() {
  }

  static Segment take() {
    synchronized (SegmentPool.class) {
      if (next != null) {//从链表中取出一个Segment
        Segment result = next;
        next = result.next;
        result.next = null;
        byteCount -= Segment.SIZE;
        return result;
      }
    }
    return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
  }

  static void recycle(Segment segment) {
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    if (segment.shared) return; // This segment cannot be recycled.
    synchronized (SegmentPool.class) {
      if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
      byteCount += Segment.SIZE;//Segment.SIZE固定为8KB
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
}

内部一个成员变量next指向链表下一个元素,take方法首先判断池中是否存在可用的,存在则返回,不存在则new一个,而recycle则是将不再使用的Segment重新扔到池中去。从而达到一个Segment池的作用。

回到writeUtf8方法中,RealBufferedSink的emitCompleteSegments()方法完成写的提交

@Override 
public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    long byteCount = buffer.completeSegmentByteCount();//返回已经缓存的字节数
    if (byteCount > 0) sink.write(buffer, byteCount);//这个sink就是刚才new的
    return this;
}

Okio的写操作流程图
image
image

参考文献

Android 善用Okio简化处理I/O操作
Android Okhttp之Okio解析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容