okio是okhttp的底層io庫,是一個我用起來比較方便io庫。然而知其然不知其所以然,所以我決定研究一下okio的源碼,這篇文件主要記錄下我學習okio源碼的心得。
okio的緩存類Segment.java
okio的原理是將要write、read的數據以byte[]的形式先緩存起來,然后再將緩存的數據write到目的地或者read成想要形式。而做到緩存數據的類就是Segment.java
我們直接看源碼:
final class Segment {
/** 一個Segment可以緩存數據的大小、源碼中定成8KB */
static final int SIZE = 8192;
/** 將Segment緩存的數據分享出去條件 */
static final int SHARE_MINIMUM = 1024;
/** 緩存的數據*/
final byte[] data;
/** 數據可以被讀取的起點*/
int pos;
/** 數據可以被讀取的終點*/
int limit;
/** 數據是分享出去、或者分享得到的*/
boolean shared;
/** 對數據擁有操作pos、limit的權限,分享得到的數據是沒有操作權限的*/
boolean owner;
/** 下一個Segment節點*/
Segment next;
/** 上一個Segment節點*/
Segment prev;
/** 一個擁有操作數據權限的Segment構造方法*/
Segment() {
this.data = new byte[SIZE];
this.owner = true;
this.shared = false;
}
/** 一個分享得到的Segment構造方法*/
Segment(Segment shareFrom) {
this(shareFrom.data, shareFrom.pos, shareFrom.limit);
shareFrom.shared = true;
}
/** 一個分享得到的Segment構造方法*/
Segment(byte[] data, int pos, int limit) {
this.data = data;
this.pos = pos;
this.limit = limit;
this.owner = false;
this.shared = true;
}
/**
* 雙向鏈表pop操作
* 鏈表中刪除自己,返回下一個節點操作
*/
public @Nullable Segment pop() {
Segment result = next != this ? next : null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
return result;
}
/**
* 雙向鏈表push操作
* 自己后面加入segment節點
*/
public Segment push(Segment segment) {
segment.prev = this;
segment.next = next;
next.prev = segment;
next = segment;
return segment;
}
//=================================================
// 之后都是優化Segment緩存數據的函數
//=================================================
/**
* 分割操作
* 把自己數據分割byteCount個出去
*/
public Segment split(int byteCount) {
//如過byteCount<= 0 或者 自己并沒有byteCount個數據,則拋出異常
if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
Segment prefix;
if (byteCount >= SHARE_MINIMUM) {
//如果要分割的數據數達到分享條件,把自己分享出去
prefix = new Segment(this);
} else {
//如果沒達到分享條件則從SegmentPool緩存池中獲取一塊可用的緩存空間
prefix = SegmentPool.take();
//把自己的數據復制給prefix
System.arraycopy(data, pos, prefix.data, 0, byteCount);
}
//分割操作的本質就是改變自己的pos、和prefix的limit達到分割的目的
prefix.limit = prefix.pos + byteCount;
pos += byteCount;
/**
* 將分割的數據push到自己之前的節點
* 因為prefix的數據和在自己的數據順序關系是prefix在自己之前
*/
prev.push(prefix);
//return分割出去的Segement
return prefix;
}
/**
* 寫入操作
* 將自己的byteCount個數據寫入另一個Segment
*/
public void writeTo(Segment sink, int byteCount) {
//如果另一個Segment沒有操作權限,直接拋出異常
if (!sink.owner) throw new IllegalArgumentException();
//如果另一個Segment沒有足夠的連續空間寫入,則嘗試壓縮data[]使其擁有足夠的連續空間
if (sink.limit + byteCount > SIZE) {
//如果另一個Segment分享出去了,那么就不能壓縮data[],拋出異常
if (sink.shared) throw new IllegalArgumentException();
//如果另一個Segment壓縮data[]之后還是沒有只夠的連續空間,拋出異常
if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
//壓縮data[]操作即:將數據的pos移動到data[0]的位置
System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
sink.limit -= sink.pos;
sink.pos = 0;
}
//經過或沒壓縮data[]操作后,將自己的byteCount個數據寫入另一個Segment
System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
sink.limit += byteCount;
pos += byteCount;
}
/**
* 合并Segement操作
* 嘗試將自己和前面一個節點合并,壓縮data[]達到優化緩存的目的
*/
public void compact() {
//如果前面一個節點就是自己,拋出異常
if (prev == this) throw new IllegalStateException();
//如果前面一個節點沒有操作權限則不能合并
if (!prev.owner) return;
//計算自己有多少數據
int byteCount = limit - pos;
//計算前面一個節點有多少剩余空間
int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
//如果自己的數據個數大于前面節點的剩余空間則不能進行合并
if (byteCount > availableByteCount) return;
//將數據寫入前一個節點
writeTo(prev, byteCount);
//雙向鏈表中刪除自己
pop();
//緩存池回收
SegmentPool.recycle(this);
}
}
從源碼可以看出Segment是一個雙向鏈表結構,源碼中有一個SegmentPool(緩存池)。這個類是用來維護Segment的,作用是回收利用Segment。我們來看下源碼:
final class SegmentPool {
/** 緩存池的最大SIZE為64KB
* 在Segment源碼中我們知道1個Segment的大小為8KB
* 即緩存池可以回收利用的Segment最多為8個
*/
static final long MAX_SIZE = 64 * 1024; // 64 KiB.
/** 第一個可以回收再利用的Segment*/
static @Nullable Segment next;
/** 緩存池現在擁有可再利用的緩存大小,一定是8KB的倍數 */
static long byteCount;
private SegmentPool() {
}
/**
* 獲取一個擁有操作權限的Segement
*/
static Segment take() {
synchronized (SegmentPool.class) {
//如果緩沖池中有就從緩存池中獲取
if (next != null) {
Segment result = next;
next = result.next;
result.next = null;
byteCount -= Segment.SIZE;
return result;
}
}
//如果沒有則直接new一個操作權限的Segement
return new Segment();
}
/**
* 回收Segment
*/
static void recycle(Segment segment) {
//如果segment還沒從雙向鏈表中脫離出則拋出異常
if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
//如果segement是分享得來的或分享出去的、則不能被回收
if (segment.shared) return;
synchronized (SegmentPool.class) {
//如果緩存池已經滿了,不能回收這個segment了
if (byteCount + Segment.SIZE > MAX_SIZE) return;
//將segment回收到緩存池鏈表
byteCount += Segment.SIZE;
segment.next = next;
segment.pos = segment.limit = 0;
next = segment;
}
}
}
從static void recycle(Segment segment){...}
函數可以看出,SegmentPool的緩存池是用一個單向鏈表來維護的,與Segment用雙向鏈表維護不同。Segment中使用雙向鏈表是為了讓數據的壓縮、分割、合并操作,更加方便和高效。而SegmentPool沒有這一需求,只要保證static Segment take(){...}
能得到Segement就好。
okio基本io結構
看完緩存我們來看看最重要的io操作
上面的類圖描述了一個最基本的io操作需要用到的東西,之后會講到。
Sink和Source是okio庫中最基礎io操作接口,定義了任何read、write操作都是從Buffer持有的Segment緩存中獲取數據再進行read、write。那么如何把數據read到緩存中,以及如何將緩存中的數據write到目的地呢?以write為例我們看Okio類中的一段源碼:
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();
//獲取Buffer的Segment緩存
Segment head = source.head;
//計算要寫入的數據個數
int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
//使用OutputStream將緩存數據寫入目標
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);
}
}
}
};
}
上面這段源碼可以看出Sink實際上是OutputStream的包裝,把緩存在Segment中的數據寫入目的地還是由OutputStream進行。同理Source也是InputStream的包裝,將數據讀取到Segment緩存還是由InputStream進行。
接下來我們看BufferedSink和BufferedSouce,這2個接口定義了各種類型的數據寫入Segment函數和把Segment數據以各種類型讀出的函數,方便大家使用。具體的實現實在Buffer中進行的。
舉個例子,BufferedSink接口中的定義了這么一個把數據源寫入緩存的函數
//將source[]中的數據從offset位置寫byteCount個到Segment緩存
BufferedSink write(byte[] source, int offset, int byteCount) throws IOException;
在Buffer中實現如下
@Override
public Buffer write(byte[] source, int offset, int byteCount) {
//如果你要寫入目標的數據源source為空,拋出異常
if (source == null) throw new IllegalArgumentException("source == null");
//檢查offset、byteCount、source.length是否有數據越界的關系,有則拋出異常
checkOffsetAndCount(source.length, offset, byteCount);
//計算要寫入數據的終點
int limit = offset + byteCount;
//如果要寫入數據的偏移位置小于要寫入數據的終點,開始寫入
while (offset < limit) {
//獲取一個擁有操作權限的Segment
Segment tail = writableSegment(1);
//計算要寫入這個Segment的字節數
int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
//寫入
System.arraycopy(source, offset, tail.data, tail.limit, toCopy);
offset += toCopy;
tail.limit += toCopy;
}
size += byteCount;
return this;
}
/**
* 獲取一個可用容量大等于minimumCapacity,且擁有權限的Segment
*
* @param minimumCapacity 最小可用容量
*/
Segment writableSegment(int minimumCapacity) {
if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();
if (head == null) {
//如果Buffer中沒有Segment緩存,則直接從緩存池中獲取一個Segment并將其作為head節點
head = SegmentPool.take();
return head.next = head.prev = head;
}
//獲取雙向鏈表的最后一個節點
Segment tail = head.prev;
if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
/**
* 如果最后一個節點沒有足夠的容量,或者沒有操作權限。
* 則從緩沖池中獲取一個Segment,并push到雙向鏈表的最后一個節點
*/
tail = tail.push(SegmentPool.take());
}
return tail;
}
同理,舉一個BufferedSouce中的讀取函數例子
//把Segement緩存中的數據讀取一個byte,以byte形式返回
byte readByte() throws IOException;
在Buffer中實現如下
@Override
public byte readByte() {
//如果緩存數據size==0,拋出異常
if (size == 0) throw new IllegalStateException("size == 0");
//獲取緩存的頭節點
Segment segment = head;
int pos = segment.pos;
int limit = segment.limit;
byte[] data = segment.data;
//讀取1字節
byte b = data[pos++];
size -= 1;
if (pos == limit) {
/**
*如果讀取后head節點沒有可以讀取的數據了
*則pop掉head節點,并且把head節點的下一個節點作為head
*/
head = segment.pop();
//緩存池回收
SegmentPool.recycle(segment);
} else {
segment.pos = pos;
}
return b;
}
現在下來理一下我們知道了的write、read流程:
- write
- read
那么如何將Okio實現的Sink、Source與Buffer連接起來呢?
答案是RealBufferedSink和RealBufferedSource
我們先來看看RealBufferedSource的部分源碼
final class RealBufferedSource implements BufferedSource {
/**讀寫Segment緩存的Buffer*/
public final Buffer buffer = new Buffer();
/**包裝了InputStream的Source*/
public final Source source;
/**用來判斷輸入流是否關閉*/
boolean closed;
/**構造函數傳入包裝了InputStream的Source*/
RealBufferedSource(Source source) {
if (source == null) throw new NullPointerException("source == null");
this.source = source;
}
/**以字節形式讀取1個字節*/
@Override
public byte readByte() throws IOException {
/**
* read前先請求說明需要從buffer的Segment中獲取1個byte,
* 1.如果buffer的Segment中有1個byte,則不進行任何操作
* 2.如果buffer的Segment中沒有1個byte
* 則使用Source包裝的InputStream讀取Segment.SIZE個數據到buffer的Segment中
* 如果InputStream讀取不到,則拋出異常
*/
require(1);
//buffer的Segment中數據以byte
return buffer.readByte();
}
@Override
public void require(long byteCount) throws IOException {
if (!request(byteCount)) throw new EOFException();
}
/**讀取到Segment*/
@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;
}
}
可以看出Okio類創建的包裝了InputStream的Source實例通過構造函數傳入RealBufferedSource類,RealBufferedSource類自己持有一個Buffer,這樣就將read流程的1、2步驟連接起來了
同理,我們再看看RealBufferedSink的部分源碼
final class RealBufferedSink implements BufferedSink {
/**讀寫Segment緩存的Buffer*/
public final Buffer buffer = new Buffer();
/**包裝了OutputStream的Sink*/
public final Sink sink;
/**用來判斷輸出流是否關閉*/
boolean closed;
/**構造函數傳入包裝了OutputStream的Sink*/
RealBufferedSink(Sink sink) {
if (sink == null) throw new NullPointerException("sink == null");
this.sink = sink;
}
/**寫入Segment*/
@Override
public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException {
//如果輸出流關閉了,拋出異常
if (closed) throw new IllegalStateException("closed");
//向buffer的Segment緩存寫入數據
buffer.write(source, offset, byteCount);
//完成寫入Segment緩存,提交給skin包裝的OutputStream將緩存寫到目的地
return emitCompleteSegments();
}
/**Segment寫到目的地*/
@Override
public BufferedSink emitCompleteSegments() throws IOException {
if (closed) throw new IllegalStateException("closed");
//先獲得t緩存中有多少數據
long byteCount = buffer.completeSegmentByteCount();
//調用sink包裝的OutputStream將緩存寫到目的地
if (byteCount > 0) sink.write(buffer, byteCount);
return this;
}
}
可以看出Okio類創建的包裝了OutputStream的Sink實例通過構造函數傳入RealBufferedSink類,RealBufferedSink類自己持有一個Buffer,這樣就將write流程的1、2步驟連接起來了
到此Okio的read、write流程學習完畢。
其他無關的廢話
剛走上開發的道路,各位大佬多多指教。另外,杭州3個月工作經驗,4個月實習經驗的Android開發有需要的嗎?