okio分析

Okio是一個對原有的java.iojava.nio進行改進的IO庫,使IO操作更加高效和方便。Okio的高效主要體現在三個方面:

  • 一它對數據進行了分塊處理,這樣在大數據IO的時候可以以塊爲單位進行IO,這可以提高IO的吞吐率。
  • 二它對這些數據塊使用鏈表進行管理,這可以僅通過移動“指針”就進行數據的管理,而不用真正去處理數據,而且對擴容來說也十分方便。
  • 三對閒置的塊進行管理,通過一個塊池(SegmentPool)的管理,避免系統GC和申請byte時的zero-fill。
    其他的還有一些小細節上的優化,比如如果你把一個UTF-8的String轉爲ByteString,ByteString會保留一份對原來String的引用,這樣當你下次
    需要decode這個String時,程序通過保留的引用直接返回對應的String,從而避免了轉碼過程。

Okio的方便主要體現在,它對數據的讀取寫入進行了封裝,調用者可以十分方便的進行各種值(string,short,int,hex,utf-8,base64等等)的轉化,還有一點就是它爲所有的Source和Sink提供了超時操作,這在Java原生的IO裏是沒有的。

Okio幾個基礎的類和接口:

  • 類Segment
    一個Segment相當於一個數據塊(由一個byte數組構成),一般存在於一個雙向循環隊列(也就是buffer)或單鏈表(也就是SegmentPool)中,通過pop()和push(Segment segment)方法可以Segment進行入隊和出隊操作。Segment部分重要字段如下:
1
2
3
4
5
int SIZE:Segment大小,2kb
int pos:指向下一個可讀的byte
int limit:指向下一個可寫的byte
Segment next:相當於鏈表的指針,指向下一個Segment
Segment prev:相當於鏈表的指針,指向上一個Segment
  • 類SegmentPool
    一個Segment池,由一個單向鏈表構成。該池負責Segment的回收和閒置Segment的管理,一般Segment都是從該池獲取的。該池是線程安全的。
  • 接口Sink,BufferedSink
    Sink和java.io中的OutputStream類似,BufferedSink是一個對Sink進行擴展的接口。使用OutputStream時,在傳輸不同的數據是需要對OutputStream進行不同的包裝,比如用DataOutputStream進行原始數據的IO,用BufferedOutputStream進行帶緩存的數據IO,用OutputStreamWriter進行字符編碼。對於Sink來說,只需要使用BufferedSink就可以實現以上所有的情況。
  • 接口Source和BufferedSource
    Source和java.io中的InputStream類似,BufferedSource是一個對Source進行擴展的接口。使用InputStream時,在傳輸不同的數據是需要對InputStream進行不同的包裝,比如用DataInputStream進行原始數據的IO,用BufferedInputStream進行帶緩存的數據IO,用OutputStreamReader進行字符編碼。對於Source來說,只需要使用BufferedSource就可以實現以上所有的情況。

    java.io是由於使用了裝飾者模式,才導致了不同的Stream操作需要一層包一層,而Okio沒有在這裏沒有使用裝飾者模式,也就不會出現一大堆類的情況。

  • 類Buffer
    Buffer是一個不固定大小的byte序列(由一個節點爲Segment的雙向循環隊列鏈表構成),它充當Sink、Source、InputStream和OutputStream間的高效緩存區,由於Buffer實現了BufferedSource和BufferedSink這兩個接口,所以可以很方便的對其進行IO操作。Buffer在多線程編程裏很有用,比如一個負責網絡的線程可以通過這種方式和工作線程進行數據交換,但是又不發生數據的複製。
  • 類ByteString
    ByteString是一個固定大小的byte序列(由一個byte數組構成)。String是Java經常使用到的一個基本類型,ByteString對String進行了封裝,爲byte和String間的轉換和String不同值間的轉換(UTF-8編解碼,Hex編解碼,Base64編解碼,ASCIll編解碼)提供了十分方便的操作。
  • 類AsyncTimeout
    AsyncTimeout爲所有的Source和Sink提供了超時功能,Java原生的IO並沒有超時功能,而AsyncTimeout填補了這點。AsyncTimeout的實現原理是:AsyncTimeout相當於一個節點,每個節點都帶有tiemout信息,程序維護一條由AsyncTimeout節點組成優先隊列鏈表(剩餘超時時間越小的排越前),然後通過後臺的一個守護線程,不斷的去輪詢這條鏈表,如果對應節點超時就調用Interrupted進行中斷,否則調用wait進行等待。

Okio高效在哪裏

  1. 前面說過Okio之所以高效是因爲在底層的數據結構上,它維護了一個由Segment構成的鏈表循環隊列,一個Segment相當於一個數據塊。這樣的好處很明顯。因爲在一塊數據塊的進行IO的過程中是沒有中斷的,相比於每次只讀一個byte,單位時間內IO的數據量當然更高。那是不是Segment越大越好?當然不是。因爲Segment內數據的IO還是以byte爲單位的,如果Segment過大的話,數據就不能很好的進行分塊。想象下把數據只分爲一個大的Segment,那每次IO不就是以byte爲單位了嗎?那一個Segment的大小爲多少比較合適,在我看來,最好和計算機中的一個頁面大小一致。
  2. 另一方面,由於使用了鏈表,這使得數據的管理十分高效,因爲只要移動指針就可以進行數據的移動。SegmentPool是Segment組成的單向鏈表,負責Segment的回收和閒置Segment的維護。
    我們可以看看SegmentPool是如何維護閒置Segment的,SegmentPool提供了兩個方法,take()用於獲取一個閒置Segment,recycle(Segment segment)用於回收一個Segment。
1
2
3
4
5
6
7
8
9
10
11
12
13
Segment take() {
synchronized (this) {
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的next指向Pool中閒置的Segment,所以直接返回next指向的Segment就可以了,當沒有閒置的Segment是就新建一個返回。

1
2
3
4
5
6
7
8
9
10
void recycle(Segment segment) {
if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
synchronized (this) {
if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
byteCount += Segment.SIZE;
segment.next = next;
segment.pos = segment.limit = 0;
next = segment;
}
}

在Pool不滿的情況下,recycle只要將對應的segment插入到單向鏈表的頭部(也就是next指向segment的前面)就相當於回收了該segment。

okio的高效主要體現在Buffer中,所以我們可以看看Buffer的實現。由於每個Segment裏都有pos和limit兩個下標,這和Java NIO裏的Buffer有點像,只要通過對pos和limit進行操作,我們就可以判斷當前Segment是否寫滿(limit==SIZE)、是否讀完(pos==limit),這就提高了IO的效率了。再看看readFrom()writeTo()方法都是儘量以塊大小進行IO的。而且爲了進行減少調用系統申請內存產生的消耗,Buffer使用了SegmentPool進行Segment的回收和申請。

Okio方便在哪裏

  1. Okio之所以方便主要體現在它對許多常用的操作進行了封裝,主要體現在BufferedSink和BufferedSource接口爲調用者提供了豐富的方法,想基本數據(short,int,long,string)的IO還有Sink和Source之間的轉換等,它都提供了相應的方法;另一方面,Okio創建了一個新的數據類型ByteString,ByteString對String進行了封裝,爲byte和String間的轉換和String不同值間的轉換(UTF-8編解碼,Hex編解碼,Base64編解碼,ASCIll編解碼,大小端轉換)提供了十分方便的操作,具體可以查看其相應的方法。
  2. Okio通過AsyncTimeout所有的Source和Sink提供了超時操作。
    Timeout是AsyncTimeout的基類,想看看Timeout實現了什麼?Timeout對原本的概念進行可擴展,它有兩個屬性,Timeouts和Deadlines。
    • Timeouts:代表一個時間段,表示等待一個操作執行完畢的最長時間。Timeouts一般用來檢測類似網絡中斷等問題。
    • Deadlines:代表一個時間點,表示一個job(包含多個操作)最長執行到某個時間點。Deadlines可以爲一個job設定它的執行時間上限,比如一個對電池電量敏感的APP爲了節省電池消耗,也許會在APP content的預加載上設置Deadlines。
      AsyncTimeout的主要字段有:
1
2
3
private static AsyncTimeout head:私有靜態變量head,用來當單鏈表的頭
private AsyncTimeout next:指向下一個AsyncTiemout的節點
private long timeoutAt:根據timeout計算出的timeout時間點

要實現對應Source和Sink的Timeout管理只需要管理這條AsyncTimeout優先隊列鏈表就可以了。添加Timeout對應入隊(scheduleTimeout方法),取消超時對應出隊(cancelScheduledTimeout方法)。守護線程Watchdog,會不斷的去輪詢這條鏈表,如果對應節點超時就調用Interrupted進行中斷,否則調用wait進行等待。

Okio中的其他類:

  • RealBufferedSinkRealBufferedSource,不帶緩存的Sink和Source,實現方式是在每次write或read之後都調用emitCompleteSegments()方法(emitCompleteSegments()方法會將Buffer中的數據flush掉)。
  • ForwardingSinkForwardingSource,將調用委託給其他Sink或Source的抽象類,在子類化的時候有用。比如當需要實現一個匿名Sink或Source時,就可以用這個。
  • GzipSinkGzipSource,實現了Gzip的Sink和Source。
  • DeflaterSinkInflaterSource,實現了ZLIB壓縮和解壓的Sink和Source,在DeflaterSink這個類中,由於每次調用flush()程序都會對整個Buffer進行同步壓縮,所以官方建議只在程序有必要時才主動去調用flush(),否則頻繁的調用flush()會引起性能上的問題。

  • 從上面這些類看出,我們完全可以對Sink和Source進行擴展,以實現一些針對不同compression或encryption的Sink和Source,比如可以編寫針對H264的H264EnCodeSink和H264DeCodeSource。
轉載:http://www.baitouwei.com/2015/01/08/okio/

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