Java io 到 Okio,從入門到放棄(精通)

爲什麼要了解io?

io是咱們天天都要使用,可是不多有人關注細節的一個模塊,尤爲對於客戶端的開發來說,幾乎不多直接面向io編程,android rom中提供的基礎方法已經足夠好,基本知足各類需求。至於NIO這種看上去高大上可能只有服務端的同窗纔有瞭解了。可是 隨着你app用戶的增多難免遇到各類各樣奇怪的問題,這個時候你若是熟悉io會對你找bug 修bug 有很大的好處。況且關於io 的代碼你若是能研讀一遍對你自身也是有很大好處的。java

本篇不探討NIO的部分,NIO的部分咱們下篇再說。

java 提供的io操做方法

簡單看一下 java 原生提供的io包 大概有哪些東西,類的關係如何android

實際上看的出來 輸入輸出流的 結果都差很少。編程

簡要分析一下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 就是一個殼嗎bash

而後咱們再來看看這個FilterInputStream 的子類都幹嗎了,咱們選BufferedInputStream 來看看。app

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以及家眷對於裝飾器模式的體現,也以此類推。ui

寫個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);
            }

        }
    }
複製代碼
相關文章
相關標籤/搜索