第3章 IO

Okio筆記

一、基本認識

Okio庫是一個由square公司開發的,它補充了java.io和java.nio的不足,以便能夠更加方便,快速地訪問、存儲和處理數據。而OkHttp的底層也使用該庫作為支持。而在開發中,使用該庫可以大大的帶來方便。

Okio中有兩個關鍵的接口,SinkSource,這兩個接口都繼承了Closeable接口;而Sink可以簡單的看做OutputStream,Source可以簡單的看做InputStream,這兩個接口都是支持讀寫超時設置的。

Okio相關類

它們各自有一個支持緩沖區的子類接口,BufferedSinkBufferedSource,而BufferedSink有一個實現類RealBufferedSink,BufferedSource有一個實現類RealBufferedSource;此外,Sink和Source它門還各自有一個支持gzip壓縮的實現類GzipSinkGzipSource;一個具有委托功能的抽象類ForwardingSinkForwardingSource;還有一個實現類便是InflaterSourceDeflaterSink,這兩個類主要用于壓縮,為GzipSink和GzipSource服務;

BufferedSink中定義了一系列寫入緩存區的方法,比如write方法寫byte數組,writeUtf8寫字符串,還有一些列的writeByte,writeString,writeShort,writeInt,writeLong,writeDecimalLong等等方法。BufferedSource定義的方法和BufferedSink極為相似,只不過一個是寫一個是讀,基本上都是一一對應的,如readUtf8,readByte,readString,readShort,readInt等等等等。這兩個接口中的方法有興趣的點源碼進去看就可以了。

二、簡單使用

//簡單的文件讀寫
public void readWriteFile() throws Exception {
    //1.文件
    File file = new File("resources/dest.txt");
    //2.構建寫緩沖池
    BufferedSink sink = Okio.buffer(Okio.sink(file));
    //3.向緩沖池寫入文本
    sink.writeUtf8("Hello, java.io file!");
    //4.關閉緩沖池
    sink.close();

    //1.構建讀文件緩沖源
    BufferedSource source = Okio.buffer(Okio.source(file));
    //2.讀文件
    source.readUtf8();
    //3.關閉緩沖源
    source.close();
}
    
//文件內容的追加
public void appendFile() throws Exception {
    File file = new File("resources/dest.txt");
    //1.將文件讀入,并構建寫緩沖池
    BufferedSink sink = Okio.buffer(Okio.appendingSink(file));
    //2.追加文本
    sink.writeUtf8("Hello, ");
    //3.關閉
    sink.close();

    //4.再次追加文本,需要重新構建緩沖池對象
    sink = Okio.buffer(Okio.appendingSink(file));
    //5.追加文本
    sink.writeUtf8("java.io file!");
    //6.關閉緩沖池
    sink.close();
}

//通過路徑來讀寫文件
public void readWritePath() throws Exception {
    Path path = new File("resources/dest.txt").toPath();
    //1.構建寫緩沖池
    BufferedSink sink = Okio.buffer(Okio.sink(path));
    //2.寫緩沖
    sink.writeUtf8("Hello, java.nio file!");
    //3.關閉緩沖
    sink.close();

    //1.構建讀緩沖源
    BufferedSource source = Okio.buffer(Okio.source(path));
    //2.讀文本
    source.readUtf8();
    //3.關閉緩沖源
    source.close();
}

//寫Buffer,在okio中Buffer是一個很重要的對象,在后面我們在詳細介紹。
public void sinkFromOutputStream() throws Exception {
    //1.構建buffer對象
    Buffer data = new Buffer();
    //2.向緩沖中寫入文本
    data.writeUtf8("a");
    //3.可以連續追加,類似StringBuffer
    data.writeUtf8("c");

    //4.構建字節數組流對象
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    //5.構建寫緩沖池
    Sink sink = Okio.sink(out);
    //6.向池中寫入buffer
    sink.write(data, 2);
}

//讀Buffer
public void sourceFromInputStream() throws Exception {
    //1.構建字節數組流
    InputStream in = new ByteArrayInputStream(
        ("a"  + "c").getBytes(UTF_8));
    // Source: ac
    //2.緩沖源
    Source source = Okio.source(in);
    //3.buffer
    Buffer sink = new Buffer();
    //4.將數據讀入buffer
    sink.readUtf8(2);
}

//Gzip功能
public static void gzipTest(String[] args) {
    Sink sink = null;
    BufferedSink bufferedSink = null;
    GzipSink gzipSink = null;
    try {
        File dest = new File("resources/gzip.txt");
        sink = Okio.sink(dest);
        gzipSink = new GzipSink(sink);
        bufferedSink = Okio.buffer(gzipSink);
        bufferedSink.writeUtf8("android vs ios");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        closeQuietly(bufferedSink);
    }
        
    Source source = null;
    BufferedSource bufferedSource = null;
    GzipSource gzipSource = null;
    try {
        File file = new File("resources/gzip.txt");
        source = Okio.source(file);
        gzipSource = new GzipSource(source);
        bufferedSource = Okio.buffer(gzipSource);
        String content = bufferedSource.readUtf8();
        System.out.println(content);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        closeQuietly(bufferedSource);
    }
}

public static void closeQuietly(Closeable closeable) {
    if (closeable != null) {
        try {
            closeable.close();
        } catch (RuntimeException rethrown) {
            throw rethrown;
        } catch (Exception ignored) {
        }
    }
}

可以看到,okio的文件讀寫操作,使用起來是很簡單的,減少了很多io操作的基本代碼,并且對內存和cpu使用做了優化(代碼直觀當然是看不出來的,okio作者在buffer類中有詳細的說明,后面我們再分析這個類)。

此外還有一個ByteString類,這個類可以用來做各種變化,它將byte轉會為String,而這個String可以是utf8的值,也可以是base64后的值,也可以是md5的值,也可以是sha256的值,總之就是各種變化,最后取得你想要的值。

三、Okio框架結構與源碼分析

Okio框架分析

Okio的簡單使用,無非下面幾個步驟:
a. 構建對象
b. 讀、寫
c. 關閉緩沖對象

構建緩沖對象

通過Okio的buffer(Source source)buffer(Sink sink)方法,構建緩沖對象

public static BufferedSource buffer(Source source) {
    return new RealBufferedSource(source);
}
  
public static BufferedSink buffer(Sink sink) {
    return new RealBufferedSink(sink);
}

這兩個static方法,返回 讀、寫池:BufferedSource 、BufferedSink。Source、Sink ,這兩種參數從哪里來?看看Okio下面兩個static方法

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

類似的方法還有

  • sink(File file) Sink
  • sink(OutputStream out) Sink
  • sink(Path path, OpenOption... options) Sink
  • sink(Socket socket) Sink
  • source(File file) Source
  • source(InputStream in) Source
  • source(Path path, OpenOption... options) Source
  • source(Socket socket) Source

以sink方法為例,最后都是調用

  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() {//new了一個Sink對象,這個對象就是emitCompleteSegments方法中的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;
          int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
          out.write(head.data, head.pos, toCopy);//真正完成寫操作!

          head.pos += toCopy;
          byteCount -= toCopy;
          source.size -= toCopy;

          if (head.pos == head.limit) {
            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 + ")";
      }
    };
  }
讀、寫操作

下面以寫操作為例,來查看源碼,在RealBufferedSink

@Override 
public BufferedSink writeUtf8(String string, int beginIndex, int endIndex)
      throws IOException {
    if (closed) throw new IllegalStateException("closed");
    buffer.writeUtf8(string, beginIndex, endIndex);//寫入到buffer中
    return emitCompleteSegments();//將buffer中的內容寫入到sink成員變量中去,然后將自身返回
}

可以看到這里,有一個成員變量buffer,并調用了其writeUtf8方法,源碼如下:

@Override 
public Buffer writeUtf8(String string, int beginIndex, int endIndex) {
    ...
    // Transcode a UTF-16 Java String to UTF-8 bytes.
    for (int i = beginIndex; i < endIndex;) {
      int c = string.charAt(i);

      if (c < 0x80) {
        Segment tail = writableSegment(1);
        byte[] data = tail.data;
        int segmentOffset = tail.limit - i;
        int runLimit = Math.min(endIndex, Segment.SIZE - segmentOffset);

        // Emit a 7-bit character with 1 byte.
        data[segmentOffset + i++] = (byte) c; // 0xxxxxxx

        // Fast-path contiguous runs of ASCII characters. This is ugly, but yields a ~4x performance
        // improvement over independent calls to writeByte().
        while (i < runLimit) {
          c = string.charAt(i);
          if (c >= 0x80) break;
          data[segmentOffset + i++] = (byte) c; // 0xxxxxxx
        }

        int runSize = i + segmentOffset - tail.limit; // Equivalent to i - (previous i).
        tail.limit += runSize;
        size += runSize;

      } else if (c < 0x800) {
        // Emit a 11-bit character with 2 bytes.
        writeByte(c >>  6        | 0xc0); // 110xxxxx
        writeByte(c       & 0x3f | 0x80); // 10xxxxxx
        i++;

      } else if (c < 0xd800 || c > 0xdfff) {
        ...
      }
    }
    return this;
  }
  
  @Override 
  public Buffer writeByte(int b) {
    //返回一個可寫的Segment,可以使用的capacity至少為1(一個Segment的總大小為8KB),若當前Segment已經寫滿了,則會新建一個Segment返回
    Segment tail = writableSegment(1);
    tail.data[tail.limit++] = (byte) b;//將入參放到Segment中
    size += 1;//總長度+1
    return this;
  }

這一切的背后都是一個叫做Buffer的類在支持著緩沖區,Buffer是BufferedSink和BufferedSource的實現類,因此它既可以用來讀數據,也可以用來寫數據,其內部使用了一個Segment和SegmentPool,維持著一個鏈表,其循環利用的機制和Android中Message的利用機制是一模一樣的。

final class SegmentPool {//這是一個鏈表,Segment是其元素,總大小為8KB
  static final long MAX_SIZE = 64 * 1024; // 64 KiB.

  static Segment next;//指向鏈表的下一個元素

  static long byteCount;

  private SegmentPool() {
  }

  static Segment take() {
    synchronized (SegmentPool.class) {
      if (next != null) {//從鏈表中取出一個Segment
        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.SIZE固定為8KB
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
}

內部一個成員變量next指向鏈表下一個元素,take方法首先判斷池中是否存在可用的,存在則返回,不存在則new一個,而recycle則是將不再使用的Segment重新扔到池中去。從而達到一個Segment池的作用。

回到writeUtf8方法中,RealBufferedSink的emitCompleteSegments()方法完成寫的提交

@Override 
public BufferedSink emitCompleteSegments() throws IOException {
    if (closed) throw new IllegalStateException("closed");
    long byteCount = buffer.completeSegmentByteCount();//返回已經緩存的字節數
    if (byteCount > 0) sink.write(buffer, byteCount);//這個sink就是剛才new的
    return this;
}

Okio的寫操作流程圖
image
image

參考文獻

Android 善用Okio簡化處理I/O操作
Android Okhttp之Okio解析

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容