okio源碼學習指北(一)

okio是okhttp的底層io庫,是一個我用起來比較方便io庫。然而知其然不知其所以然,所以我決定研究一下okio的源碼,這篇文件主要記錄下我學習okio源碼的心得。

okio的緩存類Segment.java

okio的原理是將要write、read的數據以byte[]的形式先緩存起來,然后再將緩存的數據write到目的地或者read成想要形式。而做到緩存數據的類就是Segment.java
我們直接看源碼:

final class Segment {
  /** 一個Segment可以緩存數據的大小、源碼中定成8KB */
  static final int SIZE = 8192;

  /** 將Segment緩存的數據分享出去條件 */
  static final int SHARE_MINIMUM = 1024;
  
  /** 緩存的數據*/
  final byte[] data;

  /** 數據可以被讀取的起點*/
  int pos;

  /** 數據可以被讀取的終點*/
  int limit;

  /** 數據是分享出去、或者分享得到的*/
  boolean shared;

  /** 對數據擁有操作pos、limit的權限,分享得到的數據是沒有操作權限的*/
  boolean owner;

  /** 下一個Segment節點*/
  Segment next;

  /** 上一個Segment節點*/
  Segment prev;

  /** 一個擁有操作數據權限的Segment構造方法*/
  Segment() {
    this.data = new byte[SIZE];
    this.owner = true;
    this.shared = false;
  }

  /** 一個分享得到的Segment構造方法*/
  Segment(Segment shareFrom) {
    this(shareFrom.data, shareFrom.pos, shareFrom.limit);
    shareFrom.shared = true;
  }

  /** 一個分享得到的Segment構造方法*/
  Segment(byte[] data, int pos, int limit) {
    this.data = data;
    this.pos = pos;
    this.limit = limit;
    this.owner = false;
    this.shared = true;
  }

  /**
   * 雙向鏈表pop操作
   * 鏈表中刪除自己,返回下一個節點操作
   */
  public @Nullable Segment pop() {
    Segment result = next != this ? next : null;
    prev.next = next;
    next.prev = prev;
    next = null;
    prev = null;
    return result;
  }

  /**
   * 雙向鏈表push操作
   * 自己后面加入segment節點
   */
  public Segment push(Segment segment) {
    segment.prev = this;
    segment.next = next;
    next.prev = segment;
    next = segment;
    return segment;
  }

  //=================================================
  //    之后都是優化Segment緩存數據的函數
  //=================================================

  /**
   * 分割操作
   * 把自己數據分割byteCount個出去
   */
  public Segment split(int byteCount) {
    
    //如過byteCount<= 0 或者 自己并沒有byteCount個數據,則拋出異常
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix;

    if (byteCount >= SHARE_MINIMUM) {
      //如果要分割的數據數達到分享條件,把自己分享出去
      prefix = new Segment(this);
    } else {
      //如果沒達到分享條件則從SegmentPool緩存池中獲取一塊可用的緩存空間
      prefix = SegmentPool.take();
      //把自己的數據復制給prefix
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }
    //分割操作的本質就是改變自己的pos、和prefix的limit達到分割的目的
    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    /**
     * 將分割的數據push到自己之前的節點
     * 因為prefix的數據和在自己的數據順序關系是prefix在自己之前
     */
    prev.push(prefix);
    //return分割出去的Segement
    return prefix;
  }

  /**
   * 寫入操作
   * 將自己的byteCount個數據寫入另一個Segment
   */
  public void writeTo(Segment sink, int byteCount) {

    //如果另一個Segment沒有操作權限,直接拋出異常
    if (!sink.owner) throw new IllegalArgumentException();

    //如果另一個Segment沒有足夠的連續空間寫入,則嘗試壓縮data[]使其擁有足夠的連續空間
    if (sink.limit + byteCount > SIZE) {
      //如果另一個Segment分享出去了,那么就不能壓縮data[],拋出異常
      if (sink.shared) throw new IllegalArgumentException();
      //如果另一個Segment壓縮data[]之后還是沒有只夠的連續空間,拋出異常
      if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
      //壓縮data[]操作即:將數據的pos移動到data[0]的位置
      System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
      sink.limit -= sink.pos;
      sink.pos = 0;
    }

    //經過或沒壓縮data[]操作后,將自己的byteCount個數據寫入另一個Segment
    System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
    sink.limit += byteCount;
    pos += byteCount;
  }

  /**
   * 合并Segement操作
   * 嘗試將自己和前面一個節點合并,壓縮data[]達到優化緩存的目的
   */
  public void compact() {

    //如果前面一個節點就是自己,拋出異常
    if (prev == this) throw new IllegalStateException();

    //如果前面一個節點沒有操作權限則不能合并
    if (!prev.owner) return;
 
    //計算自己有多少數據
    int byteCount = limit - pos;

    //計算前面一個節點有多少剩余空間
    int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);

    //如果自己的數據個數大于前面節點的剩余空間則不能進行合并
    if (byteCount > availableByteCount) return; 
    
    //將數據寫入前一個節點
    writeTo(prev, byteCount);

    //雙向鏈表中刪除自己
    pop();

    //緩存池回收
    SegmentPool.recycle(this);
  }

}

從源碼可以看出Segment是一個雙向鏈表結構,源碼中有一個SegmentPool(緩存池)。這個類是用來維護Segment的,作用是回收利用Segment。我們來看下源碼:

final class SegmentPool {

  /** 緩存池的最大SIZE為64KB
    * 在Segment源碼中我們知道1個Segment的大小為8KB
    * 即緩存池可以回收利用的Segment最多為8個
    */
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  /** 第一個可以回收再利用的Segment*/
  static @Nullable Segment next;

  /** 緩存池現在擁有可再利用的緩存大小,一定是8KB的倍數 */
  static long byteCount;

  private SegmentPool() {
  }
  
  /** 
    * 獲取一個擁有操作權限的Segement
    */
  static Segment take() {
    synchronized (SegmentPool.class) {
      //如果緩沖池中有就從緩存池中獲取
      if (next != null) {
        Segment result = next;
        next = result.next;
        result.next = null;
        byteCount -= Segment.SIZE;
        return result;
      }
    }
    //如果沒有則直接new一個操作權限的Segement
    return new Segment();
  }
  
  /** 
    * 回收Segment
    */
  static void recycle(Segment segment) {

    //如果segment還沒從雙向鏈表中脫離出則拋出異常
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();

    //如果segement是分享得來的或分享出去的、則不能被回收
    if (segment.shared) return; 

    synchronized (SegmentPool.class) {
      //如果緩存池已經滿了,不能回收這個segment了
      if (byteCount + Segment.SIZE > MAX_SIZE) return; 

      //將segment回收到緩存池鏈表
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
}

static void recycle(Segment segment){...}函數可以看出,SegmentPool的緩存池是用一個單向鏈表來維護的,與Segment用雙向鏈表維護不同。Segment中使用雙向鏈表是為了讓數據的壓縮、分割、合并操作,更加方便和高效。而SegmentPool沒有這一需求,只要保證static Segment take(){...}能得到Segement就好。

okio基本io結構

看完緩存我們來看看最重要的io操作


上面的類圖描述了一個最基本的io操作需要用到的東西,之后會講到。
Sink和Source是okio庫中最基礎io操作接口,定義了任何read、write操作都是從Buffer持有的Segment緩存中獲取數據再進行read、write。那么如何把數據read到緩存中,以及如何將緩存中的數據write到目的地呢?以write為例我們看Okio類中的一段源碼:

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

          //獲取Buffer的Segment緩存
          Segment head = source.head; 

          //計算要寫入的數據個數
          int toCopy = (int) Math.min(byteCount, head.limit - head.pos);

          //使用OutputStream將緩存數據寫入目標
          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);
          }
        }
      }
    };
  }

上面這段源碼可以看出Sink實際上是OutputStream的包裝,把緩存在Segment中的數據寫入目的地還是由OutputStream進行。同理Source也是InputStream的包裝,將數據讀取到Segment緩存還是由InputStream進行。
接下來我們看BufferedSinkBufferedSouce,這2個接口定義了各種類型的數據寫入Segment函數和把Segment數據以各種類型讀出的函數,方便大家使用。具體的實現實在Buffer中進行的。
舉個例子,BufferedSink接口中的定義了這么一個把數據源寫入緩存的函數

//將source[]中的數據從offset位置寫byteCount個到Segment緩存
BufferedSink write(byte[] source, int offset, int byteCount) throws IOException;

Buffer中實現如下

@Override 
public Buffer write(byte[] source, int offset, int byteCount) {

    //如果你要寫入目標的數據源source為空,拋出異常
    if (source == null) throw new IllegalArgumentException("source == null");
    
    //檢查offset、byteCount、source.length是否有數據越界的關系,有則拋出異常
    checkOffsetAndCount(source.length, offset, byteCount);
    
    //計算要寫入數據的終點
    int limit = offset + byteCount;
    
    //如果要寫入數據的偏移位置小于要寫入數據的終點,開始寫入
    while (offset < limit) {

      //獲取一個擁有操作權限的Segment
      Segment tail = writableSegment(1);

      //計算要寫入這個Segment的字節數
      int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
      
      //寫入
      System.arraycopy(source, offset, tail.data, tail.limit, toCopy);
      offset += toCopy;
      tail.limit += toCopy;
    }

    size += byteCount;
    return this;
}

/**
* 獲取一個可用容量大等于minimumCapacity,且擁有權限的Segment
*
* @param minimumCapacity 最小可用容量
*/
Segment writableSegment(int minimumCapacity) {
    if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();

    if (head == null) {
      //如果Buffer中沒有Segment緩存,則直接從緩存池中獲取一個Segment并將其作為head節點
      head = SegmentPool.take(); 
      return head.next = head.prev = head;
    }
    //獲取雙向鏈表的最后一個節點
    Segment tail = head.prev;
    if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
      /**
       * 如果最后一個節點沒有足夠的容量,或者沒有操作權限。
       * 則從緩沖池中獲取一個Segment,并push到雙向鏈表的最后一個節點
       */
      tail = tail.push(SegmentPool.take()); 
    }
    return tail;
}

同理,舉一個BufferedSouce中的讀取函數例子

//把Segement緩存中的數據讀取一個byte,以byte形式返回
byte readByte() throws IOException;

Buffer中實現如下

@Override 
public byte readByte() {
    //如果緩存數據size==0,拋出異常
    if (size == 0) throw new IllegalStateException("size == 0");

    //獲取緩存的頭節點
    Segment segment = head;
    int pos = segment.pos;
    int limit = segment.limit;
    byte[] data = segment.data;

    //讀取1字節
    byte b = data[pos++];
    size -= 1;
    
    if (pos == limit) {
      /**
      *如果讀取后head節點沒有可以讀取的數據了
      *則pop掉head節點,并且把head節點的下一個節點作為head
      */
      head = segment.pop();
      //緩存池回收
      SegmentPool.recycle(segment);
    } else {
      segment.pos = pos;
    }

    return b;
}

現在下來理一下我們知道了的write、read流程:

  • write
  1. 數據源 >> Segment緩存,由Buffer實現
  2. Sink通過包裝OutputStream將Segment緩存數據 >> 目的地(文件、Socket......), 由Okio類實現
  • read
  1. Source通過包裝InputStream將數據源(文件、Socket......) >> Segment緩存,由Okio類實現
  2. Segment緩存 >> 各種類型的數據, 由Buffer實現

那么如何將Okio實現的Sink、Source與Buffer連接起來呢?
答案是RealBufferedSinkRealBufferedSource

我們先來看看RealBufferedSource的部分源碼

final class RealBufferedSource implements BufferedSource {

  /**讀寫Segment緩存的Buffer*/
  public final Buffer buffer = new Buffer();

  /**包裝了InputStream的Source*/
  public final Source source;

  /**用來判斷輸入流是否關閉*/
  boolean closed;

  /**構造函數傳入包裝了InputStream的Source*/
  RealBufferedSource(Source source) {
    if (source == null) throw new NullPointerException("source == null");
    this.source = source;
  }

  /**以字節形式讀取1個字節*/
  @Override 
  public byte readByte() throws IOException {
    /**
    * read前先請求說明需要從buffer的Segment中獲取1個byte,
    * 1.如果buffer的Segment中有1個byte,則不進行任何操作
    * 2.如果buffer的Segment中沒有1個byte
    *   則使用Source包裝的InputStream讀取Segment.SIZE個數據到buffer的Segment中
    *   如果InputStream讀取不到,則拋出異常
    */
    require(1);

    //buffer的Segment中數據以byte
    return buffer.readByte();
  }

  @Override 
  public void require(long byteCount) throws IOException {
    if (!request(byteCount)) throw new EOFException();
  }
  
  /**讀取到Segment*/
  @Override 
  public boolean request(long byteCount) throws IOException {
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (closed) throw new IllegalStateException("closed");
    while (buffer.size < byteCount) {
      if (source.read(buffer, Segment.SIZE) == -1) return false;
    }
    return true;
  }
}

可以看出Okio類創建的包裝了InputStream的Source實例通過構造函數傳入RealBufferedSource類,RealBufferedSource類自己持有一個Buffer,這樣就將read流程的1、2步驟連接起來了

同理,我們再看看RealBufferedSink的部分源碼

final class RealBufferedSink implements BufferedSink {
  
  /**讀寫Segment緩存的Buffer*/
  public final Buffer buffer = new Buffer();

  /**包裝了OutputStream的Sink*/
  public final Sink sink;

  /**用來判斷輸出流是否關閉*/
  boolean closed;

  /**構造函數傳入包裝了OutputStream的Sink*/
  RealBufferedSink(Sink sink) {
    if (sink == null) throw new NullPointerException("sink == null");
    this.sink = sink;
  }

  /**寫入Segment*/
  @Override 
  public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException {
    
    //如果輸出流關閉了,拋出異常
    if (closed) throw new IllegalStateException("closed");
    
    //向buffer的Segment緩存寫入數據
    buffer.write(source, offset, byteCount);

    //完成寫入Segment緩存,提交給skin包裝的OutputStream將緩存寫到目的地
    return emitCompleteSegments();
  }
  
  /**Segment寫到目的地*/
  @Override 
  public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
   
    //先獲得t緩存中有多少數據
    long byteCount = buffer.completeSegmentByteCount();

    //調用sink包裝的OutputStream將緩存寫到目的地
    if (byteCount > 0) sink.write(buffer, byteCount);

    return this;
  }
}

可以看出Okio類創建的包裝了OutputStream的Sink實例通過構造函數傳入RealBufferedSink類,RealBufferedSink類自己持有一個Buffer,這樣就將write流程的1、2步驟連接起來了
到此Okio的read、write流程學習完畢。

其他無關的廢話

剛走上開發的道路,各位大佬多多指教。另外,杭州3個月工作經驗,4個月實習經驗的Android開發有需要的嗎?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容