學(xué)習(xí)基于OkHttp的網(wǎng)絡(luò)框架(一)Okio詳解

繼承的缺點


如果要給一個類擴展功能應(yīng)該怎么做挨厚?

繼承與組合:
復(fù)用代碼是進行程序設(shè)計的一個重要原因,組合和繼承被委以重任,其中繼承更是面向?qū)ο蟮幕唬啾扔诮M合或颊,繼承其實有諸多缺點。組合只要持有另一個類的對象倍踪,就可以使用它暴露的所有功能梯捕,同時也隱藏了具體的實現(xiàn)(黑盒復(fù)用);組合之間的關(guān)系是動態(tài)的钥勋,在運行才確定炬转;組合有助于保持每個類被封裝,并被集中在單個任務(wù)上(單一原則)算灸。而然扼劈,類繼承允許我們根據(jù)自己的實現(xiàn)來覆蓋重寫父類的實現(xiàn)細節(jié),父類的實現(xiàn)對于子類是可見的(白盒復(fù)用)菲驴;繼承是在編譯時刻靜態(tài)定義的荐吵,即是靜態(tài)復(fù)用,在編譯后子類已經(jīng)確定了赊瞬;繼承中父類定義了子類的部分實現(xiàn)先煎,而子類中又會重寫這些實現(xiàn),修改父類的實現(xiàn)巧涧,這是一種破壞了父類的封裝性的表現(xiàn)薯蝎。總之組合相比繼承更具靈活性。即便如此谤绳,我們有不得不使用繼承的理由:

向上轉(zhuǎn)型占锯,復(fù)用接口

如果用繼承來擴展功能會遇到上面所說的諸多問題袒哥,對父類的方法做了修改的話,則子類的方法必須做出相應(yīng)的修改消略。所以說子類與父類是一種高耦合堡称,并且因為子類是靜態(tài)的,當(dāng)擴展的功能是多種情況的組合的話艺演,你必須枚舉出所有的情況為它們定義子類却紧。比如咖啡店里有四種咖啡:


現(xiàn)在還可以給咖啡添加額外的四種調(diào)料
Milk,Chocolate胎撤,Icecream晓殊,Whip如果為每一種咖啡和調(diào)料的組合編寫子類將有64種情況,顯然這種類型體系臃腫是無法接受的哩照!
那么有什么方法可以即保留向上轉(zhuǎn)型的繼承結(jié)構(gòu)挺物,又避免繼承帶來的問題呢 ?

裝飾者優(yōu)化繼承結(jié)構(gòu)


Decorator Pattern

ConcreteComponentComponent是原有的繼承結(jié)構(gòu),相比于直接在ConcreteComponent上開刀來擴展功能飘弧,我們重新定義了一個Decorator類识藤,Decorator用組合的方式持有一個Component對象,同時繼承Component這樣就實現(xiàn)了保留向上轉(zhuǎn)型的繼承結(jié)構(gòu)的同時次伶,擁有組合的優(yōu)點:

  1. 通過動態(tài)的方式來擴展一個對象的功能
  1. 通過裝飾類的排列組合痴昧,可以創(chuàng)造恒多不同行為的組合
  2. 裝飾類Decorator和構(gòu)建類ConcreteComponent可以獨立變化

OKio原理分析


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

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

Source和Sink流程

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

  public interface Source extends Closeable {
  /**
   * 定義基礎(chǔ)的read操作,該方法將字節(jié)寫入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的結(jié)構(gòu)是相同的瘤载,就不廢話了。那么Source和Sink的具體實現(xiàn)在哪里呢卖擅?Okio類提供了靜態(tài)的方法生產(chǎn)SinkSource鸣奔,這個方法也比較簡單,將InputStream中的數(shù)據(jù)寫入到BufferSegment中惩阶,BufferSegment是Okio對io流操作進行優(yōu)化的關(guān)鍵類挎狸,后面在詳細討論,先把讀寫操作的流程走完断楷。

  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的具體實現(xiàn)是RealBufferedSource,可以看到RealBufferedSource其實是個裝飾類冬筒,內(nèi)部管理Source對象來擴展Source的功能恐锣,同時擁有Source讀取數(shù)據(jù)時用到的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");

    //先將數(shù)據(jù)讀到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);
  }
  //...
}

小結(jié)一下:
Source對象每次read,Sink對象每次write都需要一個Buffer對象侥蒙,Buffer管理者循環(huán)雙向鏈表Segment,每次讀寫數(shù)據(jù)都先保存在segment中進行緩沖匀奏,BufferedSourceBufferedSink進行讀寫操作時都是間接調(diào)用BufferSegment的操作來完成的鞭衩,整個過程層層嵌套還是有點繞的。
InputStream--Source--BufferedSource--Buffer--segment--Buffer--Sink--BufferedSink--OutputStream

為什么Okio更高效

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

  1. 采用了segment的機制進行內(nèi)存共享和復(fù)用娃善,避免了copy數(shù)組论衍;
  2. 根據(jù)需要動態(tài)分配內(nèi)存大小聚磺;
  3. 避免了數(shù)組創(chuàng)建時的zero-fill坯台,同時降低GC的頻率。

Segment和SegmentPool:
Segment是一個循環(huán)雙向列表瘫寝,內(nèi)部維護者固定長度的byte[]數(shù)組:

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

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

SegmentPool是一個Segment池蜒蕾,由一個單向鏈表構(gòu)成。該池負(fù)責(zé)Segment的回收和閑置Segment的管理焕阿,也就是說Buffer使用的Segment是從Segment單向鏈表中取出的咪啡,這樣有效的避免了GC頻率。

  /** 總?cè)萘?*/
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  /**用Segment實現(xiàn)的單向鏈表,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()根據(jù)當(dāng)前的Segment產(chǎn)生一個新的Segment,新的Segment與原來的Segment共用同一個data[]數(shù)組褒纲,但是改變了讀寫的標(biāo)記位poslimit准夷,從原來的
[pos..limit]拆分為[pos..pos+byteCount]和[pos+byteCount..limit],從而避免了復(fù)制數(shù)組帶來的性能消耗莺掠。前一個和自身的數(shù)據(jù)量都不足一半時衫嵌,compact()會對segement進行壓縮,把自身的數(shù)據(jù)寫入到前一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實戰(zhàn)

Okio封裝了io操作底層操縱字節(jié)的細節(jié)掖棉,使用起來更簡單了墓律。但一般來說高度的封裝意味著無法定制,比如說在網(wǎng)絡(luò)應(yīng)用中經(jīng)常要監(jiān)聽文件的上傳下載進度幔亥,顯然Okio默認(rèn)是沒有這個功能的耻讽,應(yīng)該怎么擴展呢?別忘了帕棉,裝飾者模式针肥。實際上Okio已經(jīng)提供了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);
        }
    }

我們復(fù)制一首歌做測試:

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

    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末具则,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子具帮,更是在濱河造成了極大的恐慌博肋,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜂厅,死亡現(xiàn)場離奇詭異匪凡,居然都是意外死亡,警方通過查閱死者的電腦和手機掘猿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門病游,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人稠通,你說我怎么就攤上這事衬衬。” “怎么了改橘?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵佣耐,是天一觀的道長。 經(jīng)常有香客問我唧龄,道長兼砖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任既棺,我火速辦了婚禮讽挟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘丸冕。我一直安慰自己耽梅,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布胖烛。 她就那樣靜靜地躺著眼姐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佩番。 梳的紋絲不亂的頭發(fā)上众旗,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音趟畏,去河邊找鬼贡歧。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的利朵。 我是一名探鬼主播律想,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绍弟!你這毒婦竟也來了技即?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤樟遣,失蹤者是張志新(化名)和其女友劉穎姥份,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體年碘,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年展鸡,在試婚紗的時候發(fā)現(xiàn)自己被綠了屿衅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡莹弊,死狀恐怖涤久,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情忍弛,我是刑警寧澤响迂,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站细疚,受9級特大地震影響蔗彤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜疯兼,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一然遏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吧彪,春花似錦待侵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至傀缩,卻和暖如春那先,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赡艰。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工胃榕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓勋又,卻偏偏與公主長得像苦掘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子楔壤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容