Okio是對java原生io的封裝,旨在簡化api同時優化io操作性能。接下來我會從下面幾個方面介紹
- Okio特性概述
- 讀寫流程源碼查看
- Buffer精華操作
- Timeout超時處理
1. Okio特性概述
java已經提供了很多io實現類供我們在不同場景使用,而Okio並不是一種新的io,而是對原生io的一次封裝,爲的是解決原生io的一些缺陷,下面我們介紹Okio的特性
1.1 api簡化
我們知道java的io相關的類非常多,有針對字節和字符的輸入輸出接口,有實現緩存的Bufferedxxx,以及各種子類實現比如文件的(FileInputStream和FileOutputStream),數據的(DataInputStream和DataOutputStream),對象的(ObjectInputStream和ObjectOutputStream)等等,針對不同的場景我們需要使用不同的類,是非常複雜的。
而Okio簡化了這一流程,統一通過Okio這個工廠類初始化,內部是通過重載方法根據傳入的參數不同初始化不同的流。
舉個板栗
File file = new File(Environment.getExternalStorageState(), "test");
Okio.buffer(Okio.sink(file)).writeString("aaaa", Util.UTF_8).close();
由於傳入的參數是File,內部是通過FileOutputStream進行io寫操作,並且支持鏈式操作。
1.2 緩存優化
原生的io緩存比較粗暴,Okio在這方面做了很多優化。
以BufferedOutputStream爲例,是利用一個長度爲8192的byte數組緩存,當要寫入數據長度大於緩存最大容量時跳過緩存直接進行io寫操作,當寫入數據長度大於緩存數組剩餘容量的時候先把緩存寫入輸出流,再將數據寫入緩存,其他情況直接寫入緩存。
然後原生的輸入輸出流之間的buffer是沒有辦法直接建立聯繫的,輸入流中的數據轉移到輸出流中是:輸入buf -> 臨時byte數組 -> 輸出buf,經歷兩次拷貝
//原生
File file = new File(Environment.getExternalStorageState(), "test");
try {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
int count = -1;
byte[] array = new byte[1024];//臨時byte數組
while ((count = bis.read(array)) != -1) {
bos.write(array, 0, count);
}
bis.close();
bos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
okio的話則是用一個Buffer對象去負責緩存,內部維護了一個Segment雙向鏈表,而這個Segment則是真正的數據緩存載體,內部利用一個長度爲8192的byte數組去緩存,當要寫入的數據超過8192的時候則會創建多個Segment對象在鏈表中有序存儲。當Segment使用完畢的時候又會利用SegmentPool進行回收,減少Segment對象創建。
對於輸入流流到輸出流有專門的優化,直接將輸入流buffer中segment數據轉移到輸出流的buffer中只有一次數據的操作,相比原生粗暴的buffer做了很多優化。(而實際操作是,如果是整段的segment數據轉移則是直接修改指針指到輸出流的buffer上,如果只轉移輸入流Segment部分數據則根據輸出Segment能不能裝得下,裝得下的話則進行數據拷貝,否則拆分輸入流Segment爲兩個然後將要轉移數據的Segment指針指向輸出流Buffer,總之Okio在這塊做了很多優化,這個後面會細說)
1.3 超時操作
原生進行io操作的時候是阻塞式的,沒有超時的處理,除非發生異常纔會停止,okio增加了超時處理,推出了Timeout機制,提供了同步超時和異步超時處理。
同步超時Timeout:是在每次進行讀寫前檢測是否超時,如果超時則拋出異常,那如果檢測完之後操作阻塞了很久是沒法停止的,只有等到下一次操作的時候纔會拋出異常停止操作。
異步超時AsyncTimeout:是在每次要進行讀寫操作的時候創建一個AsymcTimeout對象,然後通過一個鏈表存儲,根據即將超時時間排序,快要超時的排在鏈表頭部,然後啓動一個Watchdog線程進行檢測,優先從鏈表頭部取出AsyncTimeout判斷是否超時,超時了的話則調用AsyncTimeout#timeout()方法。okio給用socket創建流提供了默認實現,timeout的時候直接關閉Socket。
1.4 相關類介紹
- Source:Okio對輸入流的抽象接口,提供了read方法將數據讀到buffer中
- Sink:Okio對輸出流的抽象接口,提供了write方法將數據寫到buffer中
- BufferedSource和BufferedSink:是Okio對Buffer的接口抽象,分別繼承Source和Sink
- RealBufferedSource:BufferedSource實現類,讀操作都是由該類來完成,可以通過Okio工廠獲取
- RealBufferedSink:BufferedSink實現類,寫操作都是由該類來完成,可以通過Okio工廠獲取
- Okio:okio的工廠來用來獲取buffer實現類和流的實現類
- Segment:Buffer中存儲數據的載體,內部通過一個8K的byte數組存儲數據
- SegmentPool:管理Segment創建和回收,最多存儲64K的數據也就是8個Segment,當池中存在Segment的時候會複用減少對象的創建
以讀寫文件爲例,讀是先文件內容讀到buffer中,然後再從buffer中獲取數據,寫是先寫數據到buffer中,然後將將buffer中的數據寫入文件,而buffe中數據是通過一個個Segment存儲的,Segment則是通過SegmentPool創建和回收,每個類各司其職。
2. 讀寫流程源碼
2.1 讀流程
File file = new File(Environment.getExternalStorageState(), "test");
try {
BufferedSource source = Okio.buffer(Okio.source(file));
byte[] array = new byte[1024];
source.read(array);
source.close();
} catch (Exception e) {
e.printStackTrace();
}
根據上面的例子我們分幾段查看源碼
- Okio.source()
- Okio.buffer()
- BufferedSource.read()
2.1.1 Okio.source()
//Okio#source
public static Source source(File file) throws FileNotFoundException {
if (file == null) throw new IllegalArgumentException("file == null");
return source(new FileInputStream(file));
}
可以看到由於我們傳入的是file所以內部實際是通過FileInputStream讀文件
public static Source source(InputStream in) {
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();//判斷是否超時
Segment tail = sink.writableSegment(1);//從buffer中拿到一個可寫的Segment
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);//byteCount是要讀取的數據總量,Segment.SIZE - tail.limit是Segment可以裝的數據量,取最小值
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);//然後通過FileInputStream將文件數據讀到segment的data中
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 + ")";
}
};
}
上面這段就是創建了一個Source實現類,read的話則是先獲取buffer中可寫的segment,然後用FileInputStream將文件的數據讀到segment中。對於Segment和Buffer在下面會說到
2.1.2 Okio.Buffer()
//Okio#buffer
public static BufferedSource buffer(Source source) {
return new RealBufferedSource(source);
}
由於我們傳入的是Source所以會創建一個RealBufferedSource實例,Okio也都是通過它來做讀操作,我們先來看下這個類
final class RealBufferedSource implements BufferedSource {
public final Buffer buffer = new Buffer();//buffer對象
public final Source source;//Source實現類
RealBufferedSource(Source source) {
if (source == null) throw new NullPointerException("source == null");
this.source = source;
}
@Override
public int readInt() throws IOException {
require(4);//最終通過source#read()將數據讀到Buffer
return buffer.readInt();//再從Buffer讀取數據
}
@Override
public int read(byte[] sink, int offset, int byteCount) throws IOException {
checkOffsetAndCount(sink.length, offset, byteCount);
if (buffer.size == 0) {
long read = source.read(buffer, Segment.SIZE);//先將數據讀到Buffer
if (read == -1) return -1;
}
int toRead = (int) Math.min(byteCount, buffer.size);
return buffer.read(sink, offset, toRead);//再從Buffer讀取數據
}
@Override
public String readString(Charset charset) throws IOException {
if (charset == null) throw new IllegalArgumentException("charset == null");
buffer.writeAll(source);//最終通過source#read()將數據讀到Buffer
return buffer.readString(charset);//再從Buffer讀取數據
}
//後面省略了很多個read方法
}
可以看到它先將Okio.buffer(Okio.source(file))
傳入的Source實現類通過source成員變量存儲,然後創建了一個Buffer對象用作緩衝區,而read方法都是一個流程通過source#read()方法將數據寫入緩衝區,然後再從緩衝區中讀取數據。接下來我們在看下緩衝區Buffer對象
public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel {
@Nullable Segment head;//緩存數據的載體
long size;//當前緩衝區的數據量單位字節
public Buffer() {
}
//下面省略很多讀寫緩衝區的操作
}
Buffer是負責管理緩衝區的對象,內部則是通過一個雙向環狀鏈表Segment存儲數據,然後我們再來看下Segment是如何存儲數據的呢
final class Segment {
static final int SIZE = 8192;//最大存儲8K數據
final byte[] data;//存儲數據數組
int pos;//當前segment數據已經讀取到哪了,接下來從pos位置開始讀
int limit;//當前segment數據寫到哪了,接下來從limit開始寫
boolean owner;//當前segment是否允許追加數據,也就是能不能多次寫入
Segment next;
Segment prev;
Segment() {
this.data = new byte[SIZE];
this.owner = true;//默認可以多次寫入
this.shared = false;
}
可以看到segment內部有一個8k的byte數組來存儲數據,pos記錄的是segment可讀數據位置,(pos-1)到0是已經讀過的數據,limit是segment可寫數據的位置,limit到Segment.SIZE是剩餘可寫數據量,pos到limit是還未讀取的數據。
ok那我們回到本例中的source.read(array)
@Override public int read(byte[] sink) throws IOException {
return read(sink, 0, sink.length);
}
@Override public int read(byte[] sink, int offset, int byteCount) throws IOException {
checkOffsetAndCount(sink.length, offset, byteCount);//數組相關檢查
if (buffer.size == 0) {//如果緩衝區是空的
long read = source.read(buffer, Segment.SIZE);//將source中的數據讀取到buffer中
if (read == -1) return -1;
}
int toRead = (int) Math.min(byteCount, buffer.size);
return buffer.read(sink, offset, toRead);//將數據寫入sink也就是byte數組
}
對於source#read我們在2.1.1已經看過就是取出Buffer中的Segment,然後將數據寫入。不過細節當時我們沒說現在來看下
//source#read()
@Override public long read(Buffer sink, long byteCount) throws IOException {
try {
Segment tail = sink.writableSegment(1);//從buffer中拿到一個可寫的Segment
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);//byteCount是要讀取的數據總量,Segment.SIZE - tail.limit是Segment可寫數據量,取最小值
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);//然後通過FileInputStream將文件數據讀到segment的data數組中
if (bytesRead == -1) return -1;
tail.limit += bytesRead;//修改segment可寫位置
sink.size += bytesRead;//修改buffer中數據總量
return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}
buffer#writableSegment(1)如何獲取Segment的
Segment writableSegment(int minimumCapacity) {
if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();
if (head == null) {//如果buffer中沒有Segment
head = SegmentPool.take(); //從SegmentPool中獲取
return head.next = head.prev = head;//維護Buffer內部的Segment雙向環狀鏈表
}
Segment tail = head.prev;//拿到最後一個Segment
if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {//如果最後一個Segment可寫位置limit+需要的最小容量>Segment.SIZE || 該Segment不支持追加寫入
tail = tail.push(SegmentPool.take()); // 添加一個新的Segment到尾部
}
return tail;
}
如果Buffer中沒有Segment則直接從SegmentPool獲取,如果有則獲取鏈表尾部也就是最新的Segment,判斷數據是否存的下,存不下的話從SegmentPool獲取一個新的插到鏈表尾部
接下來看下SegmentPool#take如何創建Segment的
final class SegmentPool {
static final long MAX_SIZE = 64 * 1024; // 64 KiB.最大容量64K
static @Nullable Segment next;//通過一個單鏈表存儲回收的Segment
static long byteCount;//當前SegmentPool容量
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(); //否則直接創建
}
static void recycle(Segment segment) {
if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
if (segment.shared) return; //當前Segment如果有共享數據不能回收(這個後面說)
synchronized (SegmentPool.class) {
if (byteCount + Segment.SIZE > MAX_SIZE) return; //當前SegmentPool滿了的話則不能回收
byteCount += Segment.SIZE;//容量增加
segment.next = next;//加到鏈表中
segment.pos = segment.limit = 0;//可寫和可讀位置都清零
next = segment;
}
}
}
SegmentPool#take就是看當前的池子中有緩存的Segment的麼,有直接使用,沒有則創建一個。ok在回到最前面RealBufferedSource#read
@Override
public int read(byte[] sink, int offset, int byteCount) throws IOException {
checkOffsetAndCount(sink.length, offset, byteCount);
if (buffer.size == 0) {
long read = source.read(buffer, Segment.SIZE);//先將數據讀到Buffer
if (read == -1) return -1;
}
int toRead = (int) Math.min(byteCount, buffer.size);
return buffer.read(sink, offset, toRead);//再從Buffer讀取數據
}
第一塊source#read(buffer, Segment.SIZE)已經梳理了一遍就是通過FileInputStream將數據讀到Buffer的Segment中,然後再來buffer#read將數據讀到byte數組中
@Override public int read(byte[] sink, int offset, int byteCount) {
checkOffsetAndCount(sink.length, offset, byteCount);
Segment s = head;//拿到第一個Segment
if (s == null) return -1;
int toCopy = Math.min(byteCount, s.limit - s.pos);//判斷要讀取的數據量,取byteCount和當前segment可讀的數據量s.limit - s.pos
System.arraycopy(s.data, s.pos, sink, offset, toCopy);//將數據拷貝到數組中
s.pos += toCopy;//移動Segment已經讀過的數據指針pos
size -= toCopy;//當前Buffer容量減去讀過數據量
if (s.pos == s.limit) {//如果當前Segment已經讀完
head = s.pop();//從鏈表中脫離
SegmentPool.recycle(s);//SegmentPool嘗試回收
}
return toCopy;
}
將Buffer中Segment數據拷貝到數組中,如果Segment數據已經讀完則從Buffer鏈表中脫離,SegmentPool嘗試回收。
ok那讀流程就講完了,我們回顧下大體流程
BufferedSource source = Okio.buffer(Okio.source(file));
byte[] array = new byte[1024];
source.read(array);
source.close();
- Okio.Source():是創建了一個Source實現類,提供read的能力,需要傳入一個Buffer來獲取數據,數據讀取是通過FileInputStream來讀取的,寫入到Buffer的Segment中。
- Okio.buffer():則創建了一個Buffer實現類RealBufferedSource,內部維護了一個環形雙向鏈表Segment,是我們真正用來讀取數據的對象。
- BufferedSource.read():讀取數據操作,流程是先將數據讀到buffer中,然後再從buffer中讀取數據。
2.2 寫流程
寫流程是讀流程反過來,先將數據寫到buffer,然後在從buffer寫到文件中,大體跟前面差不多我們快速說一下
byte[] array = new byte[1024];
BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.write(array);
sink.close();
2.2.1 Okio.sink()
先看Okio#sink(file)
public static Sink sink(File file) throws FileNotFoundException {
if (file == null) throw new IllegalArgumentException("file == null");
return sink(new FileOutputStream(file));//創建FileOutputStream
}
public static Sink sink(OutputStream out) {
return sink(out, new Timeout());
}
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() {
@Override public void write(Buffer source, long byteCount) throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {//遍歷將Buffer中數據都寫入到文件中
timeout.throwIfReached();
Segment head = source.head;//獲取buffer中的segment
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();//segment脫鏈
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 + ")";
}
};
}
內部通過FileOutputStream進行io操作,通過一個while循環將Buffer中Segment數據統統寫入到文件中。
2.2.2 Okio.buffer()
public static BufferedSink buffer(Sink sink) {
return new RealBufferedSink(sink);
}
創建一個RealBufferedSink
final class RealBufferedSink implements BufferedSink {
public final Buffer buffer = new Buffer();//創建緩衝區
public final Sink sink;//真正的io操作對象
boolean closed;
RealBufferedSink(Sink sink) {
if (sink == null) throw new NullPointerException("sink == null");
this.sink = sink;
}
@Override public BufferedSink write(byte[] source) throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.write(source);//寫流程第一步是將數據寫到buffer
return emitCompleteSegments();//然後將Buffer數據寫到文件中
}
@Override public BufferedSink emitCompleteSegments() throws IOException {
if (closed) throw new IllegalStateException("closed");
long byteCount = buffer.completeSegmentByteCount();
if (byteCount > 0) sink.write(buffer, byteCount);//然後將Buffer數據寫到文件中
return this;
}
}
內部創建了一個Buffer作爲緩存區,並將Sink通過成員變量存儲起來,寫數據則是先將數據寫到buffer中,在由Buffer通過FileOutputStream寫到文件中。
接下來看看 buffer#write(source)是如何將數據寫入Buffer的
@Override public Buffer write(byte[] source) {
if (source == null) throw new IllegalArgumentException("source == null");
return write(source, 0, source.length);
}
@Override public Buffer write(byte[] source, int offset, int byteCount) {
if (source == null) throw new IllegalArgumentException("source == null");
checkOffsetAndCount(source.length, offset, byteCount);
int limit = offset + byteCount;//byte數組需要寫到的最後一位下標
while (offset < limit) {
Segment tail = writableSegment(1);//獲取可寫的Segment
int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
System.arraycopy(source, offset, tail.data, tail.limit, toCopy);//數據拷貝到Segment中
offset += toCopy;
tail.limit += toCopy;
}
size += byteCount;
return this;
}
循環的獲取一個可寫的Segment將數據寫到當中,直到byte數組中寫完。然後就是RealBufferedSink#emitCompleteSegments將Buffer數據寫到文件中
@Override public BufferedSink emitCompleteSegments() throws IOException {
if (closed) throw new IllegalStateException("closed");
long byteCount = buffer.completeSegmentByteCount();//獲取寫完的Segment中byte數
if (byteCount > 0) sink.write(buffer, byteCount);//然後將Buffer數據寫到文件中
return this;
}
buffer#completeSegmentByteCount()
public long completeSegmentByteCount() {
long result = size;
if (result == 0) return 0;
// Omit the tail if it's still writable.
Segment tail = head.prev;
if (tail.limit < Segment.SIZE && tail.owner) {//最後一個Segment如果沒裝滿則暫不寫入文件
result -= tail.limit - tail.pos;
}
return result;
}
獲取寫滿的Segment字節數
3. Buffer精華操作
除了前面看到的對緩衝區的優化接下來看看Okio對於輸入流流到輸出流的優化
try {
BufferedSource source = Okio.buffer(Okio.source(file));
BufferedSink sink = Okio.buffer(Okio.sink(file));
sink.writeAll(source);
sink.close();
source.close();
} catch (Exception e) {
e.printStackTrace();
}
先看sink#writeAll()
@Override public long writeAll(Source source) throws IOException {
if (source == null) throw new IllegalArgumentException("source == null");
long totalBytesRead = 0;
for (long readCount; (readCount = source.read(buffer, Segment.SIZE)) != -1; ) {//for循環將數據讀到輸出流的buffer中
totalBytesRead += readCount;
emitCompleteSegments();//在把數據從buffer寫入文件
}
return totalBytesRead;
}
調用了source.read()將數據讀到BufferedSink的buffer中
@Override public long read(Buffer sink, long byteCount) {
if (sink == null) throw new IllegalArgumentException("sink == null");
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (size == 0) return -1L;
if (byteCount > size) byteCount = size;
sink.write(this, byteCount);
return byteCount;
}
調用輸出流buffer.write()將數據寫入,前方高能重點來了
@Override public void write(Buffer source, long byteCount) {
if (source == null) throw new IllegalArgumentException("source == null");
if (source == this) throw new IllegalArgumentException("source == this");
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {
if (byteCount < (source.head.limit - source.head.pos)) {//如果只需要當前Segment部分數據,或者說讀到最後一個Segment了
Segment tail = head != null ? head.prev : null;
if (tail != null && tail.owner
&& (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {//如果要寫的數據byteCount+limit+(如果當前不是共享Segment,則可以覆蓋前面已讀數據所以+pos,如果是共享Segment則不能覆蓋已讀數據所以爲0)<= Segment.SIZE,證明寫Buffer的Segment裝得下
source.head.writeTo(tail, (int) byteCount);//把讀buffer中Segment數據寫到Segment中
source.size -= byteCount;
size += byteCount;
return;//return
} else {//如果裝不下則分割讀buffer的Segment爲兩個,將我們需要數據的部分放在頭部
source.head = source.head.split((int) byteCount);
}
}
//下面就是把讀buffer中segment從鏈表取出,拼接到寫buffer的segment鏈表中
Segment segmentToMove = source.head;
long movedByteCount = segmentToMove.limit - segmentToMove.pos;
source.head = segmentToMove.pop();//從讀buffer中斷鏈
if (head == null) {
head = segmentToMove;
head.next = head.prev = head;
} else {
Segment tail = head.prev;
tail = tail.push(segmentToMove);//拼接到寫buffer的鏈表中
tail.compact();//嘗試壓縮Segment數據,移除多餘Segment對象
}
source.size -= movedByteCount;
size += movedByteCount;
byteCount -= movedByteCount;
}
}
接下來我們梳理下上面的流程
- 先判斷當前是不是讀到最後一個Segment了,如果是在判斷寫buffer中最後一個Segment寫不寫的下,寫的下的話就寫入然後return結束了,否則把要讀的那個Segment拆分成兩段,將我們需要數據的部分放在頭部
- 如果第一個判斷中沒return,證明有整段的Segment數據需要拷貝,爲了提高效率則直接將讀buffer中Segment脫鏈,直接接到寫buffer中提高效率,然後嘗試壓縮Segment鏈表的數據,移除多餘的Segment
流程說了,接下來我們看上面沒分析的幾個方法
- source.head.writeTo(tail, (int) byteCount)
- source.head.split((int) byteCount)
- tail.compact()
先看source.head.writeTo(tail, (int) byteCount)
public void writeTo(Segment sink, int byteCount) {
if (!sink.owner) throw new IllegalArgumentException();
if (sink.limit + byteCount > SIZE) {//如果limit+byteCount>Size即裝不下所以需要把數據往前移覆蓋已讀數據
if (sink.shared) throw new IllegalArgumentException();//當前Segment是共享數組的則報錯,至於shared屬性在split方法的時候會說
if (sink.limit + byteCount - sink.pos > SIZE) 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(data, pos, sink.data, sink.limit, byteCount);//數據寫入
sink.limit += byteCount;
pos += byteCount;
}
可以看到Segment#writeTo()是將數據寫入傳入的Segment中,如果sink寫不下的話則會將已讀數據覆蓋騰出空間在寫入。
再來source.head.split((int) byteCount)
public Segment split(int byteCount) {
if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
Segment prefix;
if (byteCount >= SHARE_MINIMUM) {//如果要分割的數據量大於1024即1K則共享數組而不是數據拷貝
prefix = sharedCopy();//共享數組
} else {//數據量小於1K
prefix = SegmentPool.take();//創建一個新的Segment
System.arraycopy(data, pos, prefix.data, 0, byteCount);//拷貝數據
}
prefix.limit = prefix.pos + byteCount;
pos += byteCount;
prev.push(prefix);//插入分割出的Segment
return prefix;
}
Segment sharedCopy() {
shared = true;
return new Segment(data, pos, limit, true, false);//共享同一個data數組
}
split則是先判斷要分割的數據量大於1K麼,如果大於則使用共享數組的方式創建Segment減少拷貝,否則創建一個新的Segment通過數組拷貝的方式將數據傳入。
而前面判斷的shared就是這裏賦值的,使用數組共享方式創建的Segment,是不能爲了寫入數據將數據前移覆蓋已讀數據騰出位置,因爲持有的是數組引用會影響到別的Segment。
最後tail.compact()
public void compact() {
if (prev == this) throw new IllegalStateException();
if (!prev.owner) return; // Cannot compact: prev isn't writable.
int byteCount = limit - pos;//計算當前Segment數據量
int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
if (byteCount > availableByteCount) return; //判斷前一個Segment裝得下麼,裝不下return
writeTo(prev, byteCount);//將當前Segment數據寫到前一個Segment中
pop();//將當前Segment脫鏈
SegmentPool.recycle(this);//回收
}
判斷當前Segment中數據能放到前一個Segment麼,如果可以則將數據寫入,移除當前Segment。
ok這裏總結下Buffer的精髓操作
對於輸入流流到輸出流是將輸入流Buffer的數據直接轉移到輸出流Buffer中,轉移分爲兩種情況
- 整段的Segment數據轉移則是直接從輸出Buffer中脫鏈然後插入輸出Buffer中,直接修改指針效率非常高
- 非整段的Segment數據轉移是判斷輸出Buffer最後一個Segment是否寫的下,寫的下的話是數組拷貝,寫不下的話則將輸入Segment一分爲二,將要轉移數據的Segment放第一個,然後按照1方式整段Segment轉移到寫buffer中
並且對於Segment分割的時候有做優化,當需要轉移數據量小於1K的時候是通過數組拷貝的方式將數據寫到新Segment中,大於1K的時候是共享同一個數組,只是修改pos和limit來控制讀取區間。
4. Timeout超時處理
okio的超時分爲兩種
- 同步超時TimeOut
- 異步超時AsyncTimeout
4.1 同步超時TimeOut
同步超時比較簡單前面看到過,以寫爲例
public static Sink sink(OutputStream out) {
return sink(out, new Timeout());//創建同步超時對象
}
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() {
@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);
}
}
}
};
}
在每次寫之前調用throwIfReached()方法檢測是否超時
public void throwIfReached() throws IOException {
if (Thread.interrupted()) {
throw new InterruptedIOException("thread interrupted");
}
if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
throw new InterruptedIOException("deadline reached");
}
}
如果超時則拋出異常,當然如果循環中某次寫操作進行了很長時間是沒法立即停止的,只能等到下次while循環的時候纔會拋出異常停止,很明顯這樣是有缺陷的,於是還有異步超時。
4.2 異步超時AsyncTimeout
Okio給通過Socket方式創建的流提供的就是異步超時處理,以寫操作爲例
public static Sink sink(Socket socket) throws IOException {
if (socket == null) throw new IllegalArgumentException("socket == null");
if (socket.getOutputStream() == null) throw new IOException("socket's output stream == null");
AsyncTimeout timeout = timeout(socket);//創建了一個異步超時對象
Sink sink = sink(socket.getOutputStream(), timeout);
return timeout.sink(sink);//用AsyncTimeout.sink給sink包裝了一層
}
再看到AsyncTimeout#sink
public final Sink sink(final Sink sink) {
return new Sink() {
@Override public void write(Buffer source, long byteCount) throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
boolean throwOnTimeout = false;
enter();//在進行寫操作的時候會調用一下enter方法
try {
sink.write(source, toWrite);
byteCount -= toWrite;
throwOnTimeout = true;
} catch (IOException e) {
throw exit(e);
} finally {
exit(throwOnTimeout);//結束的時候會調用exit
}
}
}
再看超時開始的enter()和結束的exit()
public final void enter() {
if (inQueue) throw new IllegalStateException("Unbalanced enter/exit");
long timeoutNanos = timeoutNanos();//超時時間
boolean hasDeadline = hasDeadline();//是否有設置截止時間
if (timeoutNanos == 0 && !hasDeadline) {
return; // No timeout and no deadline? Don't bother with the queue.
}
inQueue = true;
scheduleTimeout(this, timeoutNanos, hasDeadline);//開始超時任務
}
private static synchronized void scheduleTimeout(
AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
if (head == null) {//如果head爲null創建一個AsyncTimeout作爲head站個位置沒有實際意義
head = new AsyncTimeout();
new Watchdog().start();//開啓一個Watchdog線程進行超時監聽
}
long now = System.nanoTime();
//計算超時時間賦值給timeoutAt屬性
if (timeoutNanos != 0 && hasDeadline) {
node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now);
} else if (timeoutNanos != 0) {
node.timeoutAt = now + timeoutNanos;
} else if (hasDeadline) {
node.timeoutAt = node.deadlineNanoTime();
} else {
throw new AssertionError();
}
long remainingNanos = node.remainingNanos(now);//獲取超時剩餘時間
for (AsyncTimeout prev = head; true; prev = prev.next) {//遍歷鏈表將剩餘時間少的排在鏈表頭部
if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
node.next = prev.next;
prev.next = node;
if (prev == head) {//如果是第一次notify喚醒watchdog
AsyncTimeout.class.notify();
}
break;
}
}
}
那梳理下上面代碼就是將AsyncTimeOut根據剩餘超時時間排序,即將超時的排在鏈表頭部,然後啓動了一個WatchDog線程去檢查超時情況,接下來看看WatchDog
private static final class Watchdog extends Thread {
Watchdog() {
super("Okio Watchdog");
setDaemon(true);
}
public void run() {
while (true) {
try {
AsyncTimeout timedOut;
synchronized (AsyncTimeout.class) {
timedOut = awaitTimeout();//獲取即將超時的AsyncTimeout
if (timedOut == null) continue;
if (timedOut == head) {//如果獲取到的爲head代表超時隊列沒有任務了return
head = null;
return;//return
}
}
timedOut.timedOut();//調用timeout.timeout()方法
} catch (InterruptedException ignored) {
}
}
}
}
WatchDog則是通過awaitTimeout獲取即將超時的AsyncTimeout對象,如果AsyncTimeout爲head則代表隊列中沒有任務了可以return,否則執行AsyncTimeout.timeout()方法觸發超時
static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException {
AsyncTimeout node = head.next;//拿到即將超時的AsyncTimeout
if (node == null) {
long startNanos = System.nanoTime();
AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
? head // The idle timeout elapsed.
: null; // The situation has changed.
}
long waitNanos = node.remainingNanos(System.nanoTime());//獲取剩餘超時時間
if (waitNanos > 0) {//如果大於0
// Waiting is made complicated by the fact that we work in nanoseconds,
// but the API wants (millis, nanos) in two arguments.
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
AsyncTimeout.class.wait(waitMillis, (int) waitNanos);//wait剩餘超時時間
return null;
}
head.next = node.next;
node.next = null;
return node;
}
}
awaitTimeout是獲取即將超時的AsyncTimeout,如果剩餘超時時間>0,則wait剩餘超時時間,否則返回超時的AsyncTimeout,而對於Socket,OKio給了默認實現。
@Override protected void timedOut() {
try {
socket.close();
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) {
// Catch this exception due to a Firmware issue up to android 4.2.2
// https://code.google.com/p/android/issues/detail?id=54072
logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
} else {
throw e;
}
}
}
timeout的時候會關閉socket,在看到取消超時任務的方法exit()
final void exit(boolean throwOnTimeout) throws IOException {
boolean timedOut = exit();
if (timedOut && throwOnTimeout) throw newTimeoutException(null);
}
public final boolean exit() {
if (!inQueue) return false;
inQueue = false;
return cancelScheduledTimeout(this);//取消超時任務
}
private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
// Remove the node from the linked list.
for (AsyncTimeout prev = head; prev != null; prev = prev.next) {//從節點中刪除AsyncTimeout
if (prev.next == node) {
prev.next = node.next;
node.next = null;
return false;//刪除成功則代表未超時
}
}
return true;//沒有找到該節點則超時
}
exit則是從超時隊列即AsyncTimeout鏈表中刪除該節點,如果刪除成功代表未超時,刪除失敗代表超時了,則會執行newTimeoutException(null)方法。提供個Socket的默認實現是拋出一個SocketTimeoutException
@Override protected IOException newTimeoutException(@Nullable IOException cause) {
InterruptedIOException ioe = new SocketTimeoutException("timeout");
if (cause != null) {
ioe.initCause(cause);
}
return ioe;
}
那麼接下來我們總結下異步超時,以Socket創建流爲例,在創建流的時候會創建一個AsyncTimeout對象,幷包裝生成的流對象,在進行操作的時候調用enter()方法開始超時任務,結束的調用exit()取消超時任務,內部則是將AsyncTimeout以超時時間進行排序,即將超時的排在前面,然後起一個WatchDog線程,從頭部開始獲取即將超時AsyncTimeout,如果還未超時則wait剩餘超時時間,超時了的話則從隊列中移除並調用timeout()方法,在exit()的時候則是看隊列中是否還有AsyncTimeout對象,如果有代表未超時,否則超時了調用newTimeoutException()拋出一個異常停止操作。