自從Google官方將OkHttp作為底層的網(wǎng)絡(luò)請(qǐng)求之后,作為OkHttp底層IO操作的Okio也是走進(jìn)開發(fā)者的視野,這個(gè)甚至是取代了java的原生IO庫的存在到底有什么特殊的本領(lǐng)呢?
這篇文章主要是對(duì)Okio的實(shí)現(xiàn)做一個(gè)詳盡的解析,當(dāng)然由于筆者分析中可能有紕漏的地方,也煩請(qǐng)指出,Okio的代碼比較精巧,核心的代碼大約5000行,對(duì)文章不盡興的也可以直接通讀源碼,這樣就能理解的更清晰。
全文較長,這里先放出整體的一個(gè)目錄圖
- 從Sample開始
- Sink和Source及其實(shí)現(xiàn)
- Okio中的超時(shí)機(jī)制
- Segment和SegmentPool解析
- 不可變的ByteString
- 最核心的Buffer解析
- 后記
那我們先看看Okio到底有什么好用的地方。
從Sample開始
為了展現(xiàn)Okio強(qiáng)大的能力,這里先舉幾個(gè)例子看看Okio是怎么處理IO操作的
讀寫文件
Okio中特有的兩個(gè)類Source,Sink代表的就是傳統(tǒng)的輸入流,和輸出流
Source source = null;
BufferedSource bSource = null;
File file = new File(filename);
//讀文件
source = Okio.source(file);
//通過source拿到 bufferedSource
bSource = Okio.buffer(source);
String read = bSource.readString(Charset.forName("utf-8"));
讀文件的步驟就是首先拿到一個(gè)輸入流,Okio中封裝了許多的輸入流統(tǒng)一使用方法重載的source方法轉(zhuǎn)換成一個(gè)source,然后使用buffer方法包裝成BufferedSource,這個(gè)里面提供了流的各種操作,讀String,讀字節(jié)數(shù)組,讀字byte,short等等,甚至是16進(jìn)制的數(shù),這里直接讀出文件的String內(nèi)容,十分的簡單。
private static void create_writer() {
String filename = "create.txt";
boolean isCreate = false;
Sink sink;
BufferedSink bSink = null;
try {
//判斷文件是否存在,不存在,則新建!
File file = new File(filename);
if (!file.exists()) {
isCreate = file.createNewFile();
} else {
isCreate = true;
}
//寫入操作
if (isCreate) {
sink = Okio.sink(file);
bSink = Okio.buffer(sink);
bSink.writeUtf8("1");
bSink.writeUtf8("\n");
bSink.writeUtf8("this is new file!");
bSink.writeUtf8("\n");
bSink.writeString("我是每二條", Charset.forName("utf-8"));
bSink.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != bSink) {
bSink.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
寫文件的操作使用的是Sink,同樣的將一個(gè)file輸出流包裝成一個(gè)Sink,再通過Okio的buffer方法賦予操作流的各種方法,最后寫入操作也是十分的簡單。
png decode
private static final ByteString PNG_HEADER = ByteString.decodeHex("89504e470d0a1a0a");
public void decodePng(InputStream in) throws IOException {
BufferedSource pngSource = Okio.buffer(Okio.source(in));
ByteString header = pngSource.readByteString(PNG_HEADER.size());
if (!header.equals(PNG_HEADER)) {
throw new IOException("Not a PNG.");
}
}
這個(gè)是Okio官方提供了一個(gè)Png圖片的解碼的例子,我們知道一般判斷一個(gè)文件的格式就是依靠前面的校驗(yàn)碼,比如class文件中前面的16進(jìn)制代碼就是以 cafebabe
開頭,同樣的常規(guī)的png,jpg,gif之類的都可以通過前面的魔數(shù)來進(jìn)行判斷文件類型,這里就以一個(gè)圖片輸入流轉(zhuǎn)換成一個(gè)BufferedSource,并且通過 readByteString
方法拿到一個(gè)字節(jié)串 ByteString
這樣就能驗(yàn)證這個(gè)文件是不是一個(gè)png的圖片,同樣的方法也能用在其他文件的校驗(yàn)上。
Okio除了這些外還有很多額外的功能,而且官方也提供了許多包括對(duì)于zip文件的處理,各種MD5,SHA-1.SHA256,Base64之類編碼的處理,如果需要額外的一些操作,也可以自己實(shí)現(xiàn)Sink,Source對(duì)應(yīng)的方法。
看完了例子,就來看看Okio真正的實(shí)現(xiàn)吧
Sink和Source及其實(shí)現(xiàn)
Okio中最重要的兩個(gè)概念當(dāng)屬Sink,Source,先看看這兩個(gè)類的繼承圖
Sink代表的輸出流,Source代表的是輸入流,這兩個(gè)基本都是對(duì)稱的,所以就只用一個(gè)來進(jìn)行分析了
public interface Sink extends Closeable,Flushable {
@Override
void flush() throws IOException;
@Override
void close() throws IOException;
Timeout timeout();
void write(Buffer source,long byteCount) throws IOException;
}
Sink中只包括了一些最簡單的方法,以及一個(gè)timeout超時(shí),這個(gè)后面會(huì)講到。真正龐大的寫的方法實(shí)際上都是由繼承這個(gè)接口的另一個(gè)接口中的方法,從上面的UML圖中可以看到整個(gè)繼承鏈
里面包含大量的寫的接口方法,這個(gè)BufferedSink依然只是一個(gè)接口,實(shí)現(xiàn)這個(gè)接口的類就是
RealBufferedSink
public final Buffer buffer = new Buffer();
public final Sink sink;
public RealBufferedSink(Sink sink){
if (sink == null)
throw new NullPointerException("sink == null");
this.sink = sink;
}
@Override
public Buffer buffer() {
return buffer;
}
RealBufferedSink類中有兩個(gè)主要參數(shù),一個(gè)是新建的Buffer對(duì)象,一個(gè)是Sink的對(duì)象。
雖然這個(gè)類叫RealBufferedSink,但是實(shí)際上這個(gè)只是一個(gè)保存Buffer對(duì)象的一個(gè)代理實(shí)現(xiàn),真正的實(shí)現(xiàn)都是在Buffer中實(shí)現(xiàn)的,可以看看這個(gè)類的幾個(gè)例子
@Override public BufferedSink write(byte[] source) throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.write(source);
return emitCompleteSegments();
}
@Override public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.write(source, offset, byteCount);
return emitCompleteSegments();
}
可以看到這個(gè)實(shí)現(xiàn)了BufferedSink接口的兩個(gè)方法實(shí)際上都是調(diào)用了buffer的對(duì)應(yīng)方法,對(duì)應(yīng)的RealBufferedSource也是同樣的調(diào)用buffer中的read方法,關(guān)于Buffer這個(gè)類會(huì)在下面詳述,剛才我們看到Sink接口中有一個(gè)Timeout的類,這個(gè)就是Okio所實(shí)現(xiàn)的超時(shí)機(jī)制,保證了IO操作的穩(wěn)定性。
Okio中的超時(shí)機(jī)制
Okio的超時(shí)機(jī)制讓IO不會(huì)因?yàn)楫惓W枞谀硞€(gè)未知的錯(cuò)誤上,Okio的基礎(chǔ)超時(shí)機(jī)制是采用的同步超時(shí)
以輸出流為例,當(dāng)我們用下面的方法包裝流時(shí)
public static Sink sink(OutputStream out){
return sink(out,new Timeout());
}
實(shí)際上調(diào)用了一個(gè)兩個(gè)參數(shù)的sink方法,第二個(gè)參數(shù)就是同步超時(shí)
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 {
Util.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);
byteCount -= toCopy;
source.size += toCopy;
head.pos += toCopy;
...
}
}
};
}
可以看到write方法中實(shí)際上有一個(gè)while循環(huán),在每個(gè)開始寫的時(shí)候就調(diào)用了 timeout.throwIfReached()
方法,這個(gè)方法里面去判斷的時(shí)間是否超時(shí),這很明顯是一個(gè)同步超時(shí)機(jī)制,按序執(zhí)行,同樣的Source也是一樣的操作
public void throwIfReached() throws InterruptedIOException {
if (Thread.interrupted()){
throw new InterruptedIOException("thread interrupted");
}
if (hasDeadline && deadlineNanoTime - System.nanoTime() < 0){
throw new InterruptedIOException("deadline reached");
}
}
但是當(dāng)我們看Okio對(duì)于socket的封裝時(shí)
public static Sink sink(Socket socket) throws IOException {
if (socket == null) throw new IllegalArgumentException("socket == null");
AsyncTimeout timeout = timeout(socket);
Sink sink = sink(socket.getOutputStream(), timeout);
return timeout.sink(sink);
}
這里出現(xiàn)了一個(gè) AsyncTimeout
的類,這個(gè)實(shí)際上是繼承于Timeout所實(shí)現(xiàn)的一個(gè)異步超時(shí)類,這個(gè)異步類比同步要復(fù)雜的多,它使用了一個(gè)WatchDog線程在后臺(tái)進(jìn)行監(jiān)聽超時(shí),這里的WatchDog并不是linux中的那個(gè),只是一個(gè)繼承于Thread的一個(gè)類,里面的run方法執(zhí)行的就是核心的超時(shí)判斷,之所以在socket寫時(shí)采取異步超時(shí),這完全是由socket自身的性質(zhì)決定的,socket經(jīng)常會(huì)阻塞自己,導(dǎo)致下面的事情執(zhí)行不了。
AsyncTimeout繼承于Timeout類,可以覆寫里面的timeout方法,這個(gè)方法會(huì)在watchdog的線程中調(diào)用,所以不能執(zhí)行長時(shí)間的操作,否則就會(huì)引發(fā)其他的超時(shí),下面詳細(xì)分析這個(gè)類
//不要一次寫超過64k的數(shù)據(jù)否則可能會(huì)在慢連接中導(dǎo)致超時(shí)
private static final int TIMEOUT_WRITE_SIZE = 64 * 1024;
private static AsyncTimeout head;
private boolean inQueue;
private AsyncTimeout next;
private long timeoutAt;
首先就是一個(gè)最大的寫的值,定義為64K,剛好和一個(gè)Buffer的大小是一樣的,官方解釋是因?yàn)槿绻B續(xù)寫超過這個(gè)數(shù)的字節(jié),那么及其容易導(dǎo)致超時(shí),所以為了限制這個(gè)操作,直接給出了一次能寫的最大數(shù)。
下面兩個(gè)參數(shù)一個(gè)head,next很明顯表明這是一個(gè)單鏈表,timeoutAt則是超時(shí)的時(shí)間。
使用者在操作之前首先要調(diào)用enter方法,這樣相當(dāng)于注冊(cè)了這個(gè)超時(shí)監(jiān)聽,然后配對(duì)的實(shí)現(xiàn)exit方法,這個(gè)exit有一個(gè)返回值會(huì)表明超時(shí)是否觸發(fā),請(qǐng)注意這個(gè)timeout是異步的,可能會(huì)在exit后才調(diào)用
public final void enter() {
if (inQueue)
throw new IllegalArgumentException("unbalanced enter/exit");
long timeoutNanos = timeoutNanos();
boolean hasDeadline = hasDeadline();
if (timeoutNanos == 0 && !hasDeadline) {
return; // 沒有超時(shí)的設(shè)置
}
inQueue = true;
scheduleTimeout(this, timeoutNanos, hasDeadline);
}
這里只是做了判斷以及設(shè)置inQueue的狀態(tài),真正的是調(diào)用 scheduleTimeout
方法來加入到鏈表中
...
long remainingNanos = node.remainingNanos(now);
for (AsyncTimeout prev = head ; true ; prev = prev.next){
//如果下一個(gè)為null或者剩余時(shí)間比下一個(gè)短 就插入node
if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)){
node.next = prev.next;
prev.next = node;
if (prev == node){
AsyncTimeout.class.notify();
}
break;
}
}
上面可以看出這個(gè)鏈表實(shí)際上是按照剩余的超時(shí)時(shí)間來進(jìn)行排序的,快到超時(shí)的節(jié)點(diǎn)排在表頭,依次往后遞增。
我們以一個(gè)read的代碼來看整個(gè)超時(shí)的綁定過程
@Override
public long read(Buffer sink, long byteCount) throws IOException {
boolean throwOnTimeout = false;
enter();
try {
long result = source.read(sink,byteCount);
throwOnTimeout = true;
return result;
}catch (IOException e){
throw exit(e);
}finally {
exit(throwOnTimeout);
}
}
首先調(diào)用enter方法,然后來做讀的操作,這里可以看到不僅在catch上而且在finally中也做了操作,這樣異常和正常的情況都考慮到了,在exit中調(diào)用了真正的exit方法,exit中會(huì)去判斷這個(gè)異步超時(shí)的對(duì)象是否在鏈表中
final void exit(boolean throwOnTimeout) throws IOException {
boolean timeOut = exit();
if (timeOut && throwOnTimeout)
throw newTimeoutException(null);
}
public final boolean exit(){
if (!inQueue)
return false;
inQueue = false;
return cancelScheduledTimeout(this);
}
回到前面所說的WatchDog,內(nèi)部的run方法是一個(gè)while(true)的一個(gè)循環(huán),
while (true) {
try {
AsyncTimeout timeout;
synchronized (AsyncTimeout.class) {
timeout = awaitTimeout();
//沒有找到一個(gè)node來interrupt 繼續(xù)
if (timeout == null){
continue;
}
...
timeout.timedOut();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
這里鎖住了內(nèi)部的awaitTimeout操作,這個(gè)await正是判斷是否超時(shí)的真正地方
static AsyncTimeout awaitTimeout() throws InterruptedException {
//拿到下一個(gè)節(jié)點(diǎn)
AsyncTimeout node = head.next;
//如果queue為空,等待直到有node進(jìn)隊(duì),或者觸發(fā)IDLE_TIMEOUT_MILLS
if (node == null) {
long startNanos = System.nanoTime();
AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLS);
return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS ? head : null;
}
long waitNanos = node.remainingNanos(System.nanoTime());
//這個(gè)head依然還沒有超時(shí),繼續(xù)等待
if (waitNanos > 0) {
//這里比較奇怪,但是是wait API的需求需要兩個(gè)參數(shù)
long waitMills = waitNanos / 1000000L;
waitNanos -= (waitMills * 1000000L);
AsyncTimeout.class.wait(waitMills, (int) waitNanos);
return null;
}
head.next = node.next;
node.next = null;
return node;
}
代碼中有些注釋已經(jīng)寫得比較清晰了,主要就是通過這個(gè) remainingNanos
來判斷預(yù)定的超時(shí)時(shí)間減去當(dāng)前時(shí)間是否大于0,如果比0大就說明還沒超時(shí),于是wait剩余的時(shí)間,然后表示并沒有超時(shí),如果小于0,就會(huì)把這個(gè)從鏈表中移除,根據(jù)前面exit方法中的判斷就能觸發(fā)整個(gè)超時(shí)的方法,異步超時(shí)這一部分代碼比較復(fù)雜,涉及到許多wait,鏈表,加鎖,需要詳細(xì)閱讀源碼才能理解深刻,不清楚的可以詳細(xì)看看。
Segment和SegmentPool解析
Segment字面翻譯就是片段,Okio將數(shù)據(jù)也就是Buffer分割成一塊塊的片段,同時(shí)segment擁有前置節(jié)點(diǎn)和后置節(jié)點(diǎn),構(gòu)成一個(gè)雙向循環(huán)鏈表,就像下面這個(gè)圖的方式。
這樣采取分片使用鏈表連接,片中使用數(shù)組存儲(chǔ),兼具讀的連續(xù)性,以及寫的可插入性,對(duì)比單一使用鏈表或者數(shù)組,是一種折中的方案,讀寫更快,而且有個(gè)好處根據(jù)需求改動(dòng)分片的大小來權(quán)衡讀寫的業(yè)務(wù)操作,另外,segment也有一些內(nèi)置的優(yōu)化操作,綜合這些Okio才能大放異彩,后面在Buffer解析會(huì)講解什么時(shí)候形成的雙向循環(huán)鏈表
static final int SIZE = 8192;
static final int SHARE_MINIMUM = 1024;
final byte[] data;
int pos;
int limit;
boolean shared;
boolean owner;
Segment pre;
Segment next;
SIZE就是一個(gè)segment的最大字節(jié)數(shù),其中還有一個(gè)SHARE_MINIMUM,這個(gè)涉及到segment優(yōu)化中的另一個(gè)技巧,共享內(nèi)存,然后data就是保存的字節(jié)數(shù)組,pos,limit就是開始和結(jié)束點(diǎn)的index,shared和owner用來設(shè)置狀態(tài)判斷是否可寫,一個(gè)有共享內(nèi)存的segment是不能寫入的,pre,next就是前置后置節(jié)點(diǎn)。
Segment方法分析
既然是雙向循環(huán)鏈表,其中也會(huì)有一些操作的方法,比如
public Segment pop(){
Segment result = next != this ? next : null;
pre.next = next;
next.pre = pre;
next = null;
pre = null;
return result;
}
pop方法移除了自己,首先將自己的前后兩個(gè)節(jié)點(diǎn)連接起來,然后將自己的前后引用置空,這樣就脫離了整個(gè)雙向鏈表,然后返回next
public Segment push(Segment segment){
segment.pre = this;
segment.next = next;
next.pre = segment;
next = segment;
return segment;
}
push方法就是在當(dāng)前和next引用中間插入一個(gè)segment進(jìn)來,并且返回插入的segment,這兩個(gè)都是尋常的雙向鏈表的操作,我們?cè)賮砜纯慈绾螌懭霐?shù)據(jù)
public void writeTo(Segment sink , int byteCount){
if (!sink.owner)
throw new IllegalArgumentException();
if (sink.limit + byteCount > SIZE){ //limit和需要寫的字節(jié)總和大于SIZE
if (sink.shared) //共享無法寫
throw new IllegalArgumentException();
if (sink.limit + byteCount - sink.pos > SIZE){ //如果減去頭依然比SIZE大 那么就無法寫拋異常
throw new IllegalArgumentException();
}
//否則我們需要先移動(dòng)要寫的文件地址 然后置limit pos的地址
System.arraycopy(sink.data,sink.pos,sink.data,0,sink.limit - sink.pos);
sink.limit = sink.limit - sink.pos;
sink.pos = 0;
}
//開始尾部寫入 寫完置limit地址
System.arraycopy(data,pos,sink.data,sink.limit,byteCount);
sink.limit = sink.limit + byteCount;
pos = pos + byteCount; //當(dāng)前索引后移
}
owner和Shared這兩個(gè)狀態(tài)目前看來是完全相反的,賦值都是同步賦值的,這里有點(diǎn)不明白存在兩個(gè)參數(shù)的意義,現(xiàn)在的功能主要是用來判斷如果是共享就無法寫,以免污染數(shù)據(jù),會(huì)拋出異常。當(dāng)然,如果要寫的字節(jié)大小加上原來的字節(jié)數(shù)大于單個(gè)segment的最大值也是會(huì)拋出異常,也存在一種情況就是雖然尾節(jié)點(diǎn)索引和寫入字節(jié)大小加起來超過,但是由于前面的pos索引可能因?yàn)閞ead方法取出數(shù)據(jù),pos索引后移這樣導(dǎo)致可以容納數(shù)據(jù),這時(shí)就先執(zhí)行移動(dòng)操作,使用系統(tǒng)的 System.arraycopy
方法來移動(dòng)到pos為0的狀態(tài),更改pos和limit索引后再在尾部寫入byteCount數(shù)的數(shù)據(jù),寫完之后實(shí)際上原segment讀了byteCount的數(shù)據(jù),所以pos需要后移這么多。過程十分的清晰,比較好理解。
除了寫入數(shù)據(jù)之外,segment還有一個(gè)優(yōu)化的技巧,因?yàn)槊總€(gè)segment的片段size是固定的,為了防止經(jīng)過長時(shí)間的使用后,每個(gè)segment中的數(shù)據(jù)千瘡百孔,可能十分短的數(shù)據(jù)卻占據(jù)了一整個(gè)segment,所以有了一個(gè)壓縮機(jī)制
public void compact(){
if (pre == this)
throw new IllegalStateException();
if (!pre.owner) // pre不可寫
return;
int byteCount = limit - pos;
int availableByteCount = SIZE - pre.limit + (pre.shared ? 0 : pre.pos); //前一個(gè)的剩余大小
if (byteCount > availableByteCount)
return;
writeTo(pre,byteCount); //將數(shù)據(jù)寫入到前一個(gè)的片段中
pop(); // 從雙向鏈表中移除當(dāng)前
SegmentPool.recycle(this); //加入到對(duì)象池中
}
照例如果前面是共享的那么不可寫,也就不能壓縮了,然后判斷前一個(gè)的剩余大小是否比當(dāng)前的大,有足夠的空間來容納數(shù)據(jù),調(diào)用前面的 writeTo
方法來寫數(shù)據(jù),寫完后移除當(dāng)前segment,然后通過 SegmentPool
來回收。
另一個(gè)技巧就是共享機(jī)制,為了減少數(shù)據(jù)復(fù)制帶來的性能開銷,segment存在一個(gè)共享機(jī)制
public Segment split(int byteCount){
if (byteCount <= 0 || byteCount > limit - pos )
throw new IllegalArgumentException();
Segment prefix;
if (byteCount >= SHARE_MINIMUM){ //如果byteCount大于最小的共享要求大小
prefix = new Segment(this); //this這個(gè)構(gòu)造函數(shù)會(huì)
}else {
prefix = SegmentPool.take();
System.arraycopy(data,pos,prefix,0,byteCount);
}
prefix.limit = prefix.pos + byteCount;
pos = pos + byteCount;
pre.push(prefix);
return prefix;
}
這個(gè)方法實(shí)際上經(jīng)過了很多次的改變,在回顧Okio的1.6的版本時(shí),發(fā)現(xiàn)有一個(gè)重要的差異就是多了一個(gè) SHARE_MINIMUM
參數(shù),同時(shí)也多了一個(gè)注釋,為了防止一個(gè)很小的片段就進(jìn)行共享,我們知道共享之后為了防止數(shù)據(jù)污染就無法寫了,如果存在大片的共享小片段,實(shí)際上是很浪費(fèi)資源的,所以通過這個(gè)對(duì)比可以看出這個(gè)最小數(shù)的意義,而且這個(gè)方法在1.6的版本中檢索實(shí)際上只有一個(gè)地方使用了這個(gè)方法,就是Buffer中的write方法,為了效率在移動(dòng)大數(shù)據(jù)的時(shí)候直接移動(dòng)整個(gè)segment而不是data,這樣在寫數(shù)據(jù)上能達(dá)到很高的效率,具體write的細(xì)節(jié)會(huì)在Buffer一章中詳細(xì)描述。
再回頭看剛才的 compact
中出現(xiàn)的 SegmentPool
,這個(gè)實(shí)際上是一個(gè)segment的對(duì)象池
static final long MAX_SIZE = 64 * 1024;
static Segment next;
static long byteCount;
同樣的有一個(gè)池子的上限,也就是64k,相當(dāng)于8個(gè)segment,next這個(gè)節(jié)點(diǎn)可以看出這個(gè) SegmentPool
是按照單鏈表的方式進(jìn)行存儲(chǔ)的,byteCount則是目前已有的大小。
SegmentPool方法分析
SegmentPool的方法十分的少,一個(gè)取,一個(gè)回收,十分簡潔。
/**
* take方法用來取數(shù)據(jù)
* 如果池子為空就創(chuàng)建一個(gè)空對(duì)象 owner true | share false
* next是鏈表的頭 就是一個(gè)簡單的取表頭的操作
* @return
*/
static Segment take(){
synchronized (SegmentPool.class){
if (next != null){
Segment result = next;
next = result.next;
result.next = null;
byteCount = byteCount - Segment.SIZE;
return result;
}
}
return new Segment();
}
為了防止多線程同時(shí)操作造成數(shù)據(jù)的錯(cuò)亂,這里加了鎖,這里的next命名雖然是next,但是實(shí)際上是整個(gè)對(duì)象池的頭,但是next為空,表示池子為空,直接返回一個(gè)空對(duì)象,否則從里面拿出next,并將next的下一個(gè)節(jié)點(diǎn)賦為next,置一下狀態(tài),這個(gè)方法就結(jié)束了
/**
* 如果當(dāng)前要回收的segment有前后引用或者是共享的 那么就回收失敗
* 如果加入后的大小超過了最大大小 也會(huì)失敗
* 然后將新回收的next指向表頭 也就是加到的鏈表的頭 并且將回收的segment置為next也就是head
* @param segment
*/
static void recycle(Segment segment){
if (segment.next != null || segment.pre != null)
throw new IllegalArgumentException();
if (segment.shared)
return;
synchronized (SegmentPool.class){
if (byteCount + Segment.SIZE > MAX_SIZE){
return;
}
byteCount += Segment.SIZE;
segment.next = next;
segment.pos = segment.limit = 0;
next = segment;
}
}
如果要回收的segment有前后引用或者是共享的,就不能被回收,所以要回收前先將引用置空,同樣這里也加了鎖,以免那個(gè)同時(shí)回收超過池子最大的大小,然后就是將回收的插到表頭的操作。
所以SegmentPool無論是回收和取對(duì)象都是在表頭操作。
不可變的ByteString
我們都知道String是一個(gè)不可以改變的一個(gè)對(duì)象,那可能有人問了誰說不能改變了,明明還能做分割添加的操作,那這里就不詳述了,有興趣的可以看 Java中的String為什么是不可變的? -- String源碼分析 這篇文章,同樣的ByteString也是一個(gè)不可變的對(duì)象,當(dāng)然,java語言可沒有不可變標(biāo)記關(guān)鍵字,如果想要實(shí)現(xiàn)一個(gè)不可變的對(duì)象,還需要一些操作。
Effective Java一書中有一條給了不可變對(duì)象需要遵循的幾條原則:
- 不要提供任何會(huì)修改對(duì)象狀態(tài)的方法
- 保證類不會(huì)被擴(kuò)展
- 使所有的域都是final的
- 使所有的域都是private的
- 確保對(duì)于任何可變組件的互斥訪問
不可變的對(duì)象有許多的好處,首先本質(zhì)是線程安全的,不要求同步處理,也就是沒有鎖之類的性能問題,而且可以被自由的共享內(nèi)部信息,當(dāng)然壞處就是需要?jiǎng)?chuàng)建大量的類的對(duì)象。
byte[] data;
transient String utf8;
ByteString不僅是不可變的,同時(shí)在內(nèi)部有兩個(gè)filed,分別是byte數(shù)據(jù),以及String的數(shù)據(jù),這樣能夠讓這個(gè)類在Byte和String轉(zhuǎn)換上基本沒有開銷,同樣的也需要保存兩份引用,這是明顯的空間換時(shí)間的方式,為了性能Okio做了很多的事情。
但是這個(gè)String前面有 transient
關(guān)鍵字標(biāo)記,也就是說不會(huì)進(jìn)入序列化和反序列化,所有我們看到兩個(gè)方法中并沒有utf8這個(gè)屬性。
private void readObject(ObjectInputStream in) throws IOException {
int dataLength = in.readInt();
ByteString byteString = ByteString.read(in, dataLength);
try {
Field field = ByteString.class.getDeclaredField("data");
field.setAccessible(true);
field.set(this, byteString.data);
} catch (NoSuchFieldException e) {
throw new AssertionError();
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(data.length);
out.write(data);
}
除此之外, ByteString
內(nèi)置了不少操作,方便使用
最核心的Buffer解析
前面講到Buffer這個(gè)類實(shí)際上就是整個(gè)讀和寫的核心,包括 RealBufferedSource
和 RealBufferedSink
實(shí)際上都只是一個(gè)代理,里面的操作全部都是通過Buffer來完成的
public class Buffer implements BufferedSource, BufferedSink, Cloneable {
long size;
Segment head;
整個(gè)Buffer持有了一個(gè)Segment的引用,通過這個(gè)引用能拿到整個(gè)鏈表中所有的數(shù)據(jù)。
Buffer一共實(shí)現(xiàn)了三個(gè)接口,讀,寫,以及clone。
先從最簡單的clone說起,clone是一種對(duì)象生成的方式,是除了常規(guī)的new·關(guān)鍵字以及反序列化之外的一種方式,主要分為深拷貝和淺拷貝兩種,Buffer采用的是深拷貝的方式
public Buffer clone(){
Buffer result = new Buffer();
if (size == 0){
return result;
}
result.head = new Segment(head);
result.head.pre = result.head.next = result.head;
for (Segment s = head.next ; s != head ; s = s.next){
result.head.pre.push(new Segment(s)); //這里選擇的pre上push一個(gè)segment
}
result.size = size;
return result;
}
對(duì)應(yīng)實(shí)現(xiàn)的clone方法,如果整個(gè)Buffer的size為null,也就是沒有數(shù)據(jù),那么就返回一個(gè)新建的Buffer對(duì)象,如果不為空就是遍歷所有的segment并且都創(chuàng)建一個(gè)對(duì)應(yīng)的Segment,這樣clone出來的對(duì)象就是一個(gè)全新的毫無關(guān)系的對(duì)象。
前面分析segment的時(shí)候有講到是一個(gè)雙向循環(huán)鏈表,但是segment自身構(gòu)造的時(shí)候卻沒有形成閉環(huán),其實(shí)就是在Buffer中產(chǎn)生的
result.head.pre = result.head.next = result.head;
clone的過程中創(chuàng)建了一個(gè)雙向循環(huán)鏈表,另外一個(gè)地方就是
Segment writableSegment(int minimumCapacity) {
if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE)
throw new IllegalArgumentException();
if (head == null) {
head = SegmentPool.take();
return head.next = head.pre = head;
}
//head 不為null 的情形
Segment tail = head.pre;
//如果tail會(huì)導(dǎo)致大于Segment的上限 或是owner為false 也就是不可寫
if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
tail = tail.push(SegmentPool.take()); //在tail的后面插入一個(gè)空的segment
}
return tail;
}
除了clone接口外,同時(shí)還有兩個(gè)接口BufferedSink,BufferedSource。Buffer實(shí)現(xiàn)了這兩個(gè)接口的所有方法,所有既然讀也有寫的方法,舉幾個(gè)例子
@Override
public Buffer writeShort(int s) throws IOException {
Segment tail = writableSegment(2);
byte[] data = tail.data;
int limit = tail.limit;
data[limit++] = (byte) ((s >>> 8) & 0xff);
data[limit++] = (byte) (s & 0xFF);
tail.limit = limit;
size += 2;
return this;
}
writeShort用來給Buffer中寫入一個(gè)short的數(shù)據(jù),首先通過writableSegment拿到一個(gè)能夠有2個(gè)字節(jié)空間的segment,tail中的data就是字節(jié)數(shù)組,limit則是數(shù)據(jù)的尾部索引,寫數(shù)據(jù)就是在尾部繼續(xù)往后寫,直接設(shè)置在data通過limit自增后的index,然后重置尾部索引,并且buffer的size大小加2。
@Override
public short readShort() throws IOException {
if (size < 2)
throw new IllegalArgumentException("size < 2");
Segment segment = head;
int pos = segment.pos;
int limit = segment.limit;
//如果short被segment分隔開 通過readByte來一個(gè)個(gè)字節(jié)讀
if (limit - pos < 2) {
int s = (readByte() & 0xFF) << 8 | (readByte() & 0xFF);
return (short) s;
}
byte[] data = segment.data; //與readByte類似 只不過一次讀兩個(gè)字節(jié)再組合起來
int s = (data[pos++] & 0xFF) << 8 | (data[pos++] & 0xFF); //pos自增2
size -= 2;
if (pos == limit) {
head = segment.pop();
SegmentPool.recycle(segment);
} else {
segment.pos = pos;
}
return (short) s;
}
讀的方法相對(duì)于寫的方法就復(fù)雜一些,因?yàn)閎uffer是分塊的,讀數(shù)據(jù)的過程就有可能是跨segment的,比如前面一個(gè)字節(jié),下一個(gè)segment一個(gè)字節(jié),這種情況就轉(zhuǎn)化為readbyte,讀兩個(gè)字節(jié)后合成一個(gè)short對(duì)象,對(duì)于連續(xù)的讀可以直接通過pos索引自增達(dá)到目的,讀完后Buffer的size減2。
并且會(huì)有當(dāng)前的segment會(huì)出現(xiàn)讀完后數(shù)據(jù)為null的情況,此時(shí)頭部索引pos和尾部索引limit就重合了,通過pop方法可以把這個(gè)segment分離出來,并且將下一個(gè)segment設(shè)置為Buffer的head,然后將分離出來的segment回收到對(duì)象池中。
鑒于篇幅原因就暫時(shí)只舉出Buffer中比較有代表性的讀寫方式,看完這兩個(gè)其他都是類似的。
后記
以上就是整個(gè)Okio核心實(shí)現(xiàn)的分析,篇幅比較長,能夠堅(jiān)持看到這里的都是值得敬佩的,Okio的整個(gè)源碼干貨滿滿,而且架構(gòu)清晰,如果有時(shí)間可以通讀一遍,更能理解上述文章中的分析。
最后總結(jié)一下Okio這個(gè)庫的精髓,第一就是快,Okio采取了空間換時(shí)間的方式比如Segment和ByteString之類的存在來讓IO操作盡可能不成為整個(gè)系統(tǒng)的瓶頸,雖然采取這種方式但是在內(nèi)存上也是極致的優(yōu)化,使用的片段共享以及整體的讀寫共享來加快大字節(jié)數(shù)組的讀寫,第二就是穩(wěn)定,Okio提供了超時(shí)機(jī)制,不僅在IO操作上加上超時(shí)的判定,包括close,flush之類的方法中都有超時(shí)機(jī)制,這讓上層不會(huì)錯(cuò)過一個(gè)可能導(dǎo)致系統(tǒng)崩潰的超時(shí)異常,第三就是方便,Sink,Source兩個(gè)包裝了寫和讀,區(qū)別于傳統(tǒng)的IO各種不同的輸入輸出流,這里只有一種而且支持socket,十分的方便。當(dāng)然Okio還有很多其他的好處,易于擴(kuò)展,代碼量小易于閱讀,我想這就是許多上層庫選擇Okio來作為IO操作的原因。
okio sample來源
https://github.com/ut2014/Okio_1.9