學習基于OkHttp的網絡框架(一)Okio詳解

繼承的缺點


如果要給一個類擴展功能應該怎么做?

繼承與組合:
復用代碼是進行程序設計的一個重要原因,組合和繼承被委以重任,其中繼承更是面向對象的基石之一,但相比于組合,繼承其實有諸多缺點。組合只要持有另一個類的對象,就可以使用它暴露的所有功能,同時也隱藏了具體的實現(黑盒復用);組合之間的關系是動態的,在運行才確定;組合有助于保持每個類被封裝,并被集中在單個任務上(單一原則)。而然,類繼承允許我們根據自己的實現來覆蓋重寫父類的實現細節,父類的實現對于子類是可見的(白盒復用);繼承是在編譯時刻靜態定義的,即是靜態復用,在編譯后子類已經確定了;繼承中父類定義了子類的部分實現,而子類中又會重寫這些實現,修改父類的實現,這是一種破壞了父類的封裝性的表現。總之組合相比繼承更具靈活性。即便如此,我們有不得不使用繼承的理由:

向上轉型,復用接口

如果用繼承來擴展功能會遇到上面所說的諸多問題,對父類的方法做了修改的話,則子類的方法必須做出相應的修改。所以說子類與父類是一種高耦合,并且因為子類是靜態的,當擴展的功能是多種情況的組合的話,你必須枚舉出所有的情況為它們定義子類。比如咖啡店里有四種咖啡:


現在還可以給咖啡添加額外的四種調料
Milk,Chocolate,Icecream,Whip如果為每一種咖啡和調料的組合編寫子類將有64種情況,顯然這種類型體系臃腫是無法接受的!
那么有什么方法可以即保留向上轉型的繼承結構,又避免繼承帶來的問題呢 ?

裝飾者優化繼承結構


Decorator Pattern

ConcreteComponentComponent是原有的繼承結構,相比于直接在ConcreteComponent上開刀來擴展功能,我們重新定義了一個Decorator類,Decorator用組合的方式持有一個Component對象,同時繼承Component這樣就實現了保留向上轉型的繼承結構的同時,擁有組合的優點:

  1. 通過動態的方式來擴展一個對象的功能
  1. 通過裝飾類的排列組合,可以創造恒多不同行為的組合
  2. 裝飾類Decorator和構建類ConcreteComponent可以獨立變化

OKio原理分析


好了,終于進入正題了。和Java的io流相同,Okio的整體設計也是裝飾者模式,一層層的拼接流(Stream)正是在使用使用裝飾者在裝飾的過程。

  • Okio封裝了java.io,java.nio的功能使用起來更方便
  • Okio優化了緩存,使io操作更高效

Source和Sink流程

SourceSink類似于InputStreamOutputStream,是io操作的頂級接口類,SourceSink中只定義了三個方法:

  public interface Source extends Closeable {
  /**
   * 定義基礎的read操作,該方法將字節寫入Buffer
   */
  long read(Buffer sink, long byteCount) throws IOException;

  /** Returns the timeout for this source. */
  Timeout timeout();

  /**
   * Closes this source and releases the resources held by this source. It is an
   * error to read a closed source. It is safe to close a source more than once.
   */
  @Override void close() throws IOException;
}

Sink的結構是相同的,就不廢話了。那么Source和Sink的具體實現在哪里呢?Okio類提供了靜態的方法生產SinkSource,這個方法也比較簡單,將InputStream中的數據寫入到BufferSegment中,BufferSegment是Okio對io流操作進行優化的關鍵類,后面在詳細討論,先把讀寫操作的流程走完。

  private static Source source(final InputStream in, final Timeout timeout) {
    
    //.....

    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);
          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
          //寫入Segment 
          int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
          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;
        }
      }

      //.....
}

不同的讀取操作定義在BufferedSource中,它同樣也是個接口:

BufferedSource

BufferedSource的具體實現是RealBufferedSource,可以看到RealBufferedSource其實是個裝飾類,內部管理Source對象來擴展Source的功能,同時擁有Source讀取數據時用到的Buffer對象。

final class RealBufferedSource implements BufferedSource {
  public final Buffer buffer = new Buffer();
  public final Source source;
  boolean closed;

    //....
    @Override 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");

    //先將數據讀到buffer中
    if (buffer.size == 0) {
       //source是被裝飾的對象
      long read = source.read(buffer, Segment.SIZE);
      if (read == -1) return -1;
    }

    long toRead = Math.min(byteCount, buffer.size);
    return buffer.read(sink, toRead);
  }
  //...
}

小結一下:
Source對象每次read,Sink對象每次write都需要一個Buffer對象,Buffer管理者循環雙向鏈表Segment,每次讀寫數據都先保存在segment中進行緩沖,BufferedSourceBufferedSink進行讀寫操作時都是間接調用BufferSegment的操作來完成的,整個過程層層嵌套還是有點繞的。
InputStream--Source--BufferedSource--Buffer--segment--Buffer--Sink--BufferedSink--OutputStream

為什么Okio更高效

buffer注釋中說明了Okio的高效性:

  1. 采用了segment的機制進行內存共享和復用,避免了copy數組;
  2. 根據需要動態分配內存大??;
  3. 避免了數組創建時的zero-fill,同時降低GC的頻率。

Segment和SegmentPool:
Segment是一個循環雙向列表,內部維護者固定長度的byte[]數組:

  static final int SIZE = 8192;
  /** Segments 用分享的方式避免復制數組 */
  static final int SHARE_MINIMUM = 1024;
  final byte[] data;
  /** data[]中第一個可讀的位置*/
  int pos;
  /** data[]中第一個可寫的位置 */
  int limit;
  /**與其它Segment共享  */
  boolean shared;
  boolean owner;

  Segment next;
  Segment prev;
  /**
   * 將當前segment從鏈表中移除
   */
  public Segment pop() {
     //....
  }
  /**
   * 將一個segment插入到當前segment后
   */
  public Segment push(Segment segment) {
    //....
  }

SegmentPool是一個Segment池,由一個單向鏈表構成。該池負責Segment的回收和閑置Segment的管理,也就是說Buffer使用的Segment是從Segment單向鏈表中取出的,這樣有效的避免了GC頻率。

  /** 總容量 */
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  /**用Segment實現的單向鏈表,next是表頭*/
  static Segment next;

  /** Total bytes in this pool. */
  static long byteCount;
  
  
  //回收閑置的segment,插在鏈表頭部
  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;
    }
  }
  //從鏈表頭部取出一個
    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.
  }

Segment中還有兩個特殊的方法split()compact()split()根據當前的Segment產生一個新的Segment,新的Segment與原來的Segment共用同一個data[]數組,但是改變了讀寫的標記位poslimit,從原來的
[pos..limit]拆分為[pos..pos+byteCount]和[pos+byteCount..limit],從而避免了復制數組帶來的性能消耗。前一個和自身的數據量都不足一半時,compact()會對segement進行壓縮,把自身的數據寫入到前一Segment中,然后將自身進行回收,使Segment的利用更高效!

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

    // We have two competing performance goals:
    //  - Avoid copying data. We accomplish this by sharing segments.
    //  - Avoid short shared segments. These are bad for performance because they are readonly and
    //    may lead to long chains of short segments.
    // To balance these goals we only share segments when the copy will be large.
    if (byteCount >= SHARE_MINIMUM) {
      prefix = new Segment(this);
    } else {
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    prev.push(prefix);
    return prefix;
  }

Okio實戰

Okio封裝了io操作底層操縱字節的細節,使用起來更簡單了。但一般來說高度的封裝意味著無法定制,比如說在網絡應用中經常要監聽文件的上傳下載進度,顯然Okio默認是沒有這個功能的,應該怎么擴展呢?別忘了,裝飾者模式。實際上Okio已經提供了Decorator類:ForwardingSink,ForwardingSource,只要繼承這兩個類就可以自己定制功能了。

class CountingSink extends ForwardingSink{
        private long bytesWritten = 0;
        private Listen listen;
        private File file;
        private long totalLength;
        public CountingSink(Sink delegate,File file,Listen listen) {
            super(delegate);
            this.listen = listen;
            this.file = file;
            totalLength = contentLength();
        }
        public long contentLength(){
            if (file != null) {
                long length = file.length();
                Log.d("abc : length :", length + "");
                return length;
            }else {
                return 0;
            }

        }

        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            super.write(source, byteCount);
            bytesWritten += byteCount;
            listen.onProgress(bytesWritten, totalLength);
        }

        interface Listen{
            void onProgress(long bytesWritten, long contentLength);
        }
    }

我們復制一首歌做測試:

 File fileSrc = new File(Environment.getExternalStorageDirectory() + "/000szh", "pain.mp3");
 File fileCopy = new File(Environment.getExternalStorageDirectory() + "/000szh","pain3.mp3");

      CountingSink.Listen listen = new CountingSink.Listen() {
            @Override
            public void onProgress(long bytesWritten, long contentLength) {
                long total = contentLength;
                float pos = bytesWritten *1.0f / total;
            }
        };
        BufferedSink bufferedSink = null;
        Source source = null;
        try {
            //包裝sink
            Sink sink= Okio.sink(fileCopy);
            CountingSink countingSink = new CountingSink(sink, fileSrc,listen);
            bufferedSink = Okio.buffer(countingSink);
            source = Okio.source(fileSrc);
            bufferedSink.writeAll(source);
            bufferedSink.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                closeAll(bufferedSink, source);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

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

推薦閱讀更多精彩內容