Java io 到 Okio,从入门到放弃(精通)

为何要了解io?

io是我们每天都要使用,但是很少有人关注细节的一个模块,尤其对于客户端的开发来讲,几乎很少直接面向io编程,android rom中提供的基础方法已经足够好,基本满足各种需求。至于NIO这种看上去高大上可能只有服务端的同学才有了解了。但是 随着你app用户的增多不免遇到各种各样奇怪的问题,这个时候你如果熟悉io会对你找bug 修bug 有很大的好处。何况关于io 的代码你如果能研读一遍对你自身也是有很大好处的。

本篇不探讨NIO的部分,NIO的部分我们下篇再说。

java 提供的io操作方法

简单看一下 java 原生提供的io包 大概有哪些东西,类的关系如何

实际上看的出来 输入输出流的 结果都差不多。

简要分析一下InputStream输入流的结构

首先这是一个抽象类,有一个重要的read 抽象方法 等待他的子类去实现

结合前面的图 我们可以得知,inputstream有很多子类,这里我们重点看一下 一个特殊的子类,FilterInputStream, 因为很容易看出来,InputStream中其他子类就到底了,没有子类的子类了,但是这个FilterInputStream 仍然有不少子类

public class FilterInputStream extends InputStream {
    /**
     * The input stream to be filtered.
     */
    protected volatile InputStream in;


    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

   
    public int read() throws IOException {
        return in.read();
    }

  
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

   
    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }

  
    public long skip(long n) throws IOException {
        return in.skip(n);
    }

   
    public int available() throws IOException {
        return in.available();
    }

   
    public void close() throws IOException {
        in.close();
    }

 
    public synchronized void mark(int readlimit) {
        in.mark(readlimit);
    }

   
    public synchronized void reset() throws IOException {
        in.reset();
    }

   
    public boolean markSupported() {
        return in.markSupported();
    }
}

复制代码

FilterInputStream 这个源码可以看出来,内部就只有一个inputstream 对象,然后所有操作都交给这个inputstream的对象来完成

相当于这个FilterInputStream 就是一个壳吗

然后我们再来看看这个FilterInputStream 的子类都干嘛了,我们选BufferedInputStream 来看看。

public class BufferedInputStream extends FilterInputStream {
public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }
复制代码

这里不深究细节,只需要理解,你看我们之前的FilterInputStream read方法里什么都没做,你传什么is对象进来就用这个对象的read方法,但是BufferedInputStream 不同,BufferedInputStream重写了read方法,使得这个bis对象具有了缓存读入的功能。

相当于bis 把 传进来的is对象给装饰了一下。同理,对于其它的FilterInputStream的子类,其作用也是一样的,那就是装饰一个InputStream,为它添加它原本不具有的功能。OutputStream以及家属对于装饰器模式的体现,也以此类推。

写个demo仔细体会java io中装饰者模式的用法和设计思路

        //随便打开一个本地文件
        final String filePath = "D:\\error.log";


        //InputStream相当于被装饰的接口或者抽象类,FileInputStream相当于原始的待装饰的对象,FileInputStream无法装饰InputStream
        //另外FileInputStream是以只读方式打开了一个文件,并打开了一个文件的句柄存放在FileDescriptor对象的handle属性
        //所以下面有关回退和重新标记等操作,都是在堆中建立缓冲区所造成的假象,并不是真正的文件流在回退或者重新标记
        InputStream inputStream = new FileInputStream(filePath);
        final int len = inputStream.available();//记录一下流的长度
        System.out.println("FileInputStream not support mark and reset:" + inputStream.markSupported());

        System.out.println("---------------------------------------------------------------------------------");

        
        //首先装饰成BufferedInputStream,它提供我们mark,reset的功能
        BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);//装饰成 BufferedInputStream
        System.out.println("BufferedInputStream support mark and reset:" + bufferedInputStream.markSupported());
        bufferedInputStream.mark(0);//标记一下
        char c = (char) bufferedInputStream.read();
        System.out.println("file first char is :" + c);
        bufferedInputStream.reset();//重置
        c = (char) bufferedInputStream.read();//再读
        System.out.println("reset and first char is :" + c);
        bufferedInputStream.reset();

        System.out.println("---------------------------------------------------------------------------------");

复制代码

IO学习到这就足够了吗?

可以体会一下上面的java io 代码,其实java io 为什么要使用 装饰者模式,我也没想明白,粗看起来,这样的代码,分层明确, 但是细细体会一下 这样的设计模式带来的后果就是类太多了。其实对于使用者而言,并不是特别友好。我只想要一杯水,你给我一个大海干啥?尤其对于客户端来说,有没有更加优雅的一种io解决方案呢?有,OKIO

OKIO是什么

Okio库是一个由square公司开发的,它补充了java.io和java.nio的不足,以便能够更加方便,快速的访问、存储和处理你的数据。而OkHttp的底层也使用该库作为支持。见过大多的客户端开发用OKHttp用的飞起,然后在碰到io类的开发时,又回去使用并不太好用的java io,而忽略了OKio这个神器,那这篇文章后半段就带你介绍介绍OKio。(学习OKIO源码对理解java IO 也是有很大好处的

OKIO的结构

可以看出来,这个okio的整体结构还是简洁明了非常简单的,不像java io 我截图都截不下。

(OKio的用法很简单,就不过多做介绍了)

这里写一段通过okio输出数据的代码

        String fileName="C:\\Users\\16040657\\Desktop\\iotest.txt";
        File file= new File(fileName);
        BufferedSink bufferedSink=Okio.buffer(Okio.sink(file));
        bufferedSink.writeString("12345", Charset.defaultCharset());
        bufferedSink.writeString("678910", Charset.defaultCharset());
        bufferedSink.close();
复制代码

可以看一下调用链:

1.生成一个file对象

2.通过OKio.sink的构造方法 生成一个sink 对象,我们把这个对象称之为对象A。

 private Okio() {
    }

    public static BufferedSource buffer(Source source) {
        return new RealBufferedSource(source);
    }

    public static BufferedSink buffer(Sink sink) {
        return new RealBufferedSink(sink);
    }
复制代码

然后把这个对象A 传到OKio的buffer方法里 就返回一个RealBufferedSink 对象B。

最后再对这个B对象进行实际操作。

Sink的简要分析

sink其实就等于java io 体系中的输入流。我们简要分析一下。

public interface Sink extends Closeable, Flushable {
    void write(Buffer var1, long var2) throws IOException;

    void flush() throws IOException;

    Timeout timeout();

    void close() throws IOException;
}
复制代码

可以看出来sink就4个方法,write方法有个参数叫buffer,还有个方法叫flush,很容易才想到这个东西跟缓存是相关的。 接着看她的子类BufferedSink

可以看到这仍然是一个接口,只不过提供了更多的抽象方法而已。

最后我们看看真正的实现类。RealBufferedSink

我们选取其中的一部分代码看看

public final Buffer buffer = new Buffer();
    public final Sink sink;
    boolean closed;

    RealBufferedSink(Sink sink) {
        if(sink == null) {
            throw new NullPointerException("sink == null");
        } else {
            this.sink = sink;
        }
    }

    public Buffer buffer() {
        return this.buffer;
    }

    public void write(Buffer source, long byteCount) throws IOException {
        if(this.closed) {
            throw new IllegalStateException("closed");
        } else {
            this.buffer.write(source, byteCount);
            this.emitCompleteSegments();
        }
    }

    public BufferedSink write(ByteString byteString) throws IOException {
        if(this.closed) {
            throw new IllegalStateException("closed");
        } else {
            this.buffer.write(byteString);
            return this.emitCompleteSegments();
        }
    }
复制代码

可以看出来,这些所谓write的方法真正的执行者 其实就是这个buffer对象而已。看上去很像java io的装饰者模式对吧。

对外暴露了一个类C,但其实类c的这些方法里真正操作的却是另外一个对象B

搞清楚Buffer到底在干嘛

结合上述的分析,我们可以看到,最后的操作都是通过buffer来完成的,我们就来看看这个buffer是怎么完成输出流 这件事的。

public Buffer writeString(String string, int beginIndex, int endIndex, Charset charset) {
        if(string == null) {
            throw new IllegalArgumentException("string == null");
        } else if(beginIndex < 0) {
            throw new IllegalAccessError("beginIndex < 0: " + beginIndex);
        } else if(endIndex < beginIndex) {
            throw new IllegalArgumentException("endIndex < beginIndex: " + endIndex + " < " + beginIndex);
        } else if(endIndex > string.length()) {
            throw new IllegalArgumentException("endIndex > string.length: " + endIndex + " > " + string.length());
        } else if(charset == null) {
            throw new IllegalArgumentException("charset == null");
        } else if(charset.equals(Util.UTF_8)) {
            return this.writeUtf8(string, beginIndex, endIndex);
        } else {
            byte[] data = string.substring(beginIndex, endIndex).getBytes(charset);
            return this.write(data, 0, data.length);
        }
    }
复制代码

可以看到utf-8的数据写入是比较简单的,其他的数据就是直接写成byte 字节流了。

最核心的写入数据方法就在这里了。首先我们来看看这个Segment到底是啥

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package okio;

import javax.annotation.Nullable;
import okio.SegmentPool;

final class Segment {
    //最大长度是8192个byte
    static final int SIZE = 8192;
    //可共享的data数组长度
    static final int SHARE_MINIMUM = 1024;
    //真正存放数据的地方
    final byte[] data;
    //开始和结束的limit
    int pos;
    int limit;
    //是否可以共享,可以避免数据拷贝。增强效率
    boolean shared;
    //是否对自己的data数组有写权限,比如segment b是用segment a来构造出来的,那么a就拥有写入权限,但是b没有。
    boolean owner;
    //看到next 和prev 应该很容易联想到双向链表
    Segment next;
    Segment prev;

    Segment() {
        this.data = new byte[8192];
        this.owner = true;
        this.shared = false;
    }

    Segment(Segment shareFrom) {
        this(shareFrom.data, shareFrom.pos, shareFrom.limit);
        shareFrom.shared = true;
    }

    Segment(byte[] data, int pos, int limit) {
        this.data = data;
        this.pos = pos;
        this.limit = limit;
        this.owner = false;
        this.shared = true;
    }

    @Nullable
    public Segment pop() {
        Segment result = this.next != this?this.next:null;
        this.prev.next = this.next;
        this.next.prev = this.prev;
        this.next = null;
        this.prev = null;
        return result;
    }

    public Segment push(Segment segment) {
        segment.prev = this;
        segment.next = this.next;
        this.next.prev = segment;
        this.next = segment;
        return segment;
    }

    
    //数据共享以后就无法写入了,所以要避免出现存在大片的共享小片段,所以一定要大于1024个byte 才会使用这个共享data
    //数组的提高效率的操作
    public Segment split(int byteCount) {
        if(byteCount > 0 && byteCount <= this.limit - this.pos) {
            Segment prefix;
            if(byteCount >= 1024) {
                prefix = new Segment(this);
            } else {
            //对于pool这样的关键字,我们要有敏感性就是为了防止频繁创造销毁对象造成的cpu抖动,所以可以认为是对象池
                prefix = SegmentPool.take();
                System.arraycopy(this.data, this.pos, prefix.data, 0, byteCount);
            }

            prefix.limit = prefix.pos + byteCount;
            this.pos += byteCount;
            this.prev.push(prefix);
            return prefix;
        } else {
            throw new IllegalArgumentException();
        }
    }
    //时间长了以后一个segment中间可能只有一小段是可以用的,所以这里做压缩,用于将当前的data 放到前面的
    //数据中 然后将自己移出,放入到segmentpool中
    public void compact() {
        if(this.prev == this) {
            throw new IllegalStateException();
        } else if(this.prev.owner) {
            int byteCount = this.limit - this.pos;
            int availableByteCount = 8192 - this.prev.limit + (this.prev.shared?0:this.prev.pos);
            if(byteCount <= availableByteCount) {
                this.writeTo(this.prev, byteCount);
                this.pop();
                SegmentPool.recycle(this);
            }
        }
    }
    public void writeTo(Segment sink, int byteCount) {
        if(!sink.owner) {
            throw new IllegalArgumentException();
        } else {
            if(sink.limit + byteCount > 8192) {
                //如果是共享的就不让写
                if(sink.shared) {
                    throw new IllegalArgumentException();
                }
                //如果将要写入的数据和目前存在的数据加起来大于8192 也不给写
                if(sink.limit + byteCount - sink.pos > 8192) {
                    throw new IllegalArgumentException();
                }
                
                System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
                sink.limit -= sink.pos;
                sink.pos = 0;
            }

            System.arraycopy(this.data, this.pos, sink.data, sink.limit, byteCount);
            sink.limit += byteCount;
            this.pos += byteCount;
        }
    }
}

复制代码

搞清楚segment大概是什么东西以后,我们就可以来看看buffer的write方法到底做了啥了。

    //循环写入数据
 public Buffer write(byte[] source, int offset, int byteCount) {
        if(source == null) {
            throw new IllegalArgumentException("source == null");
        } else {
            Util.checkOffsetAndCount((long)source.length, (long)offset, (long)byteCount);

            Segment tail;
            int toCopy;
            for(int limit = offset + byteCount; offset < limit; tail.limit += toCopy) {
                //拿出一个可用的segment容器来,他的内部就是byte数组也就是data,拿出的原则就是看segment是否有足够的空//间写入
                tail = this.writableSegment(1);
                //计算剩余空间长度
                toCopy = Math.min(limit - offset, 8192 - tail.limit);
                //把byte数组 复制到segmnt的容器中 并且计算索引
                System.arraycopy(source, offset, tail.data, tail.limit, toCopy);
                offset += toCopy;
            }
            //更新buffer的大小
            this.size += (long)byteCount;
            return this;
        }
    }
复制代码

到目前我们就可以稍微捋一捋okio写入数据的流程:

其实就是把bytes数组 往buffer里面写,buffer 里面 是一个双向的segment链表。写入的数据实际上就存放在segment的data数组中 这个data数组当然是bytes数组。

但是目前我们发现写入的数据还是在内存里,在缓存里啊,在哪里真正的输出到我们的硬盘当中的呢?

 public void close() throws IOException {
        if(!this.closed) {
            Throwable thrown = null;
            
            try {
                if(this.buffer.size > 0L) {
                    //这个sink其实就是一开始我们传进去的文件输出流啊。之前所有的操作都在缓存里。
                    //只有在这才是真正的输出到文件里,到硬盘。
                    this.sink.write(this.buffer, this.buffer.size);
                }
            } catch (Throwable var3) {
                thrown = var3;
            }

            try {
                this.sink.close();
            } catch (Throwable var4) {
                if(thrown == null) {
                    thrown = var4;
                }
            }

            this.closed = true;
            if(thrown != null) {
                Util.sneakyRethrow(thrown);
            }

        }
    }
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值