深入理解Okio之旅

JDK的io庫由于歷史原因設計的比較復雜,有很多裝飾類告嘲,使用起來需要記憶大量的類,相信你也對此早已詬病不滿奖地。Square公司推出的Okio應運而生橄唬,它原本是作為Okhttp的io功能庫而設計的,也是因為Okhttp而被大家熟知参歹。從知道到會使用仰楚,再到理解實現(xiàn)原理后熟練使用,甚至在此基礎上二次開發(fā)優(yōu)化犬庇,這個認知的過程需要刻意練習僧界,這篇文章就是對Okio的一個總結(jié),Okio雖然代碼量不是很多臭挽, 但是里面值得學習的地方還是很多捂襟。

Source + Sink

簡介

Okio定義了自己的一套繼承鏈,Source對應InputStream欢峰, Sink對應OutputStream葬荷,這樣對比就不難理解了,看一下接口的定義

public interface Source extends Closeable {

  long read(Buffer sink, long byteCount) throws IOException;

  Timeout timeout();

  @Override void close() throws IOException;
}


public interface Sink extends Closeable, Flushable {

  void write(Buffer source, long byteCount) throws IOException;

  @Override void flush() throws IOException;

  Timeout timeout();

  @Override void close() throws IOException;
}

接口定義的方法很簡潔纽帖,
read/write方法宠漩,讀取和寫入數(shù)據(jù)的接口方法,它們的第一個參數(shù)都是Buffer,關于這個類后面會詳細介紹,這里我們暫且按照緩沖區(qū)理解它钱贯。byteCount就是讀取或者寫入的字節(jié)數(shù)厌处。
timeout方法,Okio新增的新特性凝危,超時控制
close方法波俄,關閉輸入輸出流
flush方法,將Buffer緩沖區(qū)中的數(shù)據(jù)寫入目標流中蛾默。

如何使用

Okio已經(jīng)幫我們定義了一個門面類懦铺,名字就叫Okio,通過它可以生成各種我們需要的對象支鸡。
比如Okio.source(inputStream); 將inputStream包裝成我們的Source對象冬念,同樣的
Okio.sink(outputStream)趁窃;將outputStream包裝成Sink對象。

所以Okio的底層操作的流對象還是Jdk里面定義的InputStream和OutputStream急前,作為一個輕量級的io框架它不可能跳出Jdk的框架去另外實現(xiàn)一套醒陆,它做的只是方便開發(fā)者的封裝,但是它的封裝設計足夠優(yōu)秀裆针,這也是我還在這里跟你們吹牛x的原因刨摩,還是不想從大神那里學個一招半式。

看個例子

        File file = new File("/Users/aliouswang/Documents/olympic/JavaArt//text.temp");
        Sink sink = Okio.sink(file);

        Buffer buffer = new Buffer();
        buffer.writeString("Hello okio!", Charset.forName("UTF-8"));
        buffer.writeInt(998);
        buffer.writeByte(1);
        buffer.writeLong(System.currentTimeMillis());
        buffer.writeUtf8("Hello end!");

        sink.write(buffer, buffer.size());

        sink.flush();
        sink.close();

很簡單的一個寫文件的例子世吨,前面說過Source和Sink的讀和寫的方法都需要一個Buffer對象澡刹,Buffer對象幫我們提供了類似BufferedInputStream和BufferedOutputStream的緩沖區(qū)功能(提高讀寫效率),同時還提供了DataInputStream和DataOutputStream中的大部分功能(比如寫int耘婚,byte罢浇,long等),而且Buffer還提供了寫String的方法边篮,更是為我們經(jīng)常使用的UTF-8編碼格式己莺,單獨提供讀寫方法。

有寫就有讀

        Source source = Okio.source(file);
        buffer.clear();
        source.read(buffer, 1024);

        String string = buffer.readString("Hello okio!".length(), Charset.forName("UTF-8"));
        int intValue = buffer.readInt();
        byte byteValue = buffer.readByte();
        long longValue = buffer.readLong();
        String utf8 = buffer.readUtf8();

        System.out.println("str:" + string + ";\nint:" + intValue + ";\nbyte:" + byteValue + ";" +
                "\nlong:" + longValue + "\nutf8:" + utf8);

        source.close();

    // 打印結(jié)果:
    str:Hello okio!;
    int:998;
    byte:1;
    long:1555325659665
    utf8:Hello end!

但是每次都去new一個Buffer對象戈轿,是不是很麻煩凌受,你我都能想到的,大神們肯定早就想到了思杯,于是乎有了BufferedSink胜蛉,BufferedSource。

BufferedSource + BufferedSink

BufferedSource 和 BufferedSink 也都是接口色乾,里面定義的接口方法比較多誊册,篇幅關系,這里只列出BufferedSink的定義暖璧,更細節(jié)的可以查看源碼案怯,源碼中對很多方法的注釋都舉了例子來幫助我們理解,Okio的作者也是用心良苦澎办,生怕我們廣大的碼農(nóng)們看不懂嘲碱,不會用啊>质础B缶狻!

public interface BufferedSink extends Sink, WritableByteChannel {
  Buffer buffer();
  BufferedSink write(ByteString byteString) throws IOException;
  BufferedSink write(byte[] source) throws IOException;
  BufferedSink write(byte[] source, int offset, int byteCount) throws IOException;
  long writeAll(Source source) throws IOException;
  BufferedSink write(Source source, long byteCount) throws IOException;
  BufferedSink writeUtf8(String string) throws IOException;
  BufferedSink writeString(String string, Charset charset) throws IOException;
  BufferedSink writeString(String string, int beginIndex, int endIndex, Charset charset)
      throws IOException;
  BufferedSink writeByte(int b) throws IOException;
  BufferedSink writeShort(int s) throws IOException;
  BufferedSink writeShortLe(int s) throws IOException;
  BufferedSink writeInt(int i) throws IOException;
  BufferedSink writeIntLe(int i) throws IOException;
  BufferedSink writeLong(long v) throws IOException;
  BufferedSink writeLongLe(long v) throws IOException;
  BufferedSink writeDecimalLong(long v) throws IOException;
  BufferedSink writeHexadecimalUnsignedLong(long v) throws IOException;
  @Override void flush() throws IOException;
  BufferedSink emit() throws IOException;
  BufferedSink emitCompleteSegments() throws IOException;
  OutputStream outputStream();
}

可以看到BufferedSink繼承于Sink琅绅,同時還繼承了WritableByteChannel扶欣,這個接口是nio接口,所以Okio同樣實現(xiàn)了nio的相關功能,這里由于水平有限料祠,關于nio的知識這篇文章不會涉及骆捧,有興趣的同學可以自行查閱資料哦。

BufferedSink定義了Buffer類中定義的全部方法髓绽,同時還定義了一個buffer()方法凑懂,返回一個Buffer對象,我們大概可以猜想到梧宫,這里應該是一個不太標準的代理模式接谨,BufferedSink委托Buffer來干活。

Okio同樣提供了Buffer相關的方法方便我們使用塘匣。

  public static BufferedSink buffer(Sink sink) {
    return new RealBufferedSink(sink);
  }

  public static BufferedSource buffer(Source source) {
    return new RealBufferedSource(source);
  }

返回的是BufferedSink 和 BufferedSource脓豪,Okio的默認實現(xiàn)類是RealBufferedSink和RealBufferedSource,我們可以通過BufferedSource和BufferedSink對上面讀寫文件的例子進行修改忌卤,

        File file = new File("/Users/aliouswang/Documents/java/JavaArt/text.temp");
        Sink sink = Okio.sink(file);
        BufferedSink bufferedSink = Okio.buffer(sink);

        bufferedSink.writeString("Hello okio!", Charset.forName("UTF-8"));
        bufferedSink.writeInt(998);
        bufferedSink.writeByte(1);
        bufferedSink.writeLong(System.currentTimeMillis());
        bufferedSink.writeUtf8("Hello end!");

        bufferedSink.close();

        Source source = Okio.source(file);
        BufferedSource bufferedSource = Okio.buffer(source);

        String string = bufferedSource.readString("Hello okio!".length(), Charset.forName("UTF-8"));
        int intValue = bufferedSource.readInt();
        byte byteValue = bufferedSource.readByte();
        long longValue = bufferedSource.readLong();
        String utf8 = bufferedSource.readUtf8();

        System.out.println("str:" + string + ";\nint:" + intValue + ";\nbyte:" + byteValue + ";" +
                "\nlong:" + longValue + "\nutf8:" + utf8);

        source.close();

可以看到扫夜,BufferedSource和BufferedSink能夠滿足我們對io的日常絕大部分使用場景。

Okio門面類的實現(xiàn)

更一般的驰徊,我們會這樣去寫笤闯,鏈式調(diào)用,代碼更簡潔棍厂。

BufferedSource bufferedSource = Okio.buffer(Okio.source(file));
BufferedSink bufferedSink = Okio.buffer(Okio.sink(file));

非常簡潔的就能生成BufferedSource和BufferedSink颗味,看一下Okio幫我們做了什么。

  public static Source source(File file) throws FileNotFoundException {
    if (file == null) throw new IllegalArgumentException("file == null");
    return source(new FileInputStream(file));
  }

  public static Source source(InputStream in) {
    // 生成一個默認的Timeout超時對象牺弹,默認實現(xiàn)是沒有超時deadtime的
    return source(in, new Timeout());
  }

  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;
        try {
          // 檢查超時浦马,
          timeout.throwIfReached();
          // 從Buffer獲取一個可以寫入的Segment,這一塊只是接下來再具體分析
          Segment tail = sink.writableSegment(1);
          int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
          // 將最大能copy的字節(jié)寫入Buffer张漂,
          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;
        }
      }

      @Override public void close() throws IOException {
        in.close();
      }

      @Override public Timeout timeout() {
        return timeout;
      }

      @Override public String toString() {
        return "source(" + in + ")";
      }
    };
  }

通過Okio.source的實現(xiàn)可以看到晶默,在讀取的時候,會從傳入的InputStream in 對象中讀取字節(jié)到Buffer sink中航攒,前面我們提到過磺陡,RealBufferedSource和RealBufferedSink內(nèi)部都持有一個Buffer對象,可以猜測漠畜,它們持有的buffer對象 會在讀寫的時候傳入币他。我們進入源碼驗證一下, 這里我們以readString 方法為例。

  @Override public String readString(long byteCount, Charset charset) throws IOException {
    require(byteCount);
    if (charset == null) throw new IllegalArgumentException("charset == null");
    return buffer.readString(byteCount, charset);
  }

  @Override public void require(long byteCount) throws IOException {
    if (!request(byteCount)) throw new EOFException();
  }

  @Override 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;
    }
    return true;
  }

  @Override public String readString(long byteCount, Charset charset) throws EOFException {
    checkOffsetAndCount(size, 0, byteCount);
    if (charset == null) throw new IllegalArgumentException("charset == null");
    if (byteCount > Integer.MAX_VALUE) {
      throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
    }
    if (byteCount == 0) return "";

    Segment s = head;
    if (s.pos + byteCount > s.limit) {
      // If the string spans multiple segments, delegate to readBytes().
      return new String(readByteArray(byteCount), charset);
    }

    String result = new String(s.data, s.pos, (int) byteCount, charset);
    s.pos += byteCount;
    size -= byteCount;

    if (s.pos == s.limit) {
      head = s.pop();
      SegmentPool.recycle(s);
    }

    return result;
  }

代碼比較清晰盆驹,先從source中讀取要求的bytecount長度的String到buffer中圆丹,然后從buffer中讀取String 返回滩愁。其他的讀取方法跟readString大同小異躯喇,有興趣同學可以自行查閱源碼。

說了這么久,我們的主角Buffer對象登場了廉丽。

Buffer

看一下Buffer類的申明倦微,實現(xiàn)了BufferedSource, BufferedSink, Cloneable, ByteChannel 四個接口。

public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel {...}

我們知道Buffer作為緩沖區(qū)正压,肯定底層需要有數(shù)據(jù)結(jié)構來存儲暫存的數(shù)據(jù)欣福,JDK的BuffedInputStream和BufferedOutputStream中是使用字節(jié)數(shù)組的,而這里Okio的Buffer不是焦履,它使用的是Segment拓劝。

public Segment head;
Segment

Segment 是一個雙向循環(huán)鏈表,它的內(nèi)部持有一個byte[] data嘉裤,默認大小8192(與JDK的BufferedInputStream相同)郑临。

public final class Segment {
  /** The size of all segments in bytes. */
  static final int SIZE = 8192;

  /** 默認共享最小字節(jié)數(shù)*/
  static final int SHARE_MINIMUM = 1024;

  final byte[] data;

  /** 標識下一個讀取字節(jié)的位置 */
  int pos;

  /** 標識下一個寫入字節(jié)的位置 */
  int limit;

  /** 是否與其他Segment共享byte[] */
  boolean shared;

  /** 是否擁有這個byte[], 如果擁有可以寫入 */
  boolean owner;

  /** Segment后繼 */
  public Segment next;

  /** Segment前驅(qū) */
  Segment prev;
  Segment() {
    this.data = new byte[SIZE];
    this.owner = true;
    this.shared = false;
  }

  ......
}

Sement關鍵的成員變量都加了注釋,Okio為了優(yōu)化性能屑宠,避免頻繁的創(chuàng)建和回收對象厢洞,使用了對象池模式,設計了SegmentPool類來管理Segment典奉。

SegemntPool
final class SegmentPool {
  /** The maximum number of bytes to pool. */
  // TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments?
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  /** Singly-linked list of segments. */
  static @Nullable Segment next;

  /** Total bytes in this pool. */
  static long byteCount;

  private SegmentPool() {
  }

  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.
  }

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

SegmentPool代碼很簡潔躺翻,它的最大容量是8個Segment,如果超過調(diào)用take方法就會直接新建一個Segment對象卫玖,另外recycle回收方法負責回收閑置的Segment公你,將其加入鏈表,供其他buffer使用假瞬。

有了Segment和SegmentPool的知識省店,就更容易理解Buffer類的實現(xiàn)了。
比如Okio.source方法新建的Source對象的read方法笨触,獲取可以寫入的Segment對象懦傍,便利Segment鏈表獲取可以寫入的Segment,如果head為null則新建一個Segment芦劣。

    // 從Buffer獲取一個可以寫入的Segment粗俱,這一塊只是接下來再具體分析
  Segment tail = sink.writableSegment(1);

  Segment writableSegment(int minimumCapacity) {
    if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();

    if (head == null) {
      head = SegmentPool.take(); // Acquire a first segment.
      return head.next = head.prev = head;
    }

    Segment tail = head.prev;
    if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
      tail = tail.push(SegmentPool.take()); // Append a new empty segment to fill up.
    }
    return tail;
  }

Buffer的其他方法就不一一分析了,有了我們前面的知識虚吟,相信看起來不會太難寸认。接下來我們看一下Okio的另一個類ByteString。

ByteString

我們知道String是的內(nèi)部是基于char[] 數(shù)組來實現(xiàn)的串慰,Okio的ByteString內(nèi)部是基于byte[] 數(shù)組來實現(xiàn)的偏塞。跟String類似,ByteString也被設計為不可變的邦鲫,這樣可以保證ByteString是線程安全的灸叼。

public class ByteString implements Serializable, Comparable<ByteString> {
  final byte[] data;
  ByteString(byte[] data) {
    this.data = data; // Trusted internal constructor doesn't clone data.
  }
   ......
}

同時ByteString提供了很多方便的工具方法神汹,比如base64,sha1加密等古今。

  public String base64() {
    return Base64.encode(data);
  }

  /** Returns the 128-bit MD5 hash of this byte string. */
  public ByteString md5() {
    return digest("MD5");
  }

  /** Returns the 160-bit SHA-1 hash of this byte string. */
  public ByteString sha1() {
    return digest("SHA-1");
  }

  /** Returns the 256-bit SHA-256 hash of this byte string. */
  public ByteString sha256() {
    return digest("SHA-256");
  }

  /** Returns the 512-bit SHA-512 hash of this byte string. */
  public ByteString sha512() {
    return digest("SHA-512");
  }

同時ByteString也提供了靜態(tài)方法屁魏,方便與String類型互轉(zhuǎn)。

  /** Returns a new byte string containing the {@code UTF-8} bytes of {@code s}. */
  public static ByteString encodeUtf8(String s) {
    if (s == null) throw new IllegalArgumentException("s == null");
    ByteString byteString = new ByteString(s.getBytes(Util.UTF_8));
    byteString.utf8 = s;
    return byteString;
  }

  /** Returns a new byte string containing the {@code charset}-encoded bytes of {@code s}. */
  public static ByteString encodeString(String s, Charset charset) {
    if (s == null) throw new IllegalArgumentException("s == null");
    if (charset == null) throw new IllegalArgumentException("charset == null");
    return new ByteString(s.getBytes(charset));
  }

  /** Constructs a new {@code String} by decoding the bytes as {@code UTF-8}. */
  public String utf8() {
    String result = utf8;
    // We don't care if we double-allocate in racy code.
    return result != null ? result : (utf8 = new String(data, Util.UTF_8));
  }

  /** Constructs a new {@code String} by decoding the bytes using {@code charset}. */
  public String string(Charset charset) {
    if (charset == null) throw new IllegalArgumentException("charset == null");
    return new String(data, charset);
  }

最后

Okio并不是設計來代替Jdk io的捉腥,但是在某些重度io的場景氓拼,如果對性能優(yōu)化追求極致的話,Okio不失是一種選擇抵碟,關于Okio還有很多細節(jié)的知識由于篇幅關系沒有涉及桃漾,有興趣的同學可以去看源碼中找答案,全文完拟逮。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呈队,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子唱歧,更是在濱河造成了極大的恐慌宪摧,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颅崩,死亡現(xiàn)場離奇詭異几于,居然都是意外死亡,警方通過查閱死者的電腦和手機沿后,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門沿彭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人尖滚,你說我怎么就攤上這事喉刘。” “怎么了漆弄?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵睦裳,是天一觀的道長。 經(jīng)常有香客問我撼唾,道長廉邑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任倒谷,我火速辦了婚禮蛛蒙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘渤愁。我一直安慰自己牵祟,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布抖格。 她就那樣靜靜地躺著诺苹,像睡著了一般咕晋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上筝尾,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音办桨,去河邊找鬼筹淫。 笑死,一個胖子當著我的面吹牛呢撞,可吹牛的內(nèi)容都是我干的损姜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼殊霞,長吁一口氣:“原來是場噩夢啊……” “哼摧阅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起绷蹲,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤棒卷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后祝钢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體比规,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年拦英,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜒什。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡疤估,死狀恐怖灾常,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情铃拇,我是刑警寧澤钞瀑,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站慷荔,受9級特大地震影響仔戈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拧廊,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一监徘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吧碾,春花似錦凰盔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽落剪。三九已至,卻和暖如春尿庐,著一層夾襖步出監(jiān)牢的瞬間忠怖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工抄瑟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凡泣,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓皮假,卻偏偏與公主長得像鞋拟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子惹资,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

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

  • 最近在學習okhttp的過程中贺纲,很多地方遇到了okio的功能,okio是square公司封裝的IO框架褪测,okhtt...
    蕉下孤客閱讀 2,488評論 0 7
  • Okio的傳送門 https://github.com/square/okio 了解Okio之前先了解一個裝飾者模...
    大批閱讀 541評論 0 2
  • square在開源社區(qū)的貢獻是卓越的猴誊,這里是square在Android領域貢獻的開源項目。 1. okio概念 ...
    王英豪閱讀 1,186評論 0 2
  • 什么是Okio Retrofit侮措,OkHttp稠肘,Okio 是 Square 的開源的安卓平臺網(wǎng)絡層三板斧,它們逐層...
    kakaxicm閱讀 669評論 0 0
  • 晚自習后的夜里萝毛, 穿著厚重的棉衣项阴, 卻覺裹著寒風, 從裸露在外的手背 徹徹底底地冷到胸前笆包。 一個人环揽、一把傘, 似乎...
    臻耶閱讀 339評論 0 8