大概是最完全的Okio源碼解析文章

自從Google官方將OkHttp作為底層的網(wǎng)絡(luò)請(qǐng)求之后,作為OkHttp底層IO操作的Okio也是走進(jìn)開發(fā)者的視野,這個(gè)甚至是取代了java的原生IO庫的存在到底有什么特殊的本領(lǐng)呢?
這篇文章主要是對(duì)Okio的實(shí)現(xiàn)做一個(gè)詳盡的解析,當(dāng)然由于筆者分析中可能有紕漏的地方,也煩請(qǐng)指出,Okio的代碼比較精巧,核心的代碼大約5000行,對(duì)文章不盡興的也可以直接通讀源碼,這樣就能理解的更清晰。
全文較長,這里先放出整體的一個(gè)目錄圖

  • 從Sample開始
  • Sink和Source及其實(shí)現(xiàn)
  • Okio中的超時(shí)機(jī)制
  • Segment和SegmentPool解析
  • 不可變的ByteString
  • 最核心的Buffer解析
  • 后記

那我們先看看Okio到底有什么好用的地方。

從Sample開始

為了展現(xiàn)Okio強(qiáng)大的能力,這里先舉幾個(gè)例子看看Okio是怎么處理IO操作的

讀寫文件

Okio中特有的兩個(gè)類Source,Sink代表的就是傳統(tǒng)的輸入流,和輸出流

  Source source = null;
  BufferedSource bSource = null;
  File file = new File(filename);
  //讀文件
  source = Okio.source(file);
  //通過source拿到 bufferedSource
  bSource = Okio.buffer(source);
  String read = bSource.readString(Charset.forName("utf-8"));

讀文件的步驟就是首先拿到一個(gè)輸入流,Okio中封裝了許多的輸入流統(tǒng)一使用方法重載的source方法轉(zhuǎn)換成一個(gè)source,然后使用buffer方法包裝成BufferedSource,這個(gè)里面提供了流的各種操作,讀String,讀字節(jié)數(shù)組,讀字byte,short等等,甚至是16進(jìn)制的數(shù),這里直接讀出文件的String內(nèi)容,十分的簡單。

 private static void create_writer() {
        String filename = "create.txt";
        boolean isCreate = false;
        Sink sink;
        BufferedSink bSink = null;
        try {
            //判斷文件是否存在,不存在,則新建!
            File file = new File(filename);
            if (!file.exists()) {
                isCreate = file.createNewFile();
            } else {
                isCreate = true;
            }
            //寫入操作
            if (isCreate) {
                sink = Okio.sink(file);
                bSink = Okio.buffer(sink);
                bSink.writeUtf8("1");
                bSink.writeUtf8("\n");
                bSink.writeUtf8("this is new file!");
                bSink.writeUtf8("\n");
                bSink.writeString("我是每二條", Charset.forName("utf-8"));
                bSink.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != bSink) {
                    bSink.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

寫文件的操作使用的是Sink,同樣的將一個(gè)file輸出流包裝成一個(gè)Sink,再通過Okio的buffer方法賦予操作流的各種方法,最后寫入操作也是十分的簡單。

png decode
private static final ByteString PNG_HEADER = ByteString.decodeHex("89504e470d0a1a0a");

public void decodePng(InputStream in) throws IOException {
  BufferedSource pngSource = Okio.buffer(Okio.source(in));

  ByteString header = pngSource.readByteString(PNG_HEADER.size());
  if (!header.equals(PNG_HEADER)) {
    throw new IOException("Not a PNG.");
  }
}

這個(gè)是Okio官方提供了一個(gè)Png圖片的解碼的例子,我們知道一般判斷一個(gè)文件的格式就是依靠前面的校驗(yàn)碼,比如class文件中前面的16進(jìn)制代碼就是以 cafebabe 開頭,同樣的常規(guī)的png,jpg,gif之類的都可以通過前面的魔數(shù)來進(jìn)行判斷文件類型,這里就以一個(gè)圖片輸入流轉(zhuǎn)換成一個(gè)BufferedSource,并且通過 readByteString 方法拿到一個(gè)字節(jié)串 ByteString 這樣就能驗(yàn)證這個(gè)文件是不是一個(gè)png的圖片,同樣的方法也能用在其他文件的校驗(yàn)上。
Okio除了這些外還有很多額外的功能,而且官方也提供了許多包括對(duì)于zip文件的處理,各種MD5,SHA-1.SHA256,Base64之類編碼的處理,如果需要額外的一些操作,也可以自己實(shí)現(xiàn)Sink,Source對(duì)應(yīng)的方法。
看完了例子,就來看看Okio真正的實(shí)現(xiàn)吧

Sink和Source及其實(shí)現(xiàn)

Okio中最重要的兩個(gè)概念當(dāng)屬Sink,Source,先看看這兩個(gè)類的繼承圖


Sink Source UML

Sink代表的輸出流,Source代表的是輸入流,這兩個(gè)基本都是對(duì)稱的,所以就只用一個(gè)來進(jìn)行分析了

public interface Sink extends Closeable,Flushable {

    @Override
    void flush() throws IOException;

    @Override
    void close() throws IOException;

    Timeout timeout();

    void write(Buffer source,long byteCount) throws IOException;
}

Sink中只包括了一些最簡單的方法,以及一個(gè)timeout超時(shí),這個(gè)后面會(huì)講到。真正龐大的寫的方法實(shí)際上都是由繼承這個(gè)接口的另一個(gè)接口中的方法,從上面的UML圖中可以看到整個(gè)繼承鏈

BufferedSink

里面包含大量的寫的接口方法,這個(gè)BufferedSink依然只是一個(gè)接口,實(shí)現(xiàn)這個(gè)接口的類就是 RealBufferedSink

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

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

    @Override
    public Buffer buffer() {
        return buffer;
    }

RealBufferedSink類中有兩個(gè)主要參數(shù),一個(gè)是新建的Buffer對(duì)象,一個(gè)是Sink的對(duì)象。
雖然這個(gè)類叫RealBufferedSink,但是實(shí)際上這個(gè)只是一個(gè)保存Buffer對(duì)象的一個(gè)代理實(shí)現(xiàn),真正的實(shí)現(xiàn)都是在Buffer中實(shí)現(xiàn)的,可以看看這個(gè)類的幾個(gè)例子

  @Override public BufferedSink write(byte[] source) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.write(source);
    return emitCompleteSegments();
  }

  @Override public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.write(source, offset, byteCount);
    return emitCompleteSegments();
  }

可以看到這個(gè)實(shí)現(xiàn)了BufferedSink接口的兩個(gè)方法實(shí)際上都是調(diào)用了buffer的對(duì)應(yīng)方法,對(duì)應(yīng)的RealBufferedSource也是同樣的調(diào)用buffer中的read方法,關(guān)于Buffer這個(gè)類會(huì)在下面詳述,剛才我們看到Sink接口中有一個(gè)Timeout的類,這個(gè)就是Okio所實(shí)現(xiàn)的超時(shí)機(jī)制,保證了IO操作的穩(wěn)定性。

Okio中的超時(shí)機(jī)制

Okio的超時(shí)機(jī)制讓IO不會(huì)因?yàn)楫惓W枞谀硞€(gè)未知的錯(cuò)誤上,Okio的基礎(chǔ)超時(shí)機(jī)制是采用的同步超時(shí)
以輸出流為例,當(dāng)我們用下面的方法包裝流時(shí)

    public static Sink sink(OutputStream out){
        return sink(out,new Timeout());
    }

實(shí)際上調(diào)用了一個(gè)兩個(gè)參數(shù)的sink方法,第二個(gè)參數(shù)就是同步超時(shí)

    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 {
                Util.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);
                    byteCount -= toCopy;
                    source.size += toCopy;
                    head.pos += toCopy;
                    ...
                }
            }
        };
    }

可以看到write方法中實(shí)際上有一個(gè)while循環(huán),在每個(gè)開始寫的時(shí)候就調(diào)用了 timeout.throwIfReached() 方法,這個(gè)方法里面去判斷的時(shí)間是否超時(shí),這很明顯是一個(gè)同步超時(shí)機(jī)制,按序執(zhí)行,同樣的Source也是一樣的操作

    public void throwIfReached() throws InterruptedIOException {
        if (Thread.interrupted()){
            throw new InterruptedIOException("thread interrupted");
        }
        if (hasDeadline && deadlineNanoTime - System.nanoTime() < 0){
            throw new InterruptedIOException("deadline reached");
        }
    }

但是當(dāng)我們看Okio對(duì)于socket的封裝時(shí)

  public static Sink sink(Socket socket) throws IOException {
    if (socket == null) throw new IllegalArgumentException("socket == null");
    AsyncTimeout timeout = timeout(socket);
    Sink sink = sink(socket.getOutputStream(), timeout);
    return timeout.sink(sink);
  }

這里出現(xiàn)了一個(gè) AsyncTimeout 的類,這個(gè)實(shí)際上是繼承于Timeout所實(shí)現(xiàn)的一個(gè)異步超時(shí)類,這個(gè)異步類比同步要復(fù)雜的多,它使用了一個(gè)WatchDog線程在后臺(tái)進(jìn)行監(jiān)聽超時(shí),這里的WatchDog并不是linux中的那個(gè),只是一個(gè)繼承于Thread的一個(gè)類,里面的run方法執(zhí)行的就是核心的超時(shí)判斷,之所以在socket寫時(shí)采取異步超時(shí),這完全是由socket自身的性質(zhì)決定的,socket經(jīng)常會(huì)阻塞自己,導(dǎo)致下面的事情執(zhí)行不了。
AsyncTimeout繼承于Timeout類,可以覆寫里面的timeout方法,這個(gè)方法會(huì)在watchdog的線程中調(diào)用,所以不能執(zhí)行長時(shí)間的操作,否則就會(huì)引發(fā)其他的超時(shí),下面詳細(xì)分析這個(gè)類

    //不要一次寫超過64k的數(shù)據(jù)否則可能會(huì)在慢連接中導(dǎo)致超時(shí)
    private static final int TIMEOUT_WRITE_SIZE = 64 * 1024;

    private static AsyncTimeout head;
    private boolean inQueue;
    private AsyncTimeout next;
    private long timeoutAt;

首先就是一個(gè)最大的寫的值,定義為64K,剛好和一個(gè)Buffer的大小是一樣的,官方解釋是因?yàn)槿绻B續(xù)寫超過這個(gè)數(shù)的字節(jié),那么及其容易導(dǎo)致超時(shí),所以為了限制這個(gè)操作,直接給出了一次能寫的最大數(shù)。
下面兩個(gè)參數(shù)一個(gè)head,next很明顯表明這是一個(gè)單鏈表,timeoutAt則是超時(shí)的時(shí)間。
使用者在操作之前首先要調(diào)用enter方法,這樣相當(dāng)于注冊(cè)了這個(gè)超時(shí)監(jiān)聽,然后配對(duì)的實(shí)現(xiàn)exit方法,這個(gè)exit有一個(gè)返回值會(huì)表明超時(shí)是否觸發(fā),請(qǐng)注意這個(gè)timeout是異步的,可能會(huì)在exit后才調(diào)用

    public final void enter() {
        if (inQueue)
            throw new IllegalArgumentException("unbalanced enter/exit");
        long timeoutNanos = timeoutNanos();
        boolean hasDeadline = hasDeadline();
        if (timeoutNanos == 0 && !hasDeadline) {
            return;  // 沒有超時(shí)的設(shè)置
        }
        inQueue = true;
        scheduleTimeout(this, timeoutNanos, hasDeadline);
    }

這里只是做了判斷以及設(shè)置inQueue的狀態(tài),真正的是調(diào)用 scheduleTimeout 方法來加入到鏈表中

        ...
        long remainingNanos = node.remainingNanos(now);
        for (AsyncTimeout prev = head ; true ; prev = prev.next){
            //如果下一個(gè)為null或者剩余時(shí)間比下一個(gè)短 就插入node
            if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)){
                node.next = prev.next;
                prev.next = node;
                if (prev == node){
                    AsyncTimeout.class.notify();
                }
                break;
            }
        }

上面可以看出這個(gè)鏈表實(shí)際上是按照剩余的超時(shí)時(shí)間來進(jìn)行排序的,快到超時(shí)的節(jié)點(diǎn)排在表頭,依次往后遞增。
我們以一個(gè)read的代碼來看整個(gè)超時(shí)的綁定過程

            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                boolean throwOnTimeout = false;
                enter();
                try {
                    long result = source.read(sink,byteCount);
                    throwOnTimeout = true;
                    return result;
                }catch (IOException e){
                    throw exit(e);
                }finally {
                    exit(throwOnTimeout);
                }
            }

首先調(diào)用enter方法,然后來做讀的操作,這里可以看到不僅在catch上而且在finally中也做了操作,這樣異常和正常的情況都考慮到了,在exit中調(diào)用了真正的exit方法,exit中會(huì)去判斷這個(gè)異步超時(shí)的對(duì)象是否在鏈表中

    final void exit(boolean throwOnTimeout) throws IOException {
        boolean timeOut =  exit();
        if (timeOut && throwOnTimeout)
            throw newTimeoutException(null);
    }

    public final boolean exit(){
        if (!inQueue)
            return false;
        inQueue = false;
        return cancelScheduledTimeout(this);
    }

回到前面所說的WatchDog,內(nèi)部的run方法是一個(gè)while(true)的一個(gè)循環(huán),

             while (true) {
                try {
                    AsyncTimeout timeout;
                    synchronized (AsyncTimeout.class) {
                        timeout = awaitTimeout();
                        //沒有找到一個(gè)node來interrupt 繼續(xù)
                        if (timeout == null){
                            continue;
                        }
                    ...
                    timeout.timedOut();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

這里鎖住了內(nèi)部的awaitTimeout操作,這個(gè)await正是判斷是否超時(shí)的真正地方

    static AsyncTimeout awaitTimeout() throws InterruptedException {
        //拿到下一個(gè)節(jié)點(diǎn)
        AsyncTimeout node = head.next;
        //如果queue為空,等待直到有node進(jìn)隊(duì),或者觸發(fā)IDLE_TIMEOUT_MILLS
        if (node == null) {
            long startNanos = System.nanoTime();
            AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLS);
            return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS ? head : null;
        }
        long waitNanos = node.remainingNanos(System.nanoTime());
        //這個(gè)head依然還沒有超時(shí),繼續(xù)等待
        if (waitNanos > 0) {
            //這里比較奇怪,但是是wait API的需求需要兩個(gè)參數(shù)
            long waitMills = waitNanos / 1000000L;
            waitNanos -= (waitMills * 1000000L);
            AsyncTimeout.class.wait(waitMills, (int) waitNanos);
            return null;
        }
        head.next = node.next;
        node.next = null;
        return node;
    }

代碼中有些注釋已經(jīng)寫得比較清晰了,主要就是通過這個(gè) remainingNanos 來判斷預(yù)定的超時(shí)時(shí)間減去當(dāng)前時(shí)間是否大于0,如果比0大就說明還沒超時(shí),于是wait剩余的時(shí)間,然后表示并沒有超時(shí),如果小于0,就會(huì)把這個(gè)從鏈表中移除,根據(jù)前面exit方法中的判斷就能觸發(fā)整個(gè)超時(shí)的方法,異步超時(shí)這一部分代碼比較復(fù)雜,涉及到許多wait,鏈表,加鎖,需要詳細(xì)閱讀源碼才能理解深刻,不清楚的可以詳細(xì)看看。

Segment和SegmentPool解析

Segment字面翻譯就是片段,Okio將數(shù)據(jù)也就是Buffer分割成一塊塊的片段,同時(shí)segment擁有前置節(jié)點(diǎn)和后置節(jié)點(diǎn),構(gòu)成一個(gè)雙向循環(huán)鏈表,就像下面這個(gè)圖的方式。

Okio的Buffer和segment的關(guān)系

這樣采取分片使用鏈表連接,片中使用數(shù)組存儲(chǔ),兼具讀的連續(xù)性,以及寫的可插入性,對(duì)比單一使用鏈表或者數(shù)組,是一種折中的方案,讀寫更快,而且有個(gè)好處根據(jù)需求改動(dòng)分片的大小來權(quán)衡讀寫的業(yè)務(wù)操作,另外,segment也有一些內(nèi)置的優(yōu)化操作,綜合這些Okio才能大放異彩,后面在Buffer解析會(huì)講解什么時(shí)候形成的雙向循環(huán)鏈表

    static final int SIZE = 8192;
    static final int SHARE_MINIMUM = 1024;
    final byte[] data;
    int pos;
    int limit;
    boolean shared;
    boolean owner;
    Segment pre;
    Segment next;

SIZE就是一個(gè)segment的最大字節(jié)數(shù),其中還有一個(gè)SHARE_MINIMUM,這個(gè)涉及到segment優(yōu)化中的另一個(gè)技巧,共享內(nèi)存,然后data就是保存的字節(jié)數(shù)組,pos,limit就是開始和結(jié)束點(diǎn)的index,shared和owner用來設(shè)置狀態(tài)判斷是否可寫,一個(gè)有共享內(nèi)存的segment是不能寫入的,pre,next就是前置后置節(jié)點(diǎn)。

Segment方法分析

既然是雙向循環(huán)鏈表,其中也會(huì)有一些操作的方法,比如

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

pop方法移除了自己,首先將自己的前后兩個(gè)節(jié)點(diǎn)連接起來,然后將自己的前后引用置空,這樣就脫離了整個(gè)雙向鏈表,然后返回next

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

push方法就是在當(dāng)前和next引用中間插入一個(gè)segment進(jìn)來,并且返回插入的segment,這兩個(gè)都是尋常的雙向鏈表的操作,我們?cè)賮砜纯慈绾螌懭霐?shù)據(jù)

    public void writeTo(Segment sink , int byteCount){
        if (!sink.owner)
            throw new IllegalArgumentException();
        if (sink.limit + byteCount > SIZE){  //limit和需要寫的字節(jié)總和大于SIZE
            if (sink.shared)  //共享無法寫
                throw new IllegalArgumentException();
            if (sink.limit + byteCount - sink.pos > SIZE){  //如果減去頭依然比SIZE大 那么就無法寫拋異常
                throw new IllegalArgumentException();
            }
            //否則我們需要先移動(dòng)要寫的文件地址  然后置limit pos的地址
            System.arraycopy(sink.data,sink.pos,sink.data,0,sink.limit - sink.pos);
            sink.limit = sink.limit - sink.pos;
            sink.pos = 0;
        }
        //開始尾部寫入 寫完置limit地址
        System.arraycopy(data,pos,sink.data,sink.limit,byteCount);
        sink.limit = sink.limit + byteCount;
        pos = pos + byteCount; //當(dāng)前索引后移
    }

owner和Shared這兩個(gè)狀態(tài)目前看來是完全相反的,賦值都是同步賦值的,這里有點(diǎn)不明白存在兩個(gè)參數(shù)的意義,現(xiàn)在的功能主要是用來判斷如果是共享就無法寫,以免污染數(shù)據(jù),會(huì)拋出異常。當(dāng)然,如果要寫的字節(jié)大小加上原來的字節(jié)數(shù)大于單個(gè)segment的最大值也是會(huì)拋出異常,也存在一種情況就是雖然尾節(jié)點(diǎn)索引和寫入字節(jié)大小加起來超過,但是由于前面的pos索引可能因?yàn)閞ead方法取出數(shù)據(jù),pos索引后移這樣導(dǎo)致可以容納數(shù)據(jù),這時(shí)就先執(zhí)行移動(dòng)操作,使用系統(tǒng)的 System.arraycopy 方法來移動(dòng)到pos為0的狀態(tài),更改pos和limit索引后再在尾部寫入byteCount數(shù)的數(shù)據(jù),寫完之后實(shí)際上原segment讀了byteCount的數(shù)據(jù),所以pos需要后移這么多。過程十分的清晰,比較好理解。
除了寫入數(shù)據(jù)之外,segment還有一個(gè)優(yōu)化的技巧,因?yàn)槊總€(gè)segment的片段size是固定的,為了防止經(jīng)過長時(shí)間的使用后,每個(gè)segment中的數(shù)據(jù)千瘡百孔,可能十分短的數(shù)據(jù)卻占據(jù)了一整個(gè)segment,所以有了一個(gè)壓縮機(jī)制

    public void compact(){
        if (pre == this)
            throw new IllegalStateException();
        if (!pre.owner) // pre不可寫 
            return;
        int byteCount = limit - pos;
        int availableByteCount = SIZE - pre.limit + (pre.shared ? 0 : pre.pos);  //前一個(gè)的剩余大小
        if (byteCount > availableByteCount)
            return;
        writeTo(pre,byteCount);   //將數(shù)據(jù)寫入到前一個(gè)的片段中
        pop();  // 從雙向鏈表中移除當(dāng)前
        SegmentPool.recycle(this);   //加入到對(duì)象池中
    }

照例如果前面是共享的那么不可寫,也就不能壓縮了,然后判斷前一個(gè)的剩余大小是否比當(dāng)前的大,有足夠的空間來容納數(shù)據(jù),調(diào)用前面的 writeTo 方法來寫數(shù)據(jù),寫完后移除當(dāng)前segment,然后通過 SegmentPool 來回收。
另一個(gè)技巧就是共享機(jī)制,為了減少數(shù)據(jù)復(fù)制帶來的性能開銷,segment存在一個(gè)共享機(jī)制

    public Segment split(int byteCount){
        if (byteCount <= 0 || byteCount > limit - pos )
            throw new IllegalArgumentException();
        Segment prefix;
        if (byteCount >= SHARE_MINIMUM){  //如果byteCount大于最小的共享要求大小
            prefix = new Segment(this); //this這個(gè)構(gòu)造函數(shù)會(huì)
        }else {
            prefix = SegmentPool.take();
            System.arraycopy(data,pos,prefix,0,byteCount);
        }
        prefix.limit = prefix.pos + byteCount;
        pos = pos + byteCount;
        pre.push(prefix);
        return  prefix;
    }

這個(gè)方法實(shí)際上經(jīng)過了很多次的改變,在回顧Okio的1.6的版本時(shí),發(fā)現(xiàn)有一個(gè)重要的差異就是多了一個(gè) SHARE_MINIMUM 參數(shù),同時(shí)也多了一個(gè)注釋,為了防止一個(gè)很小的片段就進(jìn)行共享,我們知道共享之后為了防止數(shù)據(jù)污染就無法寫了,如果存在大片的共享小片段,實(shí)際上是很浪費(fèi)資源的,所以通過這個(gè)對(duì)比可以看出這個(gè)最小數(shù)的意義,而且這個(gè)方法在1.6的版本中檢索實(shí)際上只有一個(gè)地方使用了這個(gè)方法,就是Buffer中的write方法,為了效率在移動(dòng)大數(shù)據(jù)的時(shí)候直接移動(dòng)整個(gè)segment而不是data,這樣在寫數(shù)據(jù)上能達(dá)到很高的效率,具體write的細(xì)節(jié)會(huì)在Buffer一章中詳細(xì)描述。
再回頭看剛才的 compact 中出現(xiàn)的 SegmentPool ,這個(gè)實(shí)際上是一個(gè)segment的對(duì)象池

    static final long MAX_SIZE = 64 * 1024;
    static Segment next;
    static long byteCount;

同樣的有一個(gè)池子的上限,也就是64k,相當(dāng)于8個(gè)segment,next這個(gè)節(jié)點(diǎn)可以看出這個(gè) SegmentPool 是按照單鏈表的方式進(jìn)行存儲(chǔ)的,byteCount則是目前已有的大小。

SegmentPool方法分析

SegmentPool的方法十分的少,一個(gè)取,一個(gè)回收,十分簡潔。

    /**
     * take方法用來取數(shù)據(jù)
     * 如果池子為空就創(chuàng)建一個(gè)空對(duì)象 owner true | share false
     * next是鏈表的頭 就是一個(gè)簡單的取表頭的操作
     * @return
     */
    static Segment take(){
        synchronized (SegmentPool.class){
            if (next != null){
                Segment result = next;
                next = result.next;
                result.next = null;
                byteCount = byteCount - Segment.SIZE;
                return result;
            }
        }
        return new Segment();
    }

為了防止多線程同時(shí)操作造成數(shù)據(jù)的錯(cuò)亂,這里加了鎖,這里的next命名雖然是next,但是實(shí)際上是整個(gè)對(duì)象池的頭,但是next為空,表示池子為空,直接返回一個(gè)空對(duì)象,否則從里面拿出next,并將next的下一個(gè)節(jié)點(diǎn)賦為next,置一下狀態(tài),這個(gè)方法就結(jié)束了

    /**
     * 如果當(dāng)前要回收的segment有前后引用或者是共享的 那么就回收失敗
     * 如果加入后的大小超過了最大大小 也會(huì)失敗
     * 然后將新回收的next指向表頭 也就是加到的鏈表的頭 并且將回收的segment置為next也就是head
     * @param segment
     */
    static void recycle(Segment segment){
        if (segment.next != null || segment.pre != null)
            throw new IllegalArgumentException();
        if (segment.shared)
            return;
        synchronized (SegmentPool.class){
            if (byteCount + Segment.SIZE > MAX_SIZE){
                return;
            }
            byteCount += Segment.SIZE;
            segment.next = next;
            segment.pos = segment.limit = 0;
            next = segment;
        }
    }

如果要回收的segment有前后引用或者是共享的,就不能被回收,所以要回收前先將引用置空,同樣這里也加了鎖,以免那個(gè)同時(shí)回收超過池子最大的大小,然后就是將回收的插到表頭的操作。
所以SegmentPool無論是回收和取對(duì)象都是在表頭操作。

不可變的ByteString

我們都知道String是一個(gè)不可以改變的一個(gè)對(duì)象,那可能有人問了誰說不能改變了,明明還能做分割添加的操作,那這里就不詳述了,有興趣的可以看 Java中的String為什么是不可變的? -- String源碼分析 這篇文章,同樣的ByteString也是一個(gè)不可變的對(duì)象,當(dāng)然,java語言可沒有不可變標(biāo)記關(guān)鍵字,如果想要實(shí)現(xiàn)一個(gè)不可變的對(duì)象,還需要一些操作。
Effective Java一書中有一條給了不可變對(duì)象需要遵循的幾條原則:

  • 不要提供任何會(huì)修改對(duì)象狀態(tài)的方法
  • 保證類不會(huì)被擴(kuò)展
  • 使所有的域都是final的
  • 使所有的域都是private的
  • 確保對(duì)于任何可變組件的互斥訪問

不可變的對(duì)象有許多的好處,首先本質(zhì)是線程安全的,不要求同步處理,也就是沒有鎖之類的性能問題,而且可以被自由的共享內(nèi)部信息,當(dāng)然壞處就是需要?jiǎng)?chuàng)建大量的類的對(duì)象。

    byte[] data;
    transient String utf8;

ByteString不僅是不可變的,同時(shí)在內(nèi)部有兩個(gè)filed,分別是byte數(shù)據(jù),以及String的數(shù)據(jù),這樣能夠讓這個(gè)類在Byte和String轉(zhuǎn)換上基本沒有開銷,同樣的也需要保存兩份引用,這是明顯的空間換時(shí)間的方式,為了性能Okio做了很多的事情。
但是這個(gè)String前面有 transient 關(guān)鍵字標(biāo)記,也就是說不會(huì)進(jìn)入序列化和反序列化,所有我們看到兩個(gè)方法中并沒有utf8這個(gè)屬性。

  private void readObject(ObjectInputStream in) throws IOException {
    int dataLength = in.readInt();
    ByteString byteString = ByteString.read(in, dataLength);
    try {
      Field field = ByteString.class.getDeclaredField("data");
      field.setAccessible(true);
      field.set(this, byteString.data);
    } catch (NoSuchFieldException e) {
      throw new AssertionError();
    } catch (IllegalAccessException e) {
      throw new AssertionError();
    }
  }

  private void writeObject(ObjectOutputStream out) throws IOException {
    out.writeInt(data.length);
    out.write(data);
  }

除此之外, ByteString 內(nèi)置了不少操作,方便使用

方法截選

最核心的Buffer解析

前面講到Buffer這個(gè)類實(shí)際上就是整個(gè)讀和寫的核心,包括 RealBufferedSourceRealBufferedSink 實(shí)際上都只是一個(gè)代理,里面的操作全部都是通過Buffer來完成的

public class Buffer implements BufferedSource, BufferedSink, Cloneable {
long size;
Segment head;

整個(gè)Buffer持有了一個(gè)Segment的引用,通過這個(gè)引用能拿到整個(gè)鏈表中所有的數(shù)據(jù)。
Buffer一共實(shí)現(xiàn)了三個(gè)接口,讀,寫,以及clone。
先從最簡單的clone說起,clone是一種對(duì)象生成的方式,是除了常規(guī)的new·關(guān)鍵字以及反序列化之外的一種方式,主要分為深拷貝和淺拷貝兩種,Buffer采用的是深拷貝的方式

    public Buffer clone(){
        Buffer result = new Buffer();
        if (size == 0){
            return result;
        }
        result.head = new Segment(head);
        result.head.pre = result.head.next = result.head;
        for (Segment s = head.next ; s != head ; s = s.next){
            result.head.pre.push(new Segment(s));  //這里選擇的pre上push一個(gè)segment 
        }
        result.size = size;
        return result;
    }

對(duì)應(yīng)實(shí)現(xiàn)的clone方法,如果整個(gè)Buffer的size為null,也就是沒有數(shù)據(jù),那么就返回一個(gè)新建的Buffer對(duì)象,如果不為空就是遍歷所有的segment并且都創(chuàng)建一個(gè)對(duì)應(yīng)的Segment,這樣clone出來的對(duì)象就是一個(gè)全新的毫無關(guān)系的對(duì)象。
前面分析segment的時(shí)候有講到是一個(gè)雙向循環(huán)鏈表,但是segment自身構(gòu)造的時(shí)候卻沒有形成閉環(huán),其實(shí)就是在Buffer中產(chǎn)生的

result.head.pre = result.head.next = result.head;

clone的過程中創(chuàng)建了一個(gè)雙向循環(huán)鏈表,另外一個(gè)地方就是

    Segment writableSegment(int minimumCapacity) {
        if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE)
            throw new IllegalArgumentException();
        if (head == null) {
            head = SegmentPool.take();
            return head.next = head.pre = head;
        }
        //head 不為null 的情形
        Segment tail = head.pre;
        //如果tail會(huì)導(dǎo)致大于Segment的上限 或是owner為false 也就是不可寫
        if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
            tail = tail.push(SegmentPool.take());  //在tail的后面插入一個(gè)空的segment
        }
        return tail;
    }

除了clone接口外,同時(shí)還有兩個(gè)接口BufferedSink,BufferedSource。Buffer實(shí)現(xiàn)了這兩個(gè)接口的所有方法,所有既然讀也有寫的方法,舉幾個(gè)例子

    @Override
    public Buffer writeShort(int s) throws IOException {
        Segment tail = writableSegment(2);

        byte[] data = tail.data;
        int limit  = tail.limit;
        data[limit++] = (byte) ((s >>> 8) & 0xff);
        data[limit++] = (byte) (s & 0xFF);
        tail.limit = limit;
        size += 2;
        return this;
    }

writeShort用來給Buffer中寫入一個(gè)short的數(shù)據(jù),首先通過writableSegment拿到一個(gè)能夠有2個(gè)字節(jié)空間的segment,tail中的data就是字節(jié)數(shù)組,limit則是數(shù)據(jù)的尾部索引,寫數(shù)據(jù)就是在尾部繼續(xù)往后寫,直接設(shè)置在data通過limit自增后的index,然后重置尾部索引,并且buffer的size大小加2。

    @Override
    public short readShort() throws IOException {
        if (size < 2)
            throw new IllegalArgumentException("size < 2");
        Segment segment = head;
        int pos = segment.pos;
        int limit = segment.limit;
        //如果short被segment分隔開  通過readByte來一個(gè)個(gè)字節(jié)讀
        if (limit - pos < 2) {
            int s = (readByte() & 0xFF) << 8 | (readByte() & 0xFF);
            return (short) s;
        }
        byte[] data = segment.data;  //與readByte類似 只不過一次讀兩個(gè)字節(jié)再組合起來
        int s = (data[pos++] & 0xFF) << 8 | (data[pos++] & 0xFF);  //pos自增2
        size -= 2;
        if (pos == limit) {
            head = segment.pop();
            SegmentPool.recycle(segment);
        } else {
            segment.pos = pos;
        }
        return (short) s;
    }

讀的方法相對(duì)于寫的方法就復(fù)雜一些,因?yàn)閎uffer是分塊的,讀數(shù)據(jù)的過程就有可能是跨segment的,比如前面一個(gè)字節(jié),下一個(gè)segment一個(gè)字節(jié),這種情況就轉(zhuǎn)化為readbyte,讀兩個(gè)字節(jié)后合成一個(gè)short對(duì)象,對(duì)于連續(xù)的讀可以直接通過pos索引自增達(dá)到目的,讀完后Buffer的size減2。
并且會(huì)有當(dāng)前的segment會(huì)出現(xiàn)讀完后數(shù)據(jù)為null的情況,此時(shí)頭部索引pos和尾部索引limit就重合了,通過pop方法可以把這個(gè)segment分離出來,并且將下一個(gè)segment設(shè)置為Buffer的head,然后將分離出來的segment回收到對(duì)象池中。
鑒于篇幅原因就暫時(shí)只舉出Buffer中比較有代表性的讀寫方式,看完這兩個(gè)其他都是類似的。

后記

以上就是整個(gè)Okio核心實(shí)現(xiàn)的分析,篇幅比較長,能夠堅(jiān)持看到這里的都是值得敬佩的,Okio的整個(gè)源碼干貨滿滿,而且架構(gòu)清晰,如果有時(shí)間可以通讀一遍,更能理解上述文章中的分析。
最后總結(jié)一下Okio這個(gè)庫的精髓,第一就是快,Okio采取了空間換時(shí)間的方式比如Segment和ByteString之類的存在來讓IO操作盡可能不成為整個(gè)系統(tǒng)的瓶頸,雖然采取這種方式但是在內(nèi)存上也是極致的優(yōu)化,使用的片段共享以及整體的讀寫共享來加快大字節(jié)數(shù)組的讀寫,第二就是穩(wěn)定,Okio提供了超時(shí)機(jī)制,不僅在IO操作上加上超時(shí)的判定,包括close,flush之類的方法中都有超時(shí)機(jī)制,這讓上層不會(huì)錯(cuò)過一個(gè)可能導(dǎo)致系統(tǒng)崩潰的超時(shí)異常,第三就是方便,Sink,Source兩個(gè)包裝了寫和讀,區(qū)別于傳統(tǒng)的IO各種不同的輸入輸出流,這里只有一種而且支持socket,十分的方便。當(dāng)然Okio還有很多其他的好處,易于擴(kuò)展,代碼量小易于閱讀,我想這就是許多上層庫選擇Okio來作為IO操作的原因。

okio sample來源
https://github.com/ut2014/Okio_1.9

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,115評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,234評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,621評(píng)論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,822評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,380評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,128評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,319評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,548評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評(píng)論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,048評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,285評(píng)論 2 376

推薦閱讀更多精彩內(nèi)容