io是咱們天天都要使用,可是不多有人關注細節的一個模塊,尤爲對於客戶端的開發來說,幾乎不多直接面向io編程,android rom中提供的基礎方法已經足夠好,基本知足各類需求。至於NIO這種看上去高大上可能只有服務端的同窗纔有瞭解了。可是 隨着你app用戶的增多難免遇到各類各樣奇怪的問題,這個時候你若是熟悉io會對你找bug 修bug 有很大的好處。況且關於io 的代碼你若是能研讀一遍對你自身也是有很大好處的。java
簡單看一下 java 原生提供的io包 大概有哪些東西,類的關係如何android
實際上看的出來 輸入輸出流的 結果都差很少。編程
首先這是一個抽象類,有一個重要的read 抽象方法 等待他的子類去實現設計模式
結合前面的圖 咱們能夠得知,inputstream有不少子類,這裏咱們重點看一下 一個特殊的子類,FilterInputStream, 由於很容易看出來,InputStream中其餘子類就到底了,沒有子類的子類了,可是這個FilterInputStream 仍然有很多子類數組
public class FilterInputStream extends InputStream {
/**
* The input stream to be filtered.
*/
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
return in.read(b, off, len);
}
public long skip(long n) throws IOException {
return in.skip(n);
}
public int available() throws IOException {
return in.available();
}
public void close() throws IOException {
in.close();
}
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
public synchronized void reset() throws IOException {
in.reset();
}
public boolean markSupported() {
return in.markSupported();
}
}
複製代碼
FilterInputStream 這個源碼能夠看出來,內部就只有一個inputstream 對象,而後全部操做都交給這個inputstream的對象來完成緩存
至關於這個FilterInputStream 就是一個殼嗎bash
而後咱們再來看看這個FilterInputStream 的子類都幹嗎了,咱們選BufferedInputStream 來看看。app
public class BufferedInputStream extends FilterInputStream {
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
複製代碼
這裏不深究細節,只須要理解,你看咱們以前的FilterInputStream read方法裏什麼都沒作,你傳什麼is對象進來就用這個對象的read方法,可是BufferedInputStream 不一樣,BufferedInputStream重寫了read方法,使得這個bis對象具備了緩存讀入的功能。學習
至關於bis 把 傳進來的is對象給裝飾了一下。同理,對於其它的FilterInputStream的子類,其做用也是同樣的,那就是裝飾一個InputStream,爲它添加它本來不具備的功能。OutputStream以及家眷對於裝飾器模式的體現,也以此類推。ui
//隨便打開一個本地文件
final String filePath = "D:\\error.log";
//InputStream至關於被裝飾的接口或者抽象類,FileInputStream至關於原始的待裝飾的對象,FileInputStream沒法裝飾InputStream
//另外FileInputStream是以只讀方式打開了一個文件,並打開了一個文件的句柄存放在FileDescriptor對象的handle屬性
//因此下面有關回退和從新標記等操做,都是在堆中創建緩衝區所形成的假象,並非真正的文件流在回退或者從新標記
InputStream inputStream = new FileInputStream(filePath);
final int len = inputStream.available();//記錄一下流的長度
System.out.println("FileInputStream not support mark and reset:" + inputStream.markSupported());
System.out.println("---------------------------------------------------------------------------------");
//首先裝飾成BufferedInputStream,它提供咱們mark,reset的功能
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);//裝飾成 BufferedInputStream
System.out.println("BufferedInputStream support mark and reset:" + bufferedInputStream.markSupported());
bufferedInputStream.mark(0);//標記一下
char c = (char) bufferedInputStream.read();
System.out.println("file first char is :" + c);
bufferedInputStream.reset();//重置
c = (char) bufferedInputStream.read();//再讀
System.out.println("reset and first char is :" + c);
bufferedInputStream.reset();
System.out.println("---------------------------------------------------------------------------------");
複製代碼
能夠體會一下上面的java io 代碼,其實java io 爲何要使用 裝飾者模式,我也沒想明白,粗看起來,這樣的代碼,分層明確, 可是細細體會一下 這樣的設計模式帶來的後果就是類太多了。其實對於使用者而言,並非特別友好。我只想要一杯水,你給我一個大海乾啥?尤爲對於客戶端來講,有沒有更加優雅的一種io解決方案呢?有,OKIO
Okio庫是一個由square公司開發的,它補充了java.io和java.nio的不足,以便可以更加方便,快速的訪問、存儲和處理你的數據。而OkHttp的底層也使用該庫做爲支持。見過大多的客戶端開發用OKHttp用的飛起,而後在碰到io類的開發時,又回去使用並不太好用的java io,而忽略了OKio這個神器,那這篇文章後半段就帶你介紹介紹OKio。(學習OKIO源碼對理解java IO 也是有很大好處的)
能夠看出來,這個okio的總體結構仍是簡潔明瞭很是簡單的,不像java io 我截圖都截不下。
(OKio的用法很簡單,就不過多作介紹了)
這裏寫一段經過okio輸出數據的代碼
String fileName="C:\\Users\\16040657\\Desktop\\iotest.txt";
File file= new File(fileName);
BufferedSink bufferedSink=Okio.buffer(Okio.sink(file));
bufferedSink.writeString("12345", Charset.defaultCharset());
bufferedSink.writeString("678910", Charset.defaultCharset());
bufferedSink.close();
複製代碼
能夠看一下調用鏈:
1.生成一個file對象
2.經過OKio.sink的構造方法 生成一個sink 對象,咱們把這個對象稱之爲對象A。
private Okio() {
}
public static BufferedSource buffer(Source source) {
return new RealBufferedSource(source);
}
public static BufferedSink buffer(Sink sink) {
return new RealBufferedSink(sink);
}
複製代碼
而後把這個對象A 傳到OKio的buffer方法裏 就返回一個RealBufferedSink 對象B。
最後再對這個B對象進行實際操做。
sink其實就等於java io 體系中的輸入流。咱們簡要分析一下。
public interface Sink extends Closeable, Flushable {
void write(Buffer var1, long var2) throws IOException;
void flush() throws IOException;
Timeout timeout();
void close() throws IOException;
}
複製代碼
能夠看出來sink就4個方法,write方法有個參數叫buffer,還有個方法叫flush,很容易纔想到這個東西跟緩存是相關的。 接着看她的子類BufferedSink
能夠看到這仍然是一個接口,只不過提供了更多的抽象方法而已。
最後咱們看看真正的實現類。RealBufferedSink
咱們選取其中的一部分代碼看看
public final Buffer buffer = new Buffer();
public final Sink sink;
boolean closed;
RealBufferedSink(Sink sink) {
if(sink == null) {
throw new NullPointerException("sink == null");
} else {
this.sink = sink;
}
}
public Buffer buffer() {
return this.buffer;
}
public void write(Buffer source, long byteCount) throws IOException {
if(this.closed) {
throw new IllegalStateException("closed");
} else {
this.buffer.write(source, byteCount);
this.emitCompleteSegments();
}
}
public BufferedSink write(ByteString byteString) throws IOException {
if(this.closed) {
throw new IllegalStateException("closed");
} else {
this.buffer.write(byteString);
return this.emitCompleteSegments();
}
}
複製代碼
能夠看出來,這些所謂write的方法真正的執行者 其實就是這個buffer對象而已。看上去很像java io的裝飾者模式對吧。
對外暴露了一個類C,但其實類c的這些方法裏真正操做的倒是另一個對象B
結合上述的分析,咱們能夠看到,最後的操做都是經過buffer來完成的,咱們就來看看這個buffer是怎麼完成輸出流 這件事的。
public Buffer writeString(String string, int beginIndex, int endIndex, Charset charset) {
if(string == null) {
throw new IllegalArgumentException("string == null");
} else if(beginIndex < 0) {
throw new IllegalAccessError("beginIndex < 0: " + beginIndex);
} else if(endIndex < beginIndex) {
throw new IllegalArgumentException("endIndex < beginIndex: " + endIndex + " < " + beginIndex);
} else if(endIndex > string.length()) {
throw new IllegalArgumentException("endIndex > string.length: " + endIndex + " > " + string.length());
} else if(charset == null) {
throw new IllegalArgumentException("charset == null");
} else if(charset.equals(Util.UTF_8)) {
return this.writeUtf8(string, beginIndex, endIndex);
} else {
byte[] data = string.substring(beginIndex, endIndex).getBytes(charset);
return this.write(data, 0, data.length);
}
}
複製代碼
能夠看到utf-8的數據寫入是比較簡單的,其餘的數據就是直接寫成byte 字節流了。
最核心的寫入數據方法就在這裏了。首先咱們來看看這個Segment究竟是啥
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package okio;
import javax.annotation.Nullable;
import okio.SegmentPool;
final class Segment {
//最大長度是8192個byte
static final int SIZE = 8192;
//可共享的data數組長度
static final int SHARE_MINIMUM = 1024;
//真正存放數據的地方
final byte[] data;
//開始和結束的limit
int pos;
int limit;
//是否能夠共享,能夠避免數據拷貝。加強效率
boolean shared;
//是否對本身的data數組有寫權限,好比segment b是用segment a來構造出來的,那麼a就擁有寫入權限,可是b沒有。
boolean owner;
//看到next 和prev 應該很容易聯想到雙向鏈表
Segment next;
Segment prev;
Segment() {
this.data = new byte[8192];
this.owner = true;
this.shared = false;
}
Segment(Segment shareFrom) {
this(shareFrom.data, shareFrom.pos, shareFrom.limit);
shareFrom.shared = true;
}
Segment(byte[] data, int pos, int limit) {
this.data = data;
this.pos = pos;
this.limit = limit;
this.owner = false;
this.shared = true;
}
@Nullable
public Segment pop() {
Segment result = this.next != this?this.next:null;
this.prev.next = this.next;
this.next.prev = this.prev;
this.next = null;
this.prev = null;
return result;
}
public Segment push(Segment segment) {
segment.prev = this;
segment.next = this.next;
this.next.prev = segment;
this.next = segment;
return segment;
}
//數據共享之後就沒法寫入了,因此要避免出現存在大片的共享小片斷,因此必定要大於1024個byte 纔會使用這個共享data
//數組的提升效率的操做
public Segment split(int byteCount) {
if(byteCount > 0 && byteCount <= this.limit - this.pos) {
Segment prefix;
if(byteCount >= 1024) {
prefix = new Segment(this);
} else {
//對於pool這樣的關鍵字,咱們要有敏感性就是爲了防止頻繁創造銷燬對象形成的cpu抖動,因此能夠認爲是對象池
prefix = SegmentPool.take();
System.arraycopy(this.data, this.pos, prefix.data, 0, byteCount);
}
prefix.limit = prefix.pos + byteCount;
this.pos += byteCount;
this.prev.push(prefix);
return prefix;
} else {
throw new IllegalArgumentException();
}
}
//時間長了之後一個segment中間可能只有一小段是能夠用的,因此這裏作壓縮,用於將當前的data 放到前面的
//數據中 而後將本身移出,放入到segmentpool中
public void compact() {
if(this.prev == this) {
throw new IllegalStateException();
} else if(this.prev.owner) {
int byteCount = this.limit - this.pos;
int availableByteCount = 8192 - this.prev.limit + (this.prev.shared?0:this.prev.pos);
if(byteCount <= availableByteCount) {
this.writeTo(this.prev, byteCount);
this.pop();
SegmentPool.recycle(this);
}
}
}
public void writeTo(Segment sink, int byteCount) {
if(!sink.owner) {
throw new IllegalArgumentException();
} else {
if(sink.limit + byteCount > 8192) {
//若是是共享的就不讓寫
if(sink.shared) {
throw new IllegalArgumentException();
}
//若是將要寫入的數據和目前存在的數據加起來大於8192 也不給寫
if(sink.limit + byteCount - sink.pos > 8192) {
throw new IllegalArgumentException();
}
System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
sink.limit -= sink.pos;
sink.pos = 0;
}
System.arraycopy(this.data, this.pos, sink.data, sink.limit, byteCount);
sink.limit += byteCount;
this.pos += byteCount;
}
}
}
複製代碼
搞清楚segment大概是什麼東西之後,咱們就能夠來看看buffer的write方法到底作了啥了。
//循環寫入數據
public Buffer write(byte[] source, int offset, int byteCount) {
if(source == null) {
throw new IllegalArgumentException("source == null");
} else {
Util.checkOffsetAndCount((long)source.length, (long)offset, (long)byteCount);
Segment tail;
int toCopy;
for(int limit = offset + byteCount; offset < limit; tail.limit += toCopy) {
//拿出一個可用的segment容器來,他的內部就是byte數組也就是data,拿出的原則就是看segment是否有足夠的空//間寫入
tail = this.writableSegment(1);
//計算剩餘空間長度
toCopy = Math.min(limit - offset, 8192 - tail.limit);
//把byte數組 複製到segmnt的容器中 而且計算索引
System.arraycopy(source, offset, tail.data, tail.limit, toCopy);
offset += toCopy;
}
//更新buffer的大小
this.size += (long)byteCount;
return this;
}
}
複製代碼
到目前咱們就能夠稍微捋一捋okio寫入數據的流程:
其實就是把bytes數組 往buffer裏面寫,buffer 裏面 是一個雙向的segment鏈表。寫入的數據實際上就存放在segment的data數組中 這個data數組固然是bytes數組。
可是目前咱們發現寫入的數據仍是在內存裏,在緩存裏啊,在哪裏真正的輸出到咱們的硬盤當中的呢?
public void close() throws IOException {
if(!this.closed) {
Throwable thrown = null;
try {
if(this.buffer.size > 0L) {
//這個sink其實就是一開始咱們傳進去的文件輸出流啊。以前全部的操做都在緩存裏。
//只有在這纔是真正的輸出到文件裏,到硬盤。
this.sink.write(this.buffer, this.buffer.size);
}
} catch (Throwable var3) {
thrown = var3;
}
try {
this.sink.close();
} catch (Throwable var4) {
if(thrown == null) {
thrown = var4;
}
}
this.closed = true;
if(thrown != null) {
Util.sneakyRethrow(thrown);
}
}
}
複製代碼