Okio源碼解析

Okio是對java原生io的封裝,旨在簡化api同時優化io操作性能。接下來我會從下面幾個方面介紹

  1. Okio特性概述
  2. 讀寫流程源碼查看
  3. Buffer精華操作
  4. Timeout超時處理

1. Okio特性概述

java已經提供了很多io實現類供我們在不同場景使用,而Okio並不是一種新的io,而是對原生io的一次封裝,爲的是解決原生io的一些缺陷,下面我們介紹Okio的特性

1.1 api簡化

我們知道java的io相關的類非常多,有針對字節和字符的輸入輸出接口,有實現緩存的Bufferedxxx,以及各種子類實現比如文件的(FileInputStream和FileOutputStream),數據的(DataInputStream和DataOutputStream),對象的(ObjectInputStream和ObjectOutputStream)等等,針對不同的場景我們需要使用不同的類,是非常複雜的。

而Okio簡化了這一流程,統一通過Okio這個工廠類初始化,內部是通過重載方法根據傳入的參數不同初始化不同的流。

舉個板栗

        File file = new File(Environment.getExternalStorageState(), "test");
        Okio.buffer(Okio.sink(file)).writeString("aaaa", Util.UTF_8).close();

由於傳入的參數是File,內部是通過FileOutputStream進行io寫操作,並且支持鏈式操作。

1.2 緩存優化

原生的io緩存比較粗暴,Okio在這方面做了很多優化。

以BufferedOutputStream爲例,是利用一個長度爲8192的byte數組緩存,當要寫入數據長度大於緩存最大容量時跳過緩存直接進行io寫操作,當寫入數據長度大於緩存數組剩餘容量的時候先把緩存寫入輸出流,再將數據寫入緩存,其他情況直接寫入緩存。

然後原生的輸入輸出流之間的buffer是沒有辦法直接建立聯繫的,輸入流中的數據轉移到輸出流中是:輸入buf -> 臨時byte數組 -> 輸出buf,經歷兩次拷貝

        //原生
        File file = new File(Environment.getExternalStorageState(), "test");
        try {
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
            int count = -1;
            byte[] array = new byte[1024];//臨時byte數組
            while ((count = bis.read(array)) != -1) {
                bos.write(array, 0, count);
            }
            bis.close();
            bos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

okio的話則是用一個Buffer對象去負責緩存,內部維護了一個Segment雙向鏈表,而這個Segment則是真正的數據緩存載體,內部利用一個長度爲8192的byte數組去緩存,當要寫入的數據超過8192的時候則會創建多個Segment對象在鏈表中有序存儲。當Segment使用完畢的時候又會利用SegmentPool進行回收,減少Segment對象創建。

對於輸入流流到輸出流有專門的優化,直接將輸入流buffer中segment數據轉移到輸出流的buffer中只有一次數據的操作,相比原生粗暴的buffer做了很多優化。(而實際操作是,如果是整段的segment數據轉移則是直接修改指針指到輸出流的buffer上,如果只轉移輸入流Segment部分數據則根據輸出Segment能不能裝得下,裝得下的話則進行數據拷貝,否則拆分輸入流Segment爲兩個然後將要轉移數據的Segment指針指向輸出流Buffer,總之Okio在這塊做了很多優化,這個後面會細說)

1.3 超時操作

原生進行io操作的時候是阻塞式的,沒有超時的處理,除非發生異常纔會停止,okio增加了超時處理,推出了Timeout機制,提供了同步超時和異步超時處理。

同步超時Timeout:是在每次進行讀寫前檢測是否超時,如果超時則拋出異常,那如果檢測完之後操作阻塞了很久是沒法停止的,只有等到下一次操作的時候纔會拋出異常停止操作。

異步超時AsyncTimeout:是在每次要進行讀寫操作的時候創建一個AsymcTimeout對象,然後通過一個鏈表存儲,根據即將超時時間排序,快要超時的排在鏈表頭部,然後啓動一個Watchdog線程進行檢測,優先從鏈表頭部取出AsyncTimeout判斷是否超時,超時了的話則調用AsyncTimeout#timeout()方法。okio給用socket創建流提供了默認實現,timeout的時候直接關閉Socket。

1.4 相關類介紹

  • Source:Okio對輸入流的抽象接口,提供了read方法將數據讀到buffer中
  • Sink:Okio對輸出流的抽象接口,提供了write方法將數據寫到buffer中
  • BufferedSource和BufferedSink:是Okio對Buffer的接口抽象,分別繼承Source和Sink
  • RealBufferedSource:BufferedSource實現類,讀操作都是由該類來完成,可以通過Okio工廠獲取
  • RealBufferedSink:BufferedSink實現類,寫操作都是由該類來完成,可以通過Okio工廠獲取
  • Okio:okio的工廠來用來獲取buffer實現類和流的實現類
  • Segment:Buffer中存儲數據的載體,內部通過一個8K的byte數組存儲數據
  • SegmentPool:管理Segment創建和回收,最多存儲64K的數據也就是8個Segment,當池中存在Segment的時候會複用減少對象的創建

以讀寫文件爲例,讀是先文件內容讀到buffer中,然後再從buffer中獲取數據,寫是先寫數據到buffer中,然後將將buffer中的數據寫入文件,而buffe中數據是通過一個個Segment存儲的,Segment則是通過SegmentPool創建和回收,每個類各司其職。

2. 讀寫流程源碼

2.1 讀流程

		File file = new File(Environment.getExternalStorageState(), "test");
    try {
         BufferedSource source = Okio.buffer(Okio.source(file));
         byte[] array = new byte[1024];
         source.read(array);
         source.close();
     } catch (Exception e) {
         e.printStackTrace();
     }

根據上面的例子我們分幾段查看源碼

  1. Okio.source()
  2. Okio.buffer()
  3. BufferedSource.read()

2.1.1 Okio.source()

  //Okio#source
	public static Source source(File file) throws FileNotFoundException {
    if (file == null) throw new IllegalArgumentException("file == null");
    return source(new FileInputStream(file));
  }

可以看到由於我們傳入的是file所以內部實際是通過FileInputStream讀文件

  public static Source source(InputStream in) {
    return source(in, new Timeout());
  }

  private static Source source(final InputStream in, final Timeout timeout) {
    if (in == null) throw new IllegalArgumentException("in == null");
    if (timeout == null) throw new IllegalArgumentException("timeout == null");

    return new Source() {
      @Override public long read(Buffer sink, long byteCount) throws IOException {
        if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
        if (byteCount == 0) return 0;
        try {
          timeout.throwIfReached();//判斷是否超時
          Segment tail = sink.writableSegment(1);//從buffer中拿到一個可寫的Segment
          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);//byteCount是要讀取的數據總量,Segment.SIZE - tail.limit是Segment可以裝的數據量,取最小值
          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);//然後通過FileInputStream將文件數據讀到segment的data中
          if (bytesRead == -1) return -1;
          tail.limit += bytesRead;
          sink.size += bytesRead;
          return bytesRead;
        } catch (AssertionError e) {
          if (isAndroidGetsocknameError(e)) throw new IOException(e);
          throw e;
        }
      }

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

      @Override public Timeout timeout() {
        return timeout;
      }

      @Override public String toString() {
        return "source(" + in + ")";
      }
    };
  }

上面這段就是創建了一個Source實現類,read的話則是先獲取buffer中可寫的segment,然後用FileInputStream將文件的數據讀到segment中。對於Segment和Buffer在下面會說到

2.1.2 Okio.Buffer()

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

由於我們傳入的是Source所以會創建一個RealBufferedSource實例,Okio也都是通過它來做讀操作,我們先來看下這個類

final class RealBufferedSource implements BufferedSource {
  public final Buffer buffer = new Buffer();//buffer對象
  public final Source source;//Source實現類

  RealBufferedSource(Source source) {
    if (source == null) throw new NullPointerException("source == null");
    this.source = source;
  }
  
  @Override 
  public int readInt() throws IOException {
    require(4);//最終通過source#read()將數據讀到Buffer
    return buffer.readInt();//再從Buffer讀取數據
  }
  
  @Override 
  public int read(byte[] sink, int offset, int byteCount) throws IOException {
    checkOffsetAndCount(sink.length, offset, byteCount);

    if (buffer.size == 0) {
      long read = source.read(buffer, Segment.SIZE);//先將數據讀到Buffer
      if (read == -1) return -1;
    }

    int toRead = (int) Math.min(byteCount, buffer.size);
    return buffer.read(sink, offset, toRead);//再從Buffer讀取數據
  }
  
  @Override 
  public String readString(Charset charset) throws IOException {
    if (charset == null) throw new IllegalArgumentException("charset == null");

    buffer.writeAll(source);//最終通過source#read()將數據讀到Buffer
    return buffer.readString(charset);//再從Buffer讀取數據
  }
  
  //後面省略了很多個read方法
}

可以看到它先將Okio.buffer(Okio.source(file))傳入的Source實現類通過source成員變量存儲,然後創建了一個Buffer對象用作緩衝區,而read方法都是一個流程通過source#read()方法將數據寫入緩衝區,然後再從緩衝區中讀取數據。接下來我們在看下緩衝區Buffer對象

public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel {
  @Nullable Segment head;//緩存數據的載體
  long size;//當前緩衝區的數據量單位字節
  public Buffer() {
  }
  //下面省略很多讀寫緩衝區的操作
}

Buffer是負責管理緩衝區的對象,內部則是通過一個雙向環狀鏈表Segment存儲數據,然後我們再來看下Segment是如何存儲數據的呢

final class Segment {
  static final int SIZE = 8192;//最大存儲8K數據
  final byte[] data;//存儲數據數組
  int pos;//當前segment數據已經讀取到哪了,接下來從pos位置開始讀
  int limit;//當前segment數據寫到哪了,接下來從limit開始寫
  boolean owner;//當前segment是否允許追加數據,也就是能不能多次寫入
  Segment next;
  Segment prev;

  Segment() {
    this.data = new byte[SIZE];
    this.owner = true;//默認可以多次寫入
    this.shared = false;
  }

可以看到segment內部有一個8k的byte數組來存儲數據,pos記錄的是segment可讀數據位置,(pos-1)到0是已經讀過的數據,limit是segment可寫數據的位置,limit到Segment.SIZE是剩餘可寫數據量,pos到limit是還未讀取的數據。

ok那我們回到本例中的source.read(array)

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

  @Override public int read(byte[] sink, int offset, int byteCount) throws IOException {
    checkOffsetAndCount(sink.length, offset, byteCount);//數組相關檢查

    if (buffer.size == 0) {//如果緩衝區是空的
      long read = source.read(buffer, Segment.SIZE);//將source中的數據讀取到buffer中
      if (read == -1) return -1;
    }

    int toRead = (int) Math.min(byteCount, buffer.size);
    return buffer.read(sink, offset, toRead);//將數據寫入sink也就是byte數組
  }

對於source#read我們在2.1.1已經看過就是取出Buffer中的Segment,然後將數據寫入。不過細節當時我們沒說現在來看下

//source#read()
@Override public long read(Buffer sink, long byteCount) throws IOException {
        try {
          Segment tail = sink.writableSegment(1);//從buffer中拿到一個可寫的Segment
          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);//byteCount是要讀取的數據總量,Segment.SIZE - tail.limit是Segment可寫數據量,取最小值
          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);//然後通過FileInputStream將文件數據讀到segment的data數組中
          if (bytesRead == -1) return -1;
          tail.limit += bytesRead;//修改segment可寫位置
          sink.size += bytesRead;//修改buffer中數據總量
          return bytesRead;
        } catch (AssertionError e) {
          if (isAndroidGetsocknameError(e)) throw new IOException(e);
          throw e;
        }
      }

buffer#writableSegment(1)如何獲取Segment的

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

    if (head == null) {//如果buffer中沒有Segment
      head = SegmentPool.take(); //從SegmentPool中獲取
      return head.next = head.prev = head;//維護Buffer內部的Segment雙向環狀鏈表
    }

    Segment tail = head.prev;//拿到最後一個Segment
    if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {//如果最後一個Segment可寫位置limit+需要的最小容量>Segment.SIZE || 該Segment不支持追加寫入
      tail = tail.push(SegmentPool.take()); // 添加一個新的Segment到尾部
    }
    return tail;
  }

如果Buffer中沒有Segment則直接從SegmentPool獲取,如果有則獲取鏈表尾部也就是最新的Segment,判斷數據是否存的下,存不下的話從SegmentPool獲取一個新的插到鏈表尾部

接下來看下SegmentPool#take如何創建Segment的

final class SegmentPool {
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.最大容量64K
  static @Nullable Segment next;//通過一個單鏈表存儲回收的Segment
  static long byteCount;//當前SegmentPool容量

  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(); //否則直接創建
  }

  static void recycle(Segment segment) {
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    if (segment.shared) return; //當前Segment如果有共享數據不能回收(這個後面說)
    synchronized (SegmentPool.class) {
      if (byteCount + Segment.SIZE > MAX_SIZE) return; //當前SegmentPool滿了的話則不能回收
      byteCount += Segment.SIZE;//容量增加
      segment.next = next;//加到鏈表中
      segment.pos = segment.limit = 0;//可寫和可讀位置都清零
      next = segment;
    }
  }
}

SegmentPool#take就是看當前的池子中有緩存的Segment的麼,有直接使用,沒有則創建一個。ok在回到最前面RealBufferedSource#read

@Override 
  public int read(byte[] sink, int offset, int byteCount) throws IOException {
    checkOffsetAndCount(sink.length, offset, byteCount);

    if (buffer.size == 0) {
      long read = source.read(buffer, Segment.SIZE);//先將數據讀到Buffer
      if (read == -1) return -1;
    }

    int toRead = (int) Math.min(byteCount, buffer.size);
    return buffer.read(sink, offset, toRead);//再從Buffer讀取數據
  }

第一塊source#read(buffer, Segment.SIZE)已經梳理了一遍就是通過FileInputStream將數據讀到Buffer的Segment中,然後再來buffer#read將數據讀到byte數組中

  @Override public int read(byte[] sink, int offset, int byteCount) {
    checkOffsetAndCount(sink.length, offset, byteCount);

    Segment s = head;//拿到第一個Segment
    if (s == null) return -1;
    int toCopy = Math.min(byteCount, s.limit - s.pos);//判斷要讀取的數據量,取byteCount和當前segment可讀的數據量s.limit - s.pos
    System.arraycopy(s.data, s.pos, sink, offset, toCopy);//將數據拷貝到數組中

    s.pos += toCopy;//移動Segment已經讀過的數據指針pos
    size -= toCopy;//當前Buffer容量減去讀過數據量

    if (s.pos == s.limit) {//如果當前Segment已經讀完
      head = s.pop();//從鏈表中脫離
      SegmentPool.recycle(s);//SegmentPool嘗試回收
    }

    return toCopy;
  }

將Buffer中Segment數據拷貝到數組中,如果Segment數據已經讀完則從Buffer鏈表中脫離,SegmentPool嘗試回收。

ok那讀流程就講完了,我們回顧下大體流程

BufferedSource source = Okio.buffer(Okio.source(file));
         byte[] array = new byte[1024];
         source.read(array);
         source.close();
  • Okio.Source():是創建了一個Source實現類,提供read的能力,需要傳入一個Buffer來獲取數據,數據讀取是通過FileInputStream來讀取的,寫入到Buffer的Segment中。
  • Okio.buffer():則創建了一個Buffer實現類RealBufferedSource,內部維護了一個環形雙向鏈表Segment,是我們真正用來讀取數據的對象。
  • BufferedSource.read():讀取數據操作,流程是先將數據讀到buffer中,然後再從buffer中讀取數據。

2.2 寫流程

寫流程是讀流程反過來,先將數據寫到buffer,然後在從buffer寫到文件中,大體跟前面差不多我們快速說一下

            byte[] array = new byte[1024];
            BufferedSink sink = Okio.buffer(Okio.sink(file));
            sink.write(array);
            sink.close();

2.2.1 Okio.sink()

先看Okio#sink(file)

  public static Sink sink(File file) throws FileNotFoundException {
    if (file == null) throw new IllegalArgumentException("file == null");
    return sink(new FileOutputStream(file));//創建FileOutputStream
  }

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

  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) {//遍歷將Buffer中數據都寫入到文件中
          timeout.throwIfReached();
          Segment head = source.head;//獲取buffer中的segment
          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();//segment脫鏈
            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 + ")";
      }
    };
  }

內部通過FileOutputStream進行io操作,通過一個while循環將Buffer中Segment數據統統寫入到文件中。

2.2.2 Okio.buffer()

  public static BufferedSink buffer(Sink sink) {
    return new RealBufferedSink(sink);
  }

創建一個RealBufferedSink

final class RealBufferedSink implements BufferedSink {
  public final Buffer buffer = new Buffer();//創建緩衝區
  public final Sink sink;//真正的io操作對象
  boolean closed;
  
    RealBufferedSink(Sink sink) {
    if (sink == null) throw new NullPointerException("sink == null");
    this.sink = sink;
  }
  
   @Override public BufferedSink write(byte[] source) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.write(source);//寫流程第一步是將數據寫到buffer
    return emitCompleteSegments();//然後將Buffer數據寫到文件中
  }
  
    @Override public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    long byteCount = buffer.completeSegmentByteCount();
    if (byteCount > 0) sink.write(buffer, byteCount);//然後將Buffer數據寫到文件中
    return this;
  }
}

內部創建了一個Buffer作爲緩存區,並將Sink通過成員變量存儲起來,寫數據則是先將數據寫到buffer中,在由Buffer通過FileOutputStream寫到文件中。

接下來看看 buffer#write(source)是如何將數據寫入Buffer的

  @Override public Buffer write(byte[] source) {
    if (source == null) throw new IllegalArgumentException("source == null");
    return write(source, 0, source.length);
  }

  @Override public Buffer write(byte[] source, int offset, int byteCount) {
    if (source == null) throw new IllegalArgumentException("source == null");
    checkOffsetAndCount(source.length, offset, byteCount);

    int limit = offset + byteCount;//byte數組需要寫到的最後一位下標
    while (offset < limit) {
      Segment tail = writableSegment(1);//獲取可寫的Segment

      int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
      System.arraycopy(source, offset, tail.data, tail.limit, toCopy);//數據拷貝到Segment中

      offset += toCopy;
      tail.limit += toCopy;
    }

    size += byteCount;
    return this;
  }

循環的獲取一個可寫的Segment將數據寫到當中,直到byte數組中寫完。然後就是RealBufferedSink#emitCompleteSegments將Buffer數據寫到文件中

@Override public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    long byteCount = buffer.completeSegmentByteCount();//獲取寫完的Segment中byte數
    if (byteCount > 0) sink.write(buffer, byteCount);//然後將Buffer數據寫到文件中
    return this;
  }

buffer#completeSegmentByteCount()

  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) {//最後一個Segment如果沒裝滿則暫不寫入文件
      result -= tail.limit - tail.pos;
    }

    return result;
  }

獲取寫滿的Segment字節數

3. Buffer精華操作

除了前面看到的對緩衝區的優化接下來看看Okio對於輸入流流到輸出流的優化

        try {
            BufferedSource source = Okio.buffer(Okio.source(file));
            BufferedSink sink = Okio.buffer(Okio.sink(file));
            sink.writeAll(source);
            sink.close();
            source.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

先看sink#writeAll()

  @Override public long writeAll(Source source) throws IOException {
    if (source == null) throw new IllegalArgumentException("source == null");
    long totalBytesRead = 0;
    for (long readCount; (readCount = source.read(buffer, Segment.SIZE)) != -1; ) {//for循環將數據讀到輸出流的buffer中
      totalBytesRead += readCount;
      emitCompleteSegments();//在把數據從buffer寫入文件
    }
    return totalBytesRead;
  }

調用了source.read()將數據讀到BufferedSink的buffer中

  @Override public long read(Buffer sink, long byteCount) {
    if (sink == null) throw new IllegalArgumentException("sink == null");
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (size == 0) return -1L;
    if (byteCount > size) byteCount = size;
    sink.write(this, byteCount);
    return byteCount;
  }

調用輸出流buffer.write()將數據寫入,前方高能重點來了

  @Override public void write(Buffer source, long byteCount) {
		if (source == null) throw new IllegalArgumentException("source == null");
    if (source == this) throw new IllegalArgumentException("source == this");
    checkOffsetAndCount(source.size, 0, byteCount);

    while (byteCount > 0) {
      if (byteCount < (source.head.limit - source.head.pos)) {//如果只需要當前Segment部分數據,或者說讀到最後一個Segment了
        Segment tail = head != null ? head.prev : null;
        if (tail != null && tail.owner
            && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {//如果要寫的數據byteCount+limit+(如果當前不是共享Segment,則可以覆蓋前面已讀數據所以+pos,如果是共享Segment則不能覆蓋已讀數據所以爲0)<= Segment.SIZE,證明寫Buffer的Segment裝得下
          source.head.writeTo(tail, (int) byteCount);//把讀buffer中Segment數據寫到Segment中
          source.size -= byteCount;
          size += byteCount;
          return;//return
        } else {//如果裝不下則分割讀buffer的Segment爲兩個,將我們需要數據的部分放在頭部
          source.head = source.head.split((int) byteCount);
        }
      }

      //下面就是把讀buffer中segment從鏈表取出,拼接到寫buffer的segment鏈表中
      Segment segmentToMove = source.head;
      long movedByteCount = segmentToMove.limit - segmentToMove.pos;
      source.head = segmentToMove.pop();//從讀buffer中斷鏈
      if (head == null) {
        head = segmentToMove;
        head.next = head.prev = head;
      } else {
        Segment tail = head.prev;
        tail = tail.push(segmentToMove);//拼接到寫buffer的鏈表中
        tail.compact();//嘗試壓縮Segment數據,移除多餘Segment對象
      }
      source.size -= movedByteCount;
      size += movedByteCount;
      byteCount -= movedByteCount;
    }
  }

接下來我們梳理下上面的流程

  1. 先判斷當前是不是讀到最後一個Segment了,如果是在判斷寫buffer中最後一個Segment寫不寫的下,寫的下的話就寫入然後return結束了,否則把要讀的那個Segment拆分成兩段,將我們需要數據的部分放在頭部
  2. 如果第一個判斷中沒return,證明有整段的Segment數據需要拷貝,爲了提高效率則直接將讀buffer中Segment脫鏈,直接接到寫buffer中提高效率,然後嘗試壓縮Segment鏈表的數據,移除多餘的Segment

流程說了,接下來我們看上面沒分析的幾個方法

  • source.head.writeTo(tail, (int) byteCount)
  • source.head.split((int) byteCount)
  • tail.compact()

先看source.head.writeTo(tail, (int) byteCount)

  public void writeTo(Segment sink, int byteCount) {
    if (!sink.owner) throw new IllegalArgumentException();
    if (sink.limit + byteCount > SIZE) {//如果limit+byteCount>Size即裝不下所以需要把數據往前移覆蓋已讀數據
      if (sink.shared) throw new IllegalArgumentException();//當前Segment是共享數組的則報錯,至於shared屬性在split方法的時候會說
      if (sink.limit + byteCount - sink.pos > SIZE) 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(data, pos, sink.data, sink.limit, byteCount);//數據寫入
    sink.limit += byteCount;
    pos += byteCount;
  }

可以看到Segment#writeTo()是將數據寫入傳入的Segment中,如果sink寫不下的話則會將已讀數據覆蓋騰出空間在寫入。

再來source.head.split((int) byteCount)

  public Segment split(int byteCount) {
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix;

    if (byteCount >= SHARE_MINIMUM) {//如果要分割的數據量大於1024即1K則共享數組而不是數據拷貝
      prefix = sharedCopy();//共享數組
    } else {//數據量小於1K
      prefix = SegmentPool.take();//創建一個新的Segment
      System.arraycopy(data, pos, prefix.data, 0, byteCount);//拷貝數據
    }

    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    prev.push(prefix);//插入分割出的Segment
    return prefix;
  }

  Segment sharedCopy() {
    shared = true;
    return new Segment(data, pos, limit, true, false);//共享同一個data數組
  }

split則是先判斷要分割的數據量大於1K麼,如果大於則使用共享數組的方式創建Segment減少拷貝,否則創建一個新的Segment通過數組拷貝的方式將數據傳入。

而前面判斷的shared就是這裏賦值的,使用數組共享方式創建的Segment,是不能爲了寫入數據將數據前移覆蓋已讀數據騰出位置,因爲持有的是數組引用會影響到別的Segment。

最後tail.compact()

  public void compact() {
    if (prev == this) throw new IllegalStateException();
    if (!prev.owner) return; // Cannot compact: prev isn't writable.
    int byteCount = limit - pos;//計算當前Segment數據量
    int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
    if (byteCount > availableByteCount) return; //判斷前一個Segment裝得下麼,裝不下return
    writeTo(prev, byteCount);//將當前Segment數據寫到前一個Segment中
    pop();//將當前Segment脫鏈
    SegmentPool.recycle(this);//回收
  }

判斷當前Segment中數據能放到前一個Segment麼,如果可以則將數據寫入,移除當前Segment。

ok這裏總結下Buffer的精髓操作

對於輸入流流到輸出流是將輸入流Buffer的數據直接轉移到輸出流Buffer中,轉移分爲兩種情況

  1. 整段的Segment數據轉移則是直接從輸出Buffer中脫鏈然後插入輸出Buffer中,直接修改指針效率非常高
  2. 非整段的Segment數據轉移是判斷輸出Buffer最後一個Segment是否寫的下,寫的下的話是數組拷貝,寫不下的話則將輸入Segment一分爲二,將要轉移數據的Segment放第一個,然後按照1方式整段Segment轉移到寫buffer中

並且對於Segment分割的時候有做優化,當需要轉移數據量小於1K的時候是通過數組拷貝的方式將數據寫到新Segment中,大於1K的時候是共享同一個數組,只是修改pos和limit來控制讀取區間。

4. Timeout超時處理

okio的超時分爲兩種

  1. 同步超時TimeOut
  2. 異步超時AsyncTimeout

4.1 同步超時TimeOut

同步超時比較簡單前面看到過,以寫爲例

  public static Sink sink(OutputStream out) {
    return sink(out, new Timeout());//創建同步超時對象
  }

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

在每次寫之前調用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");
    }
  }

如果超時則拋出異常,當然如果循環中某次寫操作進行了很長時間是沒法立即停止的,只能等到下次while循環的時候纔會拋出異常停止,很明顯這樣是有缺陷的,於是還有異步超時。

4.2 異步超時AsyncTimeout

Okio給通過Socket方式創建的流提供的就是異步超時處理,以寫操作爲例

  public static Sink sink(Socket socket) throws IOException {
    if (socket == null) throw new IllegalArgumentException("socket == null");
    if (socket.getOutputStream() == null) throw new IOException("socket's output stream == null");
    AsyncTimeout timeout = timeout(socket);//創建了一個異步超時對象
    Sink sink = sink(socket.getOutputStream(), timeout);
    return timeout.sink(sink);//用AsyncTimeout.sink給sink包裝了一層
  }

再看到AsyncTimeout#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);
          
          boolean throwOnTimeout = false;
          enter();//在進行寫操作的時候會調用一下enter方法
          try {
            sink.write(source, toWrite);
            byteCount -= toWrite;
            throwOnTimeout = true;
          } catch (IOException e) {
            throw exit(e);
          } finally {
            exit(throwOnTimeout);//結束的時候會調用exit
          }
        }
      }

再看超時開始的enter()和結束的exit()

  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);//開始超時任務
  }

  private static synchronized void scheduleTimeout(
      AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
    if (head == null) {//如果head爲null創建一個AsyncTimeout作爲head站個位置沒有實際意義
      head = new AsyncTimeout();
      new Watchdog().start();//開啓一個Watchdog線程進行超時監聽
    }

    long now = System.nanoTime();
    //計算超時時間賦值給timeoutAt屬性
    if (timeoutNanos != 0 && hasDeadline) {
      node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now);
    } else if (timeoutNanos != 0) {
      node.timeoutAt = now + timeoutNanos;
    } else if (hasDeadline) {
      node.timeoutAt = node.deadlineNanoTime();
    } else {
      throw new AssertionError();
    }

    
    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) {//如果是第一次notify喚醒watchdog
          AsyncTimeout.class.notify();
        }
        break;
      }
    }
  }

那梳理下上面代碼就是將AsyncTimeOut根據剩餘超時時間排序,即將超時的排在鏈表頭部,然後啓動了一個WatchDog線程去檢查超時情況,接下來看看WatchDog

  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();//獲取即將超時的AsyncTimeout

           
            if (timedOut == null) continue;
            if (timedOut == head) {//如果獲取到的爲head代表超時隊列沒有任務了return
              head = null;
              return;//return
            }
          }

          
          timedOut.timedOut();//調用timeout.timeout()方法
        } catch (InterruptedException ignored) {
        }
      }
    }
  }

WatchDog則是通過awaitTimeout獲取即將超時的AsyncTimeout對象,如果AsyncTimeout爲head則代表隊列中沒有任務了可以return,否則執行AsyncTimeout.timeout()方法觸發超時

  static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException {
    AsyncTimeout node = head.next;//拿到即將超時的AsyncTimeout
    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());//獲取剩餘超時時間
    if (waitNanos > 0) {//如果大於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);//wait剩餘超時時間
      return null;
    }

   
    head.next = node.next;
    node.next = null;
    return node;
  }
}

awaitTimeout是獲取即將超時的AsyncTimeout,如果剩餘超時時間>0,則wait剩餘超時時間,否則返回超時的AsyncTimeout,而對於Socket,OKio給了默認實現。

@Override protected void timedOut() {
        try {
          socket.close();
        } catch (Exception e) {
          logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
        } catch (AssertionError e) {
          if (isAndroidGetsocknameError(e)) {
            // Catch this exception due to a Firmware issue up to android 4.2.2
            // https://code.google.com/p/android/issues/detail?id=54072
            logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
          } else {
            throw e;
          }
        }
      }

timeout的時候會關閉socket,在看到取消超時任務的方法exit()

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

  public final boolean exit() {
    if (!inQueue) return false;
    inQueue = false;
    return cancelScheduledTimeout(this);//取消超時任務
  }

  private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
    // Remove the node from the linked list.
    for (AsyncTimeout prev = head; prev != null; prev = prev.next) {//從節點中刪除AsyncTimeout
      if (prev.next == node) {
        prev.next = node.next;
        node.next = null;
        return false;//刪除成功則代表未超時
      }
    }

    return true;//沒有找到該節點則超時
  }

exit則是從超時隊列即AsyncTimeout鏈表中刪除該節點,如果刪除成功代表未超時,刪除失敗代表超時了,則會執行newTimeoutException(null)方法。提供個Socket的默認實現是拋出一個SocketTimeoutException

@Override protected IOException newTimeoutException(@Nullable IOException cause) {
        InterruptedIOException ioe = new SocketTimeoutException("timeout");
        if (cause != null) {
          ioe.initCause(cause);
        }
        return ioe;
      }

那麼接下來我們總結下異步超時,以Socket創建流爲例,在創建流的時候會創建一個AsyncTimeout對象,幷包裝生成的流對象,在進行操作的時候調用enter()方法開始超時任務,結束的調用exit()取消超時任務,內部則是將AsyncTimeout以超時時間進行排序,即將超時的排在前面,然後起一個WatchDog線程,從頭部開始獲取即將超時AsyncTimeout,如果還未超時則wait剩餘超時時間,超時了的話則從隊列中移除並調用timeout()方法,在exit()的時候則是看隊列中是否還有AsyncTimeout對象,如果有代表未超時,否則超時了調用newTimeoutException()拋出一個異常停止操作。

發佈了51 篇原創文章 · 獲贊 359 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章