Okio 框架原始碼學習

看我眼神007發表於2019-02-26
Retrofit,OkHttp,Okio Square 安卓平臺網路層三板斧原始碼學習
基於 okio 1.13.0 版本 okio github 地址

簡介

Okio 主要是替代 java.io 和 java.nio 那一套複雜的 api 呼叫框架,讓資料訪問、儲存和處理更加便捷。

Okio 是 OkHttp 的基石,而 OkHttp 是 Retrofit 的基石。這三個框架合稱『Square 安卓平臺網路層三板斧』

使用方式

參考 OkioTest

……
@Test public void readWriteFile() throws Exception {
    File file = temporaryFolder.newFile();

    BufferedSink sink = Okio.buffer(Okio.sink(file));
    sink.writeUtf8("Hello, java.io file!");
    sink.close();
    assertTrue(file.exists());
    assertEquals(20, file.length());

    BufferedSource source = Okio.buffer(Okio.source(file));
    assertEquals("Hello, java.io file!", source.readUtf8());
    source.close();
}

……
複製程式碼

通過 OkioTest 可以大致明白 Okio 主要有 『讀』、『寫』兩大類操作。可以操作的物件為:

1. 檔案
2. 檔案路徑描述類 Path
3. Socket
4. OutputStream
5. InputStream
複製程式碼

Okio 通過 sink(xxx) 去寫一個物件,通過 source(xxx)去讀一個物件。

一覽 Okio 讀寫框架

通過 Okio.buffer() 獲得用來讀寫的 BufferedSource、BufferedSink

BufferedSink sink = Okio.buffer(Okio.sink(file));
BufferedSource source = Okio.buffer(Okio.source(file));
複製程式碼

進一步檢視 buffer() 方法

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

public static BufferedSink buffer(Sink sink) {
    return new RealBufferedSink(sink);
}
複製程式碼
RealBufferedSink

看下 RealBufferedSink 類

final class RealBufferedSink implements BufferedSink {
    public final Buffer buffer = new Buffer();
    public final Sink sink;
    boolean closed;

    RealBufferedSink(Sink sink) {
    if (sink == null) throw new NullPointerException("sink == null");
    this.sink = sink;
    }
    ……
}
複製程式碼

RealBufferedSink 實現了 BufferedSink 介面,BufferedSink 實現了 Sink 介面。

而 Sink 實現了 Closeable, Flushable 介面。

1. Flushable 介面只定義了一個方法 flush() ,用來實現把資料從快取區寫入到底層輸入流。
2. Closeable 介面定義 close() ,用來關閉流。
3. Sink 介面又定義了一個 write(Buffer source, long byteCount) 和 timeout() 用來寫入資料和設定超時。
4. BufferedSink 介面則定義了一堆 wirteXXX(……) 用來操作不同型別的資料寫入。
複製程式碼

在看 RealBufferedSink 的成員變數

public final Buffer buffer = new Buffer();
public final Sink sink;
boolean closed;
複製程式碼

這裡出現了一個 Buffer 物件,一個從建構函式裡面傳入的 Sink 物件,以及一個用來記錄流是否關閉的 boolean 標誌。

RealBufferedSink 的各種 wirteXXX(……)大都如下

@Override public BufferedSink writeXXX(……) throws IOException {
  if (closed) throw new IllegalStateException("closed");
  buffer.writeXXX(……);
  return emitCompleteSegments();
}
複製程式碼

可見寫入資料的方法,都是有 buffer 物件實現。而在 emitXXX() 和 flush() 方法中則呼叫了 sink.write(buffer, byteCount) 和 sink.flush()

RealBufferedSource

RealBufferedSource 和 RealBufferedSink 類似

final class RealBufferedSource implements BufferedSource {
    public final Buffer buffer = new Buffer();
    public final Source source;
    boolean closed;

    RealBufferedSource(Source source) {
        if (source == null) throw new NullPointerException("source == null");
        this.source = source;
    }
}
複製程式碼

RealBufferedSource 實現了 BufferedSource 介面,BufferedSource 實現了 Source 介面。

Source 介面同樣也實現了 Closeable 介面。

1. Source 整合了 Closeable 介面,表示 Source 提供了一個 close 方法關閉讀取資料的流。
2. Source 定義了一個 read(Buffer sink, long byteCount) 用來讀取資料,一個 timeout() 方法用來設定讀取超時。
3. BufferedSource 定義了很多 readXXX(……) 用來讀取資料。
複製程式碼

RealBufferedSource 中的 readXXX(……) 方法和 RealBufferedSink 中的 writeXXX(……) 類似,都是通過成員變數 buffer 和 構造物件時傳入的 Source 物件配合起來讀取資料。

總結一下整個讀寫框架的結構如下:

okio_01.png

對所有讀寫物件的統一處理

無論是 File 、Path、InputStream、OutputStream 、Socket ,在 Okio 框架中只要一個簡單的 Okio.sink(……) 方法即可獲得對應的輸入流(RealBufferedSink)和輸出流(RealBufferedSource)

而且 Okio 還給輸入/輸出流的都提供一個額外引數:Timeout,用來設定讀寫的超時設定。

所有的 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() {
    @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.sink() 會建立一個匿名內部類的例項,實現了 write 方法,用來寫入資料到 OutputStream(File 、Path、Socket 都會被轉成成 OutputStream(),每次寫入資料的時候,都會檢測是否超時。(超時機制後面後講)

Okio.Source() 類似會建立一個實現 Source 介面的匿名內部類例項。實現 read 方法 ,負責從 InputStream 中讀取資料。

Okio 在讀寫資料的時候,裡面都會用用一個 Segment 物件。Segment 是 Okio 定義的一個***連結串列結構***的資料片段,每個 Segment 可以最多存放 8K 的位元組。

萬能的 Buffer

寫資料的時候 Okio 會先把資料寫到 buffer 中

BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeUtf8("Hello, java.io file!");
sink.close();
複製程式碼

Okio.buffer() 返回的是 RealBufferedSink

@Override public BufferedSink writeUtf8(String string) throws IOException {
  if (closed) throw new IllegalStateException("closed");
  buffer.writeUtf8(string);
  return emitCompleteSegments();
}
複製程式碼

檢視 writeUtf8

@Override public Buffer writeUtf8(String string) {
  return writeUtf8(string, 0, string.length());
}
複製程式碼

然後把 String 變成一個 Segment 連結串列

@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;
        ……
        while (i < runLimit) {
          c = string.charAt(i);
          if (c >= 0x80) break;
          data[segmentOffset + i++] = (byte) c; // 0xxxxxxx
        }    
        ……

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

      } ……
    }

    return this;
  }
複製程式碼

通過 writableSegment 是不是要開闢新的 Segment 到佇列尾部

Segment writableSegment(int minimumCapacity) {
  if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();

  if (head == null) {
    head = SegmentPool.take(); // Acquire a first segment.
    return head.next = head.prev = head;
  }

  Segment tail = head.prev;
  if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
    tail = tail.push(SegmentPool.take()); // Append a new empty segment to fill up.
  }
  return tail;
}
複製程式碼

在看 emitCompleteSegments()

@Override
public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    long byteCount = buffer.completeSegmentByteCount();
    if (byteCount > 0) sink.write(buffer, byteCount);
    return this;
}
複製程式碼

buffer.completeSegmentByteCount() 用來計算 Segment 的快取的位元組長度

public long completeSegmentByteCount() {
    long result = size;
    if (result == 0) return 0;

    // Omit the tail if it's still writable.
    Segment tail = head.prev;
    if (tail.limit < Segment.SIZE && tail.owner) {
        result -= tail.limit - tail.pos;
    }

    return result;
}
複製程式碼

sink.write(buffer, byteCount) 就是之前傳入的整合的 Sink 匿名類。

總結一下整個流程

okio_02.png

讀資料的時候 Buffer 起到的作用類似,直接貼一下流程圖

okio_03.png

Okio 超時機制

Okio 可以給他 OutputStream 、InputStream 增加一個超市設定。讀寫檔案時會設定一個預設的 TimeOut ,這個方法是個空實現。

在讀寫 Socket 的時候,Okio 給我們展示一個如何設定一個非同步的超時機制,用來在 Socket 讀寫超時時關閉流。

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);
}
複製程式碼

先看 timeout(socket)

private static AsyncTimeout timeout(final Socket socket) {
    return new AsyncTimeout() {
        @Override
        protected IOException newTimeoutException(@Nullable IOException cause) {
            ……
        }

        @Override
        protected void timedOut() {
            try {
                socket.close();
            }……
        }
    };
}
複製程式碼

這裡看出會返回一個 AsyncTimeout 的匿名物件,主要在 timeOut() 中關閉 Socket。

sink(socket.getOutputStream(), timeout) 方法在上面已經看過了主要看其中的一句程式碼

private static Sink sink(final OutputStream out, final Timeout timeout) {
    ……
    return new Sink() {
        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            ……
            while (byteCount > 0) {
                timeout.throwIfReached();
                ……
            }
        }
        ……
    };
}
複製程式碼

在看一下 throwIfReached 方法

public void throwIfReached() throws IOException {
    if (Thread.interrupted()) {
        throw new InterruptedIOException("thread interrupted");
    }

    if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
        throw new InterruptedIOException("deadline reached");
    }
}
複製程式碼

如果 hasDeadline 是 true,並且 deadlineNanoTime 大於 System.nanoTime() 來判斷是否達超時。

在看 timeout.sink(sink)

public final Sink sink(final Sink sink) {
    return new Sink() {
        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            checkOffsetAndCount(source.size, 0, byteCount);

            while (byteCount > 0L) {
                // Count how many bytes to write. This loop guarantees we split on a segment boundary.
                long toWrite = 0L;
                for (Segment s = source.head; toWrite < TIMEOUT_WRITE_SIZE; s = s.next) {
                    int segmentSize = s.limit - s.pos;
                    toWrite += segmentSize;
                    if (toWrite >= byteCount) {
                        toWrite = byteCount;
                        break;
                    }
                }

                // Emit one write. Only this section is subject to the timeout.
                boolean throwOnTimeout = false;
                enter();
                try {
                    sink.write(source, toWrite);
                    byteCount -= toWrite;
                    throwOnTimeout = true;
                } catch (IOException e) {
                    throw exit(e);
                } finally {
                    exit(throwOnTimeout);
                }
            }
        }

        @Override
        public void flush() throws IOException {
            boolean throwOnTimeout = false;
            enter();
            try {
                sink.flush();
                throwOnTimeout = true;
            } catch (IOException e) {
                throw exit(e);
            } finally {
                exit(throwOnTimeout);
            }
        }

        @Override
        public void close() throws IOException {
            boolean throwOnTimeout = false;
            enter();
            try {
                sink.close();
                throwOnTimeout = true;
            } catch (IOException e) {
                throw exit(e);
            } finally {
                exit(throwOnTimeout);
            }
        }

        @Override
        public Timeout timeout() {
            return AsyncTimeout.this;
        }

        @Override
        public String toString() {
            return "AsyncTimeout.sink(" + sink + ")";
        }
    };
}
複製程式碼

可以看出 timeout.sink(sink) 重新包裝了 Sink 給 Sink 的每個方法都增加一個 enter() 方法

public final void enter() {
    if (inQueue) throw new IllegalStateException("Unbalanced enter/exit");
    long timeoutNanos = timeoutNanos();
    boolean hasDeadline = hasDeadline();
    if (timeoutNanos == 0 && !hasDeadline) {
        return; // No timeout and no deadline? Don't bother with the queue.
    }
    inQueue = true;
    scheduleTimeout(this, timeoutNanos, hasDeadline);
}
複製程式碼

這裡會發現如果滿足了條件,會執行 scheduleTimeout 方法。但是預設情況下,條件不會被滿足。

檢視一下 SocketTimeoutTest

@Test
public void readWithoutTimeout() throws Exception {
    Socket socket = socket(ONE_MB, 0);
    BufferedSource source = Okio.buffer(Okio.source(socket));
    source.timeout().timeout(5000, TimeUnit.MILLISECONDS);
    source.require(ONE_MB);
    socket.close();
}
複製程式碼

這裡可以看到,需要呼叫 source.timeout().timeout(5000, TimeUnit.MILLISECONDS)

public Timeout timeout(long timeout, TimeUnit unit) {
    if (timeout < 0) throw new IllegalArgumentException("timeout < 0: " + timeout);
    if (unit == null) throw new IllegalArgumentException("unit == null");
    this.timeoutNanos = unit.toNanos(timeout);
    return this;
}
複製程式碼

這裡可以看到 timeoutNanos 在這裡賦值了。所以設定 timeout(5000, TimeUnit.MILLISECONDS) 後會出發 scheduleTimeout(this, timeoutNanos, hasDeadline)

private static synchronized void scheduleTimeout(
        AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
    // Start the watchdog thread and create the head node when the first timeout is scheduled.
    if (head == null) {
        head = new AsyncTimeout();
        new Watchdog().start();
    }
    ……
    // Insert the node in sorted order.
    long remainingNanos = node.remainingNanos(now);
    for (AsyncTimeout prev = head; true; prev = prev.next) {
        if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
            node.next = prev.next;
            prev.next = node;
            if (prev == head) {
                AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front.
            }
            break;
        }
    }
}
複製程式碼

這裡用到一個同步鎖、啟動一個 Watchdog 執行緒。並且根據 timeout 的超時時間,把 AsyncTimeout 新增到一個任務佇列中。

private static final class Watchdog extends Thread {
    Watchdog() {
        super("Okio Watchdog");
        setDaemon(true);
    }

    public void run() {
        while (true) {
            try {
                AsyncTimeout timedOut;
                synchronized (AsyncTimeout.class) {
                    timedOut = awaitTimeout();

                    // Didn't find a node to interrupt. Try again.
                    if (timedOut == null) continue;

                    // The queue is completely empty. Let this thread exit and let another watchdog thread
                    // get created on the next call to scheduleTimeout().
                    if (timedOut == head) {
                        head = null;
                        return;
                    }
                }

                // Close the timed out node.
                timedOut.timedOut();
            } catch (InterruptedException ignored) {
            }
        }
    }
}
複製程式碼

Watchdog 執行緒會一直同步遍歷任務佇列執行 awaitTimeout()

static @Nullable
AsyncTimeout awaitTimeout() throws InterruptedException {
    // Get the next eligible node.
    AsyncTimeout node = head.next;

    // The queue is empty. Wait until either something is enqueued or the idle timeout elapses.
    if (node == null) {
        long startNanos = System.nanoTime();
        AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
        return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
                ? head  // The idle timeout elapsed.
                : null; // The situation has changed.
    }

    long waitNanos = node.remainingNanos(System.nanoTime());

    // The head of the queue hasn't timed out yet. Await that.
    if (waitNanos > 0) {
        // Waiting is made complicated by the fact that we work in nanoseconds,
        // but the API wants (millis, nanos) in two arguments.
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
        return null;
    }

    // The head of the queue has timed out. Remove it.
    head.next = node.next;
    node.next = null;
    return node;
}
複製程式碼

} 這裡會根據佇列頭部的 AsyncTimeout 節點,計算出剩餘時間,然後執行 AsyncTimeout.class.wait(waitMillis, (int) waitNanos) 方法阻塞。

注意這個的 wait(timeout) 會被 AsyncTimeout.class.notify() 喚醒。
複製程式碼

如果任務佇列為空會執行 AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS) ,等待一分鐘。然後再判斷是否有新的任務。

參考資料

拆輪子系列:拆 Okio

Okio原始碼分析

okio github 地址

相關文章