天天看點

OkHttp深入學習(四)——0kiookio的資料存儲實體okio的輸入輸出流okio的執行個體okio的原理介紹

轉載請注明出處:http://blog.csdn.net/evan_man/article/details/51204469

    上一節《OkHttp深入學習(三)——Cache》我們對okhttp中的Cache緩存機制進行了學習,學習了上一節的内容,如果叫我們自己去設計一個緩存機制,那麼我們一定會有了自己的思路,想想還有點小激動。這一節我們繼續來看看okhttp這個教科書中還有什麼值得我們繼續挖掘的東西。果不其然,我們發現了okio這個好東西,該類主要負責對java中io的封裝,使得java中的io流讀寫更加友善,甚至還能提高讀寫效率。okio項目開源位址請戳這裡。在正式學習之前,我們先來了解一下它是如何使用的,随後我們再根據涉及到的内容進行深入學習。

okio的資料存儲實體

    okio提供了ByteString和Buffer兩鐘資料類型,用于存儲資料。     ByteString存儲的是不可變比特序列,可能你不太了解這句話,如果給出final byte[] data你是不是就懂了呢。官方文檔說可以把它看成String的遠方親戚,且這個親戚符合人體工學設計,有沒有感覺很高大上。不過簡單的講它就是能夠對一個byte序列(數組),以指定的編碼格式進行編解碼。目前支援的編解碼規則有hex, base64和 UTF-8等。機智的朋友會說jav.lang.String也可以實作這些功能,是的你說的沒錯,ByteString隻是把這些方法進行了封裝,免去了我們直接輸入類似"UTF-8"這樣的字元串,通過直接調用byteString.utf8()擷取對應的解碼結果,你說哪個友善?使用前面的方法容易引入輸出時的錯誤,在敲UTF-8這幾個字元的時候可能會出現輸入的錯誤。是以總的來講ByteString幹了一件封裝的事情,它把經常用到的幾種編解碼方式進行了封裝,不過封裝的好處避免了犯錯。最後ByteString對于UTF-8格式的解碼還做了優化,在第一次調用utf8()方法的時候得到一個該編碼的String,同時在ByteString内部保留了這個引用 ,當再次調用utf8()的時候則直接傳回這個引用。     Buffer存儲的是可變比特序列,需要注意的是Buffer内部對比特資料的存儲不是直接使用一個byte數組那麼簡單,它使用了一種新的資料類型Segment進行存儲。不過我們先不去管Segment是什麼東西,可以先直接将Buffer想象成一個ArrayList集合就可以了,之是以做這樣的想象是因為Buffer的容量可以動态拓展,從序列的尾部存入資料,從序列的頭部讀取資料。其實Buffer的底層實作遠比ArrayList複雜的多,它使用的是一個雙向連結清單的形式存儲資料,連結清單結點的資料類型就是前面說的Segment,Segment中存儲有一個不可變比特序列,即final byte[] data。使用Buffer的好處在于在從一個Buffer移動到另一個Buffer的時候,實際上并沒有對比特序列進行拷貝,隻是改變了對應Segment的所有者,其實這也采用連結清單存儲資料的好處,這樣的特點在多線程網絡通信中會帶來很大的好處。最後使用Buffer還有另一個好處那就是它實作了BufferedSource和BufferedSink接口,這兩個接口我們後面再講,主要是實作了形如nextInt等方法,友善從buffer中讀取資料,否則Buffer中存儲的byte資料我們并不能直接拿來使用。

    上面的文字有點多,我們對ByteString和Buffer做個小節。ByteString存儲了一個final byte[] data比特數組,通過調用ByteString的相關方法對存儲的比特資料進行相應的編解碼,常用編解碼有UTF-8、Base64和hex。Buffer用雙向連結清單形式存儲了一系列的Segment結點,Segment結點中存儲final byte[] data比特數組;Buffer實作了BufferedSource和BufferedSink接口,通過調用形如buffer.readXX()方法将比特數組進行解碼,傳回XX類型的資料,就像scanner.nextInt一樣使用。

okio的輸入輸出流

    接下來看看okio中與java sdk中的InputStream和OutPutStream兩個對應的接口,Sink和Source。 官方認為sink和source接口相對于java sdk的inputStream和OutputStream更加容易實作,定義sink和source兩個接口的作者認為,sdk中的available()和讀寫單位元組的方法純屬雞肋。

  • Sink定義了四個方法write(Buffer source, long byteCount), flush(), close(), and timeout();
  • Source定義了三個方法long read(Buffer sink, long byteCount), close(), and timeout();

雖然Sink和Source隻定義了很少的方法,這也是為何說它容易實作的原因,但是我們在使用過程中,并不直接拿它進行使用,而是使用BufferedSink和BufferedSource對前面的接口進行再度的封裝,BufferedSink和BufferedSource接口定義了一系列好用的方法。

  • BufferedSink定義了writeUtf8、writeString、writeByte、writeShort、 writeInt、writeLong等常用方法;
  • BufferedSource定義了readByte()、readShort()、readInt()、readLong()、readUtf8()等常用方法;

    最後okio的作者認為,java的sdk對位元組流和字元流進行分開定義這一事情,并不是那麼優雅,特此okio并不進行這樣的劃分。具體做法就是把比特資料都交給Buffer管理,然後Buffer實作BufferedSource和BufferedSink這兩個接口,最後通過調用buffer相應的方法對資料進行編解碼。

okio的執行個體

private static final ByteString PNG_HEADER = ByteString.decodeHex("89504e470d0a1a0a");
public void decodePng(InputStream in) throws IOException {
  BufferedSource pngSource = Okio.buffer(Okio.source(in)); //note1
  ByteString header = pngSource.readByteString(PNG_HEADER.size()); //note2
  if (!header.equals(PNG_HEADER)) {
    throw new IOException("Not a PNG.");
  }
  while (true) {
    Buffer chunk = new Buffer(); //note3
    // Each chunk is a length, type, data, and CRC offset.
    int length = pngSource.readInt(); //note4
    String type = pngSource.readUtf8(4);
    pngSource.readFully(chunk, length); //note5
    int crc = pngSource.readInt();
    decodeChunk(type, chunk);
    if (type.equals("IEND")) break;
  }
  pngSource.close(); //note7
}
private void decodeChunk(String type, Buffer chunk) {
  if (type.equals("IHDR")) {
    int width = chunk.readInt(); //note6
    int height = chunk.readInt();
    System.out.printf("%08x: %s %d x %d%n", chunk.size(), type, width, height);
  } else {
    System.out.printf("%08x: %s%n", chunk.size(), type);
  }
}
           

1、将InputStream轉換為BufferedSource對象 2、将指定長度的比特資料轉換成ByteString資料 3、建立Buffer對象 4、從第一步擷取到的BufferedSource中讀取到int和UTF8資料 5、讀取固定長度的資料到Buffer中 6、Buffer的讀寫跟BufferedSource和BufferedSink一樣,因為它們實作了同樣的接口 7、關閉第一步得到的輸出流。     總的來講okio的使用就是首先建立一個對應的BufferedSource和BufferedSink對象,随後利用相關方法,擷取或寫入ByteString、Buffer、int、long等類型資料,對于Buffer資料可以通過調用與BufferedSource和BufferedSink一樣的方法對已經讀取到的資料進行進一步的解析。

okio的原理介紹

    上面我們對于okio的基本情況進行了介紹,同時給出了一個簡答的使用案例。本小節将對底層的實作進行介紹。介紹的内容以前面的幾個小節涉及到的内容為主,首先對ByteString、Buffer兩個資料類型介紹,随後對Okio.buffer和Okio.source兩個方法進行介紹。

ByteString

按照以往的慣例,首先看看該對象都有哪些域:

static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; //很明顯這個肯定是在進行hex解析的時候被使用到
final byte[] data;
transient int hashCode; //說是String遠方親戚也不假,String也有類似上面的兩個域
transient String utf8; //這個域的存在就是實作了對utf-8解碼的優化,是一個對utf8解碼String的優化
           

ByteString()@ByteString.class

ByteString(byte[] data) {
    this.data = data; 
}
           

構造器 的參數為 一個byte[]類型,不過該構造器隻能被同包的類使用,是以我們建立ByteString對象并不是通過該方法。 我們是如何構造一個ByteString對象?看下面的方法 [email protected]

public static ByteString of(byte... data) {
    if (data == null) throw new IllegalArgumentException("data == null");
    return new ByteString(data.clone());//note1
}
public static ByteString of(byte[] data, int offset, int byteCount) {
    if (data == null) throw new IllegalArgumentException("data == null");
    checkOffsetAndCount(data.length, offset, byteCount);//note2
    byte[] copy = new byte[byteCount];
    System.arraycopy(data, offset, copy, 0, byteCount);//note3
    return new ByteString(copy);
}
           

1、調用clone方法,重新建立一個byte數組,clone一個數組的原因很簡單,我們確定ByteString的data域指向的byte[]沒有被其它對象所引用,否則就容易破壞ByteString中存儲的是一個不可變比特流資料這一限制。 2、邊界檢查 3、老朋友了,将data中指定的資料拷貝到copy數組中去。 接下來對ByteString的相關方法進行介紹,在此并不準備對byteString中的所有方法進行介紹,隻是介紹幾個常用的方法。 toString()@ByteString.class

public String toString() {
    if (data.length == 0) {
      return "ByteString[size=0]";
    }
    if (data.length <= 16) {
      return String.format("ByteString[size=%s data=%s]", data.length, hex());
    }
    return String.format("ByteString[size=%s md5=%s]", data.length, md5().hex());
}
           

很簡單,這就不講了,将data資料分别以hex和md5格式列印 utf8()@ByteString.class

public String utf8() {
    String result = utf8;
    return result != null ? result : (utf8 = new String(data, Util.UTF_8));
}
           

這裡的一個判斷語句,實作ByteString性能的優化,看來優化這個東西還是很容易實作的嘛。第一次建立UTF-8對象的方法是調用new String(data, Util.UTF_8),後面就不再調用該方法而是直接傳回result;發現utf8就是對String的方法進一步封裝,ByteString中很多其它的方法也類似,在此就不講了。 在正式介紹Buffer之前我們先來了解一下Segment。

Segment.class

需要注意的是這是一個雙向連結清單結構!! 按照以往的慣例,首先看看該對象都有哪些域:

static final int SIZE = 2048; //一個Segment存儲的最大比特資料的數量
final byte[] data; //比特數組的引用
int pos; //pos第一個可以讀的位置
int limit; //limit是第一個可以寫的位置,是以一個Segment的可讀資料數量為pos~limit-1=limit-pos;limit和pos的有效值為0~SIZE-1
boolean shared; //目前存儲的data資料是其它對象共享的則為真
boolean owner; //是目前data的所有者
Segment next; //下一個Segment
Segment prev; //前一個Segment
           

Segment()@Segment.class

Segment() {
    this.data = new byte[SIZE];
    this.owner = true; //note1
    this.shared = false; //note2
  }
  Segment(Segment shareFrom) {
    this(shareFrom.data, shareFrom.pos, shareFrom.limit);
    shareFrom.shared = true; //note3
  }
  Segment(byte[] data, int pos, int limit) {
    this.data = data;
    this.pos = pos;
    this.limit = limit;
    this.owner = false; //note4
    this.shared = true; //note5
  }
           

1、采用該構造器表明該資料data的所有者是該Segment,故owner為真 2、資料不是來自其它對象,是以shared為假 3、資料來自其它的Segment,設定參數Segment的Shared為真,表明該Segment資料被别人共享了 4、資料是來自其它對象,是以shared為真 pop()@Seg ment.class

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

将目前Segment從Segment鍊中移除出去。 傳回參數Segment的後一個Segment push()@Segment.class

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

将參數中的Segment壓人調用該方法的Segment結點後面。傳回剛剛壓入的Segment split()@Segment.class

public Segment split(int byteCount) {
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix = new Segment(this);
    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    prev.push(prefix);
    return prefix;
  }
           

該方法用于将Segment一分為二,将pos+1~pos+byteCount-1的内容給新的Segment,将pos+byteCount~limit-1的内容留給自己,然後将前面的新Segment插入到自己前面。這裡需要注意的是,雖然這裡變成了兩個Segment但是實際上byte[]資料并沒有被拷貝,兩個Segment都引用該Segment。

下面介紹的這個方法嘗試将目前Segment和它之前的Segment進行合并,目的是減少Segment數量,如果沒有任何異常出現的話,結果就是将調用該方法的Segment從Segment鍊中被移除出去。 compact()@Segment.class

public void compact() {
    if (prev == this) throw new IllegalStateException();
    if (!prev.owner) return; //note1
    int byteCount = limit - pos; //note2
    int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos); //note3
    if (byteCount > availableByteCount) return; //note4
    writeTo(prev, byteCount); //note5
    pop(); //note6
    SegmentPool.recycle(this); //note7
  }
           

1、如果目前Segment前結點不是自己,且前結點具備可寫性。則進行下列操作,或者說下列操作的前提是目前結點的前結點可以寫入資料 2、記錄目前Segment具有的資料,資料大小為limit-pos-1; 3、統計前結點是否被共享,如果共享則隻記錄Size-limit大小,如果沒有被共享,則加上pre.pos之前的空位置; 4、判斷pre擁有的空餘位置是否夠将目前Segment的全部資料存入進來; 5、将目前Segment中資料寫入pre中 6、将目前Segment從Segment連結清單中移除 7、回收該Segment

下面這個方法的作用就是将調用該方法的Segment中的byteCount個資料寫入到方法參數的Segment中。 writeTo()@Segment.class

public void writeTo(Segment sink, int byteCount) {
    if (!sink.owner) throw new IllegalArgumentException(); //note1
    if (sink.limit + byteCount > SIZE) { //note2
      if (sink.shared) throw new IllegalArgumentException(); //note3
      if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException(); //note4
      System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos); //note5
      sink.limit -= sink.pos;
      sink.pos = 0;
    }
    System.arraycopy(data, pos, sink.data, sink.limit, byteCount);//note6
    sink.limit += byteCount;
    pos += byteCount;
}
           

1、首先判斷參數sink是否具有更改資料的權限,沒有則抛出異常,運作時異常不需要捕獲 2、如果目前寫入的資料在limit~SIZE-1之間得不到滿足,意味着需要先将sink中的資料移動到位元組數組的最前端,是以需要進行下面的判斷,判斷是否可移動不能則抛出異常 3、目前Fragment處于可共享的狀态,抛出異常 4、條件轉換為 byteCount  > SIZE-(sink.limit- sink.pos),意味着目前Segment沒有足夠的空間寫入byteCount資料 5、 這裡我們來複習一下arraycopy方法public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length);  将src中的srcPos~srcPos+length-1的資料複制到dest中的destPos~destPos+length-1位置處; 該方法很重要隻要涉及到數組的移動,最底層都是調用該方法。 那麼note5中的含義就是将pos~limit-1之間的資料移動到0~limit-pos-1位置處,并設定Segment.limit值為limit-pos,sink.pos設定為0; 6、将目前Segment的pos~pos+byteCount-1之間的資料複制到sink的limit~limit+byteCount-1之間,同時設定sink.limit和目前Segment的pos值。

    到此為止我們對于Segment的分析就結束了,但是在我們正式介紹Buffer之前,還需要介紹一下SegmentPool這個類,在compact()方法的最後我們調用egmentPool.recycle(this);方法對該Segment資源進行回收。

SegmentPool.class

按照以往的慣例,首先看看該對象都有哪些域:

static final long MAX_SIZE = 64 * 1024; // 大家是否還記得一個Segment記錄的資料最大長度為2048?是以該Segment相當于能存儲32個Segment對象。不過為何不是32*2048?
static Segment next; //該SegmentPool存儲了一個回收Segment的連結清單
static long byteCount; //該值記錄目前存儲的所有Segment總大小,最大值為MAX_SIZE
           

take()@SegmentPool.class

static Segment take() {
    synchronized (SegmentPool.class) {
      if (next != null) {
        Segment result = next;
        next = result.next;
        result.next = null;
        byteCount -= Segment.SIZE;
        return result;
      }
    }
    return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
}
           

方法很簡單,就是嘗試從SegmentPool的Segment連結清單中取出一個Segment對象,如果連結清單為空則建立一個Segment對象傳回。 recycle()@SegmentPool.class

static void recycle(Segment segment) {
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    if (segment.shared) return; // This segment cannot be recycled.
    synchronized (SegmentPool.class) {
      if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
           

方法同樣很簡單,就是嘗試将參數的Segment對象加入到自身的Segment連結清單中,如果SegmentPool已經滿了,則直接抛棄該Segment,否則加入到連結清單中。     細心的同學肯定發現,SegmentPool中的域和方法都是static修飾的,原因很簡單,SegmentPool的作用就是管理多餘的Segment,不直接丢棄廢棄的Segment,等客戶需要Segment的時候直接從該池中擷取一個對象,避免了重複建立新對象,提高資源使用率。

在正式進入Buffer内容前我們先梳理一下,我們期望從中了解的内容,我們參照ByteString部分的講解,這裡我們需要學習和了解如何建構一個Buffer對象,Buffer中的資料如何管理、存儲,Buffer中的buffer.readXX()方法底層是如何實作的。有了這些目标我們就來分析一下Buffer這個類。

Buffer.class

首先看看該對象都有哪些域:

static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; //很明顯這個肯定是在進行hex解析的時候被使用到
Segment head; //Buffer存儲了一個這樣的head結點,這就是Buffer對資料的存儲結構。位元組數組都是交給Segment進行管理。
long size; //目前存儲的資料的大小
           

Buffer()@Buffer.class

public Buffer() {
}
           

構造器是空的!不過想想構造器是空的也不足為奇,因為即使構造器是空的,其實域中的值也早就被賦了值,即segment=null,size=0; 前面一直講Buffer到Buffer之間的移動資料效率是如何如何的牛逼,這裡我們就一探究竟 copyTo()@@Buffer.class

public Buffer copyTo(Buffer out, long offset, long byteCount) {
    if (out == null) throw new IllegalArgumentException("out == null");
    checkOffsetAndCount(size, offset, byteCount);
    if (byteCount == 0) return this;
    out.size += byteCount; //note1
    Segment s = head;  
    for (; offset >= (s.limit - s.pos); s = s.next) { //note2
      offset -= (s.limit - s.pos);
    }
    // note3
    for (; byteCount > 0; s = s.next) {
      Segment copy = new Segment(s);
      copy.pos += offset;
      copy.limit = Math.min(copy.pos + (int) byteCount, copy.limit);
      if (out.head == null) {
        out.head = copy.next = copy.prev = copy;
      } else {
        out.head.prev.push(copy);
      }
      byteCount -= copy.limit - copy.pos;
      offset = 0;
    }
    return this;
  }
           

1、更改目标Buffer的size值 2、求出從目前Buffer的head需要擴過多少個segment才能能夠滿足offset,之後将對該s往後的byteCount個資料進行拷貝 3、每次插入一個Segment節點,實際建立的new Fragment并不是真的建立一個Segment對象,而是将Segment中的資料進行共享。 4、注意!!Buffer中建立的連結清單是一個雙向連結清單結構,是以out.head.prev等價于擷取到了buffer雙向連結清單的尾部Segment節點,随後在該尾部節點後插入新得到Segment。

下面我們最後看一個readInt方法 readInt()@Buffer.class

public int readInt() {
    if (size < 4) throw new IllegalStateException("size < 4: " + size); //note1
    Segment segment = head;
    int pos = segment.pos;
    int limit = segment.limit;
    //note2
    if (limit - pos < 4) {
      return (readByte() & 0xff) << 24
          |  (readByte() & 0xff) << 16
          |  (readByte() & 0xff) <<  8
          |  (readByte() & 0xff);
    }
   //note3
    byte[] data = segment.data;
    int i = (data[pos++] & 0xff) << 24
        |   (data[pos++] & 0xff) << 16
        |   (data[pos++] & 0xff) <<  8
        |   (data[pos++] & 0xff);
    size -= 4;
    if (pos == limit) { //note4
      head = segment.pop();
      SegmentPool.recycle(segment);
    } else {
      segment.pos = pos;
    }
    return i; //note5
  }
           

1、很明顯一個int資料的位元組數是4,是以必須保證目前buffer的size大于4 2、目前的Segment所包含的位元組數小于4,是以還需要去下一個Segment中擷取一部分資料,是以通過調用readByte()方法一位元組一個位元組的讀取,該方法我們後面進行介紹。 3、目前的Segment資料夠用,是以直接從pos位置起讀取4個位元組資料,然後将其轉換為int資料,轉換方式很簡單就是進行移位和或運算 4、如果pos==limit證明目前head對應的Segment沒有可讀資料,是以将該Segment從雙向連結清單中移除出去,并回收該Segment。如果還有資料則重新整理Segment的pos值。 5、傳回解析得到的int值

readByte()@Buffer.class

public byte readByte() {
    if (size == 0) throw new IllegalStateException("size == 0");
    Segment segment = head;
    int pos = segment.pos;
    int limit = segment.limit;
    byte[] data = segment.data;
    byte b = data[pos++]; //note1
    size -= 1;
    if (pos == limit) { //note2
      head = segment.pop();
      SegmentPool.recycle(segment);
    } else {
      segment.pos = pos;
    }
    return b; //note3
  }
           

1、從目前Segment中擷取一個位元組資料 2、如果pos==limit證明目前head對應的Segment沒有可讀資料,是以将該Segment從雙向連結清單中移除出去,并回收該Segment,如果還有資料則重新整理Segment的pos值。 3、傳回讀到的位元組資料

到此為止我們對okio的資料存儲内容就介紹到這裡,下面我們對Okio.buffer、Okio.source、Okio.sink這幾個方法進行介紹。

Okio.class Okio為我們提供了如下的方法擷取一個Source對象:共計5個方法

Source source(final InputStream in) { return source(in, new Timeout());}
Source source(File file) throws FileNotFoundException{ return source(new FileInputStream(file)); }
Source source(Path path, OpenOption... options) throws IOException { return source(Files.newInputStream(path, options)); }
Source source(final Socket socket) throws IOException{ 
    AsyncTimeout timeout = timeout(socket);
    Source source = source(socket.getInputStream(), timeout);
    return timeout.source(source);
}
           

上面的無論參數是path、file、socket、InputStream最終都是會調用下面的方法建立一個Source對象 Source source(final InputStream in, final Timeout timeout){  } source()@Okio.class

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;
        timeout.throwIfReached();//note1
        Segment tail = sink.writableSegment(1); //note2
        int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); //note3
        int bytesRead = in.read(tail.data, tail.limit, maxToCopy); //note4
        if (bytesRead == -1) return -1;
        tail.limit += bytesRead;//note5
        sink.size += bytesRead;
        return bytesRead;
      }
      @Override public void close() throws IOException {
        in.close();
      }
      @Override public Timeout timeout() {
        return timeout;
      }
      @Override public String toString() {
        return "source(" + in + ")";
      }
    };
  }
           

1、檢查目前線程是否被中斷,是否觸及到了deadLine;滿足任意一個條件都會抛出異常 2、從sink(Buffer對象)中擷取得到可以寫入一個字元的第一個Segment 3、在Segment剩餘寫入空間和目标寫入數byteCount之間選擇最小的數,結果為從InputStream讀入的最大資料量。 4、從InputStream中讀入最多maxToCopy數量的位元組到tail.data中,寫入位置為tail.limit; 5、對buffer的Segment的值進行重新整理;傳回讀取位元組數

Okio為我們提供了如下的方法擷取一個Sink對象:共計6個方法

Sink sink(final OutputStream out) {  return sink(out, new Timeout()); }
Sink sink(File file) throws FileNotFoundException{ return sink(new FileOutputStream(file)); }
Sink appendingSink(File file) throws FileNotFoundException{ return sink(new FileOutputStream(file, true)); }
Sink sink(Path path, OpenOption... options) throws IOException{ return sink(Files.newOutputStream(path, options)); }
Sink sink(final Socket socket) throws IOException{ 
    AsyncTimeout timeout = timeout(socket);
    Sink sink = sink(socket.getOutputStream(), timeout);
    return timeout.sink(sink);
}
           

上面的無論參數是path、file、socket、OutputStream最終都是會調用下面的方法建立一個Sink對象

Sink sink(final OutputStream out, final Timeout timeout){ }  sink()@Okio.class

public static Sink sink(final 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; //note1
          int toCopy = (int) Math.min(byteCount, head.limit - head.pos);//note2
          out.write(head.data, head.pos, toCopy); //note3
          head.pos += toCopy;//note4
          byteCount -= toCopy;
          source.size -= toCopy;
          if (head.pos == head.limit) {//note4
            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 + ")";
      }
    };
  }
           

1、擷取資料源buffer的第一個Segment 2、在Segment的可讀資料和預期寫入位元組數之間選擇一個最小值 3、将Segment的pos位置處到toCopy之間的資料寫入到輸出流中 4、判斷Segment值是否全部讀取完畢,完畢則将該Segment從buffer中移出,并将移出的Segment進行回收

RealBufferedSource.class

本節的最後我們在看看buffer(Source source)和buffer(Sink sink)這兩個方法 BufferedSource buffer()@Okio.class

public static BufferedSource buffer(Source source) {
    if (source == null) throw new IllegalArgumentException("source == null");
    return new RealBufferedSource(source);
}
           

是以Okio的buffer方法實際上建立的是一個RealBufferedSource對象,下面我們看其構造器的内容

RealBufferedSource()@RealBufferedSource.class

public RealBufferedSource(Source source) {
    this(source, new Buffer()); //note1
}
public RealBufferedSource(Source source, Buffer buffer) {
    if (source == null) throw new IllegalArgumentException("source == null");
    this.buffer = buffer;
    this.source = source;
}
           

1、通過new RealBufferedSource(source); 建立的RealBufferedSource對象,系統還會自動的給其建立一個Buffer對象。RealBufferedSource對象的建立到此為止就結束了,但是為了内容的健全,這裡再對RealBufferedSource的個别方法進行簡單介紹。使用RealBufferedSource對Source進行包裝的目的在于,RealBufferedSource提供了很多好用的方法,如readXX,同時既然使用了BufferedSource這個名字,意味着它一次可能讀取多個資料,提高I/O讀寫效率,是以下面我們分别看下它read方法和readInt方法。 read()@RealBufferedSource.class

public long read(Buffer sink, long byteCount) throws IOException {
    if (sink == null) throw new IllegalArgumentException("sink == null");
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (closed) throw new IllegalStateException("closed");
    if (buffer.size == 0) { //note1
      long read = source.read(buffer, Segment.SIZE);
      if (read == -1) return -1;
    }
    long toRead = Math.min(byteCount, buffer.size); //note2
    return buffer.read(sink, toRead); //note3
}
           

1、如果目前buffer為空,則通過source的read方法讀取最多Segment.Size個資料。 2、從Segment.SIZE和目标讀取數中取出的最小值 3、從buffer中取出上面得到的toRead個位元組資料 是以RealBufferedSource的緩存機制(提高I/O讀寫效率的方式)為當buffer為空時從Source中讀取最多2048個位元組數,對于多次調用read方法讀取少量的位元組的情況,很可能隻進行一次真實的I/O流操作,大多數情況是從buffer讀取資料。

接着我們檢視一下readInt方法 readInt()@RealBufferedSource.class

public int readInt() throws IOException {
    require(4);
    return buffer.readInt();
}
           

喂喂喂,那位同學裡下巴掉了诶。對方法就是這麼偷懶,RealBufferedSource結果就是調用buffer的readInt方法。不過require(4)方法第一次見,我們進入看看。 require()@RealBufferedSource.class

public void require(long byteCount) throws IOException {
    if (!request(byteCount)) throw new EOFException(); //沒有讀到要求的資料寶寶表示不開心,後果很嚴重
}
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; //note1
    }
    return true;
}
           

1、底層調用source的read方法讀取目标位元組數到buffer中。 到此為止我們對RealBufferedSource介紹完畢。RealBufferedSource扮演的一個中間的角色,利用source讀取目标位元組的位元組資料,存入buffer。随後又利用buffer将結果位元組流資料按照要求格式進行轉換輸出。

RealBufferedSink.class

接着看buffer(Sink sink)這個方法。 BufferedSink buffer()@Okio.class

public static BufferedSink buffer(Sink sink) {
    if (sink == null) throw new IllegalArgumentException("sink == null");
    return new RealBufferedSink(sink);
}
           

跟前面一樣這裡建立一個RealBufferedSink對象 RealBufferedSink()@RealBufferedSink.class

public RealBufferedSink(Sink sink) {
    this(sink, new Buffer());
}
public RealBufferedSink(Sink sink, Buffer buffer) {
    if (sink == null) throw new IllegalArgumentException("sink == null");
    this.buffer = buffer;
    this.sink = sink;
}
           

RealBufferedSink對象的建立也同樣很簡單,我們直接看看write方法如何實作 write()@RealBufferedSink.class

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

1、将資料寫入buffer中 2、下面我們介紹其emitCompleteSegments()方法 emitCompleteSegments()@@RealBufferedSink.class

public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    long byteCount = buffer.completeSegmentByteCount(); //note1
    if (byteCount > 0) sink.write(buffer, byteCount);
    return this;
}
           

1、該方法我們前面沒有介紹,不過它的結果就是buffer的size減去雙向連結清單存儲資料尾結點的可讀資料大小,即除去尾結點的所有資料都寫入到OutputStream流中。這樣做的好處就是沒必要寫入幾個位元組就直接通過OutputStream寫入,這樣頻繁的io會消耗的資源比較多。因為一個Segment大小為2048是以正常情況等到寫資料大于2048時才會想OutputStream流中寫入資料。這也就是BufferedSink的緩存機制,提高I/O讀寫效率的方法。 接着我們看下writeInt方法 writeInt()@RealBufferedSink.class

public BufferedSink writeInt(int i) throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.writeInt(i); //note1
    return emitCompleteSegments(); //note2
}
           

1、與RealBufferedSource很類似,這裡也是通過buffer來對目标資料進行格式的轉換 2、emitCompleteSegments()方法前面已經介紹過

最後我們介紹一個RealBufferedSink的flush方法 flush()@@RealBufferedSink.class

public void flush() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    if (buffer.size > 0) {
      sink.write(buffer, buffer.size);
    }
    sink.flush();
  }
           

方法很簡單就是将buffer中所有資料寫入到OutputStream流中,然後調用sink的flush方法。

到此為止我們對okio的介紹就結束了,回顧一下我們學習的内容。     首先okio的存儲資料的類型ByteString,Buffer各自都維護一個byte[]數組,提供了一系列方法實作了位元組序列和目标格式之間的轉換。Buffer并不直接對byte[]進行操作,而是操作管理Segment對象,Buffer中有一個由多個Segment組成的雙向連結清單,同時okio還提供了一個SegmentPool用于對廢棄Segment的管理,不用的Segment并不直接丢棄而是丢入這個池中,提高了Segment的使用率。Okio中Segment的建立都是通過SegmentPool來擷取的。     okio提供的輸入輸出流分别為Sink Source,分别定義了write和read方法,實作對位元組流的輸入輸出。不過客戶使用一般都是對BufferedSource和BufferedSink進行操作,這兩個接口定義了大量的方法用于對位元組資料的編解碼,同時通過該對象能有效提高I/O使用效率。     okio中的BufferedSink和BufferedSource這兩個接口很重要,okio的輸入輸出流分别實作了這兩個接口,而且buffer也同樣實作這兩個接口,是以buffer的方法和okio的輸入輸出流有很多方法都是相同的,這一點在實際使用過程中帶來了極大的便利。

 本篇是整個okhttp系列部落格的完結篇。回顧一下該部落格系列的内容,《OkHttp深入學習(一)——初探》對okhttp開源項目的使用方式以及經常用到的幾個類進行簡單學習,随後《OkHttp深入學習(二)——網絡》對okhttp的網絡通路底層如何實作進行分析,接着《OkHttp深入學習(三)——Cache》對okhttp的網絡緩存底層如何實作進行解析,最後本篇《OkHttp深入學習(四)——0kio》對okhttp的okio子項目如何提高I/O讀寫效率與使用方式進行深入的分析和介紹。感謝各位的閱讀!