OkHttp深入學習(四)——0kio

轉載請註明出處: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對象?看下面的方法
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()@Segment.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讀寫效率與使用方式進行深入的分析和介紹。感謝各位的閱讀!










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章