源碼剖析Android中Okio的使用
okio庫的類結(jié)構(gòu)
okio 主要的接口和類
okio接口和類的說明
名稱 | 類型 | 描述 |
---|---|---|
Source | 接口 | 定義了輸入流的幾個(gè)基本方法 |
BufferedSource | 接口 | 繼承Source接口,新增了一系列readXxx方法 |
RealBufferedSource | 類 | 實(shí)現(xiàn)了BufferedSource接口 |
Sink | 接口 | 定義了輸出流的幾個(gè)基本方法 |
BufferedSink | 接口 | 繼承Sink接口,新增了一系列writeXxx方法 |
RealBufferedSink | 類 | 實(shí)現(xiàn)了BufferedSink接口 |
Buffer | 類 | 同時(shí)實(shí)現(xiàn)了BufferedSource和BufferedSink接口。被RealBufferedSource和RealBufferedSink所持有,是讀取和寫入操作的真正實(shí)現(xiàn)類。 |
readXxx
系列方法是從緩沖區(qū)讀出數(shù)據(jù)的方法。writeXxx
系列方法是向緩沖區(qū)寫入數(shù)據(jù)的方法。
okio讀取文件
使用 okio 來讀取文件非常的簡單,只需要簡單的幾步。
- 調(diào)用
Okio.source
方法獲得Source
對象 - 調(diào)用
Okio.buffer
方法獲得BufferedSource
對象。因?yàn)?code>BufferedSource是個(gè)接口,它里面定義了一系列的readXxx
方法,可以用來方便的讀取輸入流的內(nèi)容。
public void readFile() { try { FileInputStream fis = new FileInputStream("test.txt"); okio.Source source = Okio.source(fis); BufferedSource bs = Okio.buffer(source); String res = bs.readUtf8(); System.out.println(res); } catch (Exception e){ e.printStackTrace(); } }
Okio.source 方法
Okio.source
重寫了read
方法,并返回一個(gè)Source
對象。所以當(dāng)我們調(diào)用**Source**
對象的**read(Buffer sink, long byteCount)**
方法時(shí),其實(shí)是在調(diào)用該處重寫的方法。read
方法會(huì)從輸入流進(jìn)行一次讀取操作,將數(shù)據(jù)讀取到尾部的Segment
中。
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 Segment tail = sink.writableSegment(1); // 從輸入流最多讀取 maxToCopy 個(gè)字節(jié) int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); // 從輸入流讀取數(shù)據(jù)到 Segment int bytesRead = in.read(tail.data, tail.limit, maxToCopy); // 到達(dá)輸入流尾部 if (bytesRead == -1) return -1; // 更新 tail 的 limit tail.limit += bytesRead; // 更新 sink 的 size 值 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 + ")"; } }; }
read 方法首先會(huì)調(diào)用timeout.throwIfReached()
,這個(gè)方法是Okio中的同步超時(shí)檢測。它的作用有兩個(gè),一是檢查當(dāng)前線程是否中斷,二是判斷即將開始的讀取操作是否在已經(jīng)到達(dá)了截止時(shí)間,以上有任何一個(gè)條件不滿足,將會(huì)拋出異常中斷此次操作。比如我們將上面讀取文件的代碼設(shè)置一下讀取操作需要在未來的1ms內(nèi)完成。這意味著接下來的readUtf8操作,必須要在未來的1ms內(nèi)完成,否則拋出異常。
public void readFile() { try { FileInputStream fis = new FileInputStream("test.txt"); okio.Source source = Okio.source(fis); BufferedSource bs = Okio.buffer(source); // 設(shè)置超時(shí)時(shí)間為 1ms source.timeout().deadline(1, TimeUnit.MILLISECONDS); String res = bs.readUtf8(); System.out.println(res); } catch (Exception e){ e.printStackTrace(); } }
上面代碼將會(huì)拋出如下異常。由于throwIfReached
是在每次讀取數(shù)據(jù)之前調(diào)用并且與數(shù)據(jù)讀取在同一個(gè)線程,所以如果讀取操作阻塞,則無法及時(shí)拋出異常。
java.io.InterruptedIOException: deadline reached at okio.Timeout.throwIfReached(Timeout.kt:102) at okio.InputStreamSource.read(JvmOkio.kt:87) at okio.Buffer.writeAll(Buffer.kt:1642) at okio.RealBufferedSource.readUtf8(RealBufferedSource.kt:297)
又或者在讀取操作之前中斷了線程,也會(huì)拋出同樣的異常,如下代碼。
public void readFile() { Thread thread = new Thread(){ @Override public void run() { try { FileInputStream fis = new FileInputStream("test.txt"); okio.Source source = Okio.source(fis); BufferedSource bs = Okio.buffer(source); // 中斷當(dāng)前線程 interrupt(); String res = bs.readUtf8(); System.out.println(res); } catch (Exception e){ e.printStackTrace(); } } }; thread.start(); try { thread.join(); } catch (Exception e) { e.printStackTrace(); } }
這里簡單介紹了Okio的同步超時(shí)機(jī)制,而異步超時(shí)機(jī)制,這里就不做介紹了。
read
方法接著會(huì)將數(shù)據(jù)讀取到雙鏈表最尾部的Segment中,關(guān)于Segment是啥,這里暫時(shí)理解成它是一個(gè)存放數(shù)據(jù)的容器就行了。后面會(huì)詳細(xì)介紹。
Okio.buffer 方法
Okio.buffer
方法的看起來就簡單多了,直接實(shí)例化了一個(gè)RealBufferedSource
對象返回。 RealBufferedSource
實(shí)現(xiàn)了BufferedSource
接口,所以會(huì)有一系列的readXxx
方法。注意此處傳入了**Source**
對象,所以在**RealBufferedSource**
中調(diào)用**source**
對象的**read**
方法,是在調(diào)用上面重寫過的**read**
方法!
public static BufferedSource buffer(Source source) { return new RealBufferedSource(source); }
readUtf8() 方法
RealBufferedSource實(shí)現(xiàn)了BufferedSource接口,所以調(diào)用readUtf8()方法來讀取字符串時(shí)候,其實(shí)調(diào)用的是RealBufferedSource的readUtf8()方法。下面是readUtf8()方法的源碼。
@Override public String readUtf8() throws IOException { buffer.writeAll(source); return buffer.readUtf8(); }
1.buffer.writeAll(source)
會(huì)將數(shù)據(jù)寫入Buffer
的Segment
中,來看看這個(gè)方法的實(shí)現(xiàn)。我們發(fā)現(xiàn),這里會(huì)循環(huán)的調(diào)用**source.read**
方法,上面我們說過,調(diào)用**source**
對象的**read**
方法,是在調(diào)用上面重寫過的**read**
方法!所以writeAll
方法的任務(wù)就是將所有的數(shù)據(jù)寫入到一個(gè)或多個(gè)Segment中(一個(gè)Segment的最大容量是8kb,如果數(shù)據(jù)量大,一個(gè)Segment可能讀取不了這么多)。
@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(this, Segment.SIZE)) != -1; ) { totalBytesRead += readCount; } return totalBytesRead; }
- 2.
buffer.readUtf8()
會(huì)將存儲(chǔ)在Segment
中的數(shù)據(jù)讀出,轉(zhuǎn)化為字符串。若一個(gè)Segment
的數(shù)據(jù)被讀完且它是非共享的,那么這個(gè)Segment
將會(huì)被回收。
總結(jié)
使用Okio來讀取輸入流的數(shù)據(jù),Okio首先會(huì)將所有的數(shù)據(jù)讀取到**Buffer**
類的一個(gè)或多個(gè)**Segment**
中,當(dāng)我們想要獲取這些數(shù)據(jù)的時(shí)候,再從**Segment**
中讀出來。Buffer
這個(gè)類是整個(gè)Okio框架的靈魂所在,它實(shí)現(xiàn)了BufferedSource, BufferedSink
接口,最終的讀寫操作都會(huì)交給它來完成。而RealBufferedSource
和RealBufferedSink
更像是中間人,負(fù)責(zé)把讀寫任務(wù)交給Buffer
。
有讀者到這就會(huì)問了,使用Okio來讀取數(shù)據(jù)并沒有看到明顯的優(yōu)勢,就是在API調(diào)用上精簡了一些。其實(shí)不然,Okio天然的設(shè)計(jì)了Segment
作為數(shù)據(jù)的緩沖區(qū)。同時(shí)Segment
是可以回收和復(fù)用的,這就減少了內(nèi)存的消耗,提高了內(nèi)存的利用率??紤]一種雙流操作,先讀取輸入流的內(nèi)容再寫入到輸出流。傳統(tǒng)的操作首先要將輸入流緩沖區(qū)的數(shù)據(jù)拷貝到一個(gè)字節(jié)數(shù)組中,然后再將字節(jié)數(shù)組的內(nèi)容拷貝到輸出流緩沖區(qū),這中間存在不同緩沖區(qū)的數(shù)據(jù)拷貝操作。而對于Okio來說,在不同緩沖區(qū)移動(dòng)數(shù)據(jù),只需移動(dòng)**Segment**
的引用,而非拷貝字節(jié)數(shù)組。
Okio雙流操作
Okio的優(yōu)點(diǎn)在于設(shè)計(jì)了Segment
,而雙流操作最能體現(xiàn)出這種天然的優(yōu)勢。下面代碼首先從test.txt
中讀取文件內(nèi)容,然后寫入test2.txt
中。
public void readAndWrite() { try { FileInputStream fis = new FileInputStream("test.txt"); Source source = Okio.source(fis); BufferedSource bSource = Okio.buffer(source); FileOutputStream fos = new FileOutputStream("test2.txt"); Sink sink = Okio.sink(fos); BufferedSink bSink = Okio.buffer(sink); while (!bSource.exhausted()){ // 不停的從 test.txt 中讀取數(shù)據(jù)并寫入到 test2.txt bSource.read(bSink.buffer(), 8*1024); // 將輸出流緩沖區(qū)的數(shù)據(jù)完全寫入到文件中 bSink.emit(); } bSource.close(); bSink.close(); } catch (Exception e) { e.printStackTrace(); } }
特別注意上面read
最終會(huì)調(diào)用到Buffer
類的write(Buffer source, long byteCount)
方法,這個(gè)方法可以說是Buffer
類最重要的方法。當(dāng)將一個(gè)Buffer
緩沖區(qū)的數(shù)據(jù)寫入到另一個(gè)Buffer
緩沖區(qū)**,并不會(huì)拷貝字節(jié),而是移動(dòng)****Segment**
**的引用。**除此之外,該方法還使用了Segment
的分割與合并操作,將內(nèi)存利用最大化。正如該方法的注釋所言“while balancing two conflicting goals: don't waste CPU and don't waste memory.”(同時(shí)平衡兩個(gè)相互沖突的目標(biāo):不浪費(fèi)CPU和不浪費(fèi)內(nèi)存。)
在下文分析Buffer
類的設(shè)計(jì)時(shí),會(huì)詳細(xì)介紹這個(gè)方法的源碼。
Segment類的設(shè)計(jì)
Okio將Java類庫中的輸入輸出流做了封裝,讓我們能很方便的使用這些API來完成文件的讀寫操作,這是Okio的一個(gè)優(yōu)點(diǎn)。但是僅僅從API封裝調(diào)用的角度,不能體現(xiàn)出一個(gè)框架的優(yōu)勢所在。Okio最精妙的地方是它設(shè)計(jì)了數(shù)據(jù)緩沖區(qū)**Segment**
。
Segment的特點(diǎn)
- Segment是一個(gè)循環(huán)雙鏈表,有前驅(qū)(prev)和后繼節(jié)點(diǎn)(next)
- 一個(gè)Segment可以存儲(chǔ)的最大數(shù)據(jù)量是8kb(8192=8*1024)
- Segment有兩種狀態(tài),分別是可共享和不可共享,由shared字段來區(qū)分(本質(zhì)上是data字節(jié)數(shù)組是否共享)。
- 一個(gè)Segment如果是共享的,那么只有data字節(jié)數(shù)組的宿主Segment能對它進(jìn)行修改。由owner字段來區(qū)分當(dāng)前Segment是不是data字節(jié)數(shù)組的宿主。
- 一個(gè)Segment如果是共享的,那么這個(gè)Segment將不可以被回收,data字節(jié)數(shù)組也不可以被非宿主的Segment所修改。
Segment成員變量
/** 一個(gè)Segment的容量 8kb */ static final int SIZE = 8192; /** data可共享閾值,小于這個(gè)值則使用 System.arraycopy 拷貝,不共享*/ static final int SHARE_MINIMUM = 1024; final byte[] data; /** 讀數(shù)據(jù)的起始位 */ int pos; /** 寫數(shù)據(jù)的起始位 */ int limit; /** data字節(jié)數(shù)組是否共享. */ boolean shared; /** 當(dāng)前Segment是否為data字節(jié)數(shù)組的宿主Segment,與shared互斥 */ boolean owner; /** 后繼節(jié)點(diǎn) */ Segment next; /** 前驅(qū)節(jié)點(diǎn) */ Segment prev;
Segment成員方法
Segment sharedCopy() Segment unsharedCopy() Segment pop() Segment push(Segment segment) Segment split(int byteCount) void compact() void writeTo(Segment sink, int byteCount)
sharedCopy 共享拷貝
sharedCopy
是共享拷貝的意思,該方法會(huì)將shared
字段改為true
,然后實(shí)例化一個(gè)新的Segment
返回。新的Segment
會(huì)與當(dāng)前Segment
共享data字節(jié)數(shù)組(本質(zhì)上是都持有data數(shù)組的引用),新返回的Segment
并不是data字節(jié)數(shù)組的宿主Segment
,所以它不能對data數(shù)組進(jìn)行修改操作。同樣,當(dāng)一個(gè)Segment
被標(biāo)記為共享狀態(tài)的時(shí)候,不能夠被回收。
final Segment sharedCopy() { shared = true; return new Segment(data, pos, limit, true, false); }
unsharedCopy 非共享拷貝
unsharedCopy
非共享拷貝,該方法對data字節(jié)數(shù)組進(jìn)行深拷貝,返回的Segment
完完全全是一個(gè)新的對象。
final Segment unsharedCopy() { return new Segment(data.clone(), pos, limit, false, true); }
pop 將當(dāng)前 Segment 從雙鏈表中移除
pop
方法可以將當(dāng)前的Segment從它所在的雙鏈表中移除,并返回它的后繼節(jié)點(diǎn)(下一個(gè)節(jié)點(diǎn))。若鏈表中只有一個(gè)節(jié)點(diǎn)(只有當(dāng)前節(jié)點(diǎn)),則將當(dāng)前節(jié)點(diǎn)移除后返回null。
public final @Nullable Segment pop() { Segment result = next != this ? next : null; prev.next = next; next.prev = prev; next = null; prev = null; return result; }
pop
方法涉及到循環(huán)雙鏈表刪除節(jié)點(diǎn)的操作,這里詳細(xì)介紹下。 當(dāng)鏈表中只有當(dāng)前Segment
,調(diào)用pop
方法后,結(jié)構(gòu)會(huì)發(fā)生如下變化,當(dāng)前Segment
不再會(huì)有指向它的引用,會(huì)在某個(gè)時(shí)刻被垃圾回收掉。
其實(shí)不論鏈表中有多少個(gè)節(jié)點(diǎn),要?jiǎng)h除哪一個(gè)節(jié)點(diǎn)。循環(huán)雙鏈表中刪除節(jié)點(diǎn)的操作都是一樣的,只需將當(dāng)前要?jiǎng)h除節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)的next引用指向到要?jiǎng)h除節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn),將當(dāng)前要?jiǎng)h除節(jié)點(diǎn)的后一個(gè)節(jié)點(diǎn)的pre引用指向到要?jiǎng)h除節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)。對應(yīng)如下兩行代碼。
prev.next = next; next.prev = prev;
然后將待刪除節(jié)點(diǎn)的prev
和next
引用指向null
,這樣需要?jiǎng)h除的節(jié)點(diǎn)就脫離了這個(gè)鏈表,等待垃圾回收。
next = null; prev = null;
push 將一個(gè) Segment 添加到當(dāng)前 Segment 后面
push
方法可以將一個(gè)Segment
添加到當(dāng)前Segment
的后面,與上述鏈表節(jié)點(diǎn)的刪除操作類似,也是改變prev
和next
引用的指向來實(shí)現(xiàn)的,這里就不再詳細(xì)說明了。
public final Segment push(Segment segment) { segment.prev = this; segment.next = next; next.prev = segment; next = segment; return segment; }
split 字節(jié)數(shù)組數(shù)據(jù)分割
split
方法可以將當(dāng)前Segment
分割成兩個(gè)Segment
(實(shí)際上是將data字節(jié)數(shù)組的數(shù)據(jù)分成兩部分)。傳入的byteCount
參數(shù)決定了分割后的第一個(gè)Segment
含有多少個(gè)字節(jié)的數(shù)據(jù)。第一個(gè)Segment
會(huì)有[pos+byteCount, limit)
區(qū)間的數(shù)據(jù),第二個(gè)Segment
含有[pos, pos+byteCount)
區(qū)間的數(shù)據(jù),都是左閉右開區(qū)間。
public final Segment split(int byteCount) { // byteCount 參數(shù)合法性校驗(yàn),若要分割的字節(jié)數(shù)量 <=0 或 > 已有的數(shù)據(jù)量,則拋出異常 if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException(); Segment prefix; // 從當(dāng)前Segment分割出一個(gè)新的Segment(prefix) //1. 若要分割的字節(jié)數(shù) >= SHARE_MINIMUM(1kb),則采用共享拷貝(拷貝引用)的方式 //2. 若分割的字節(jié)數(shù) < 1kb,則采用拷貝的方式(完全復(fù)制,新開辟內(nèi)存空間) if (byteCount >= SHARE_MINIMUM) { prefix = sharedCopy(); } else { prefix = SegmentPool.take(); System.arraycopy(data, pos, prefix.data, 0, byteCount); } // 更新剛分割出來的Segment(prefix)的limit值, [pos, limit = (pos+byteCount)) prefix.limit = prefix.pos + byteCount; // 更新當(dāng)前Segment的pos值, [pos = (pos+byteCount), limit) pos += byteCount; // 將新分割出來的Segment(prefix)添加到當(dāng)前Segment的后面 prev.push(prefix); // 返回新分割出來的 Segment(prefix) return prefix; }
假設(shè)當(dāng)前有一個(gè)Segment
存儲(chǔ)了2kb的數(shù)據(jù),現(xiàn)在要分割出512b的數(shù)據(jù)(byteCount = 512),使用split
方法分割的流程如下。
需要注意的是,若采用共享拷貝的方式,那當(dāng)前Segment
和分割出來的Segment
共享同一個(gè)data字節(jié)數(shù)組(data數(shù)組內(nèi)存空間一樣),區(qū)別是pos
和limit
的值會(huì)不同。若采用完全拷貝的方式,那么兩個(gè)Segment就是完全獨(dú)立的,即各自的data字節(jié)數(shù)組在不同的內(nèi)存空間,不共享。split
方法遵循了**"大塊數(shù)據(jù)移動(dòng)引用,小塊數(shù)據(jù)進(jìn)行拷貝"**的思想,平衡了CPU與內(nèi)存的消耗。
writeTo Segment之間字節(jié)數(shù)組數(shù)據(jù)的移動(dòng)
writeTo
方法可以將byteCount
個(gè)字節(jié)數(shù)據(jù)從當(dāng)前Segment
移動(dòng)到sink
中去。
public final void writeTo(Segment sink, int byteCount) { // sink 參數(shù)合法性校驗(yàn),若sink非data的宿主Segemnt,則拋出異常。 // 這說明獲得數(shù)據(jù)的Segment必須是data的宿主,只有宿主Segment才能對data進(jìn)行修改 if (!sink.owner) throw new IllegalArgumentException(); // 若 sink 從 limit 開始寫數(shù)據(jù),剩余的容量不足以容納 byteCount 個(gè)字節(jié) if (sink.limit + byteCount > SIZE) { // We can't fit byteCount bytes at the sink's current position. Shift sink first. if (sink.shared) throw new IllegalArgumentException(); // (byteCount > SIZE - (sink.limit-sink.pos)) // 即 sink 剩余的容量不能容納 byteCount 個(gè)字節(jié)數(shù)據(jù),拋出異常 if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException(); // 移動(dòng) sink 的數(shù)據(jù),從 pos = 0 開始 System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos); sink.limit -= sink.pos; sink.pos = 0; } // 拷貝數(shù)據(jù)到 sink System.arraycopy(data, pos, sink.data, sink.limit, byteCount); // 更新 sink 的 limit 值 sink.limit += byteCount; // 更新當(dāng)前 Segment 的 pos 值 pos += byteCount; }
從上面代碼可以看出,writeTo
方法可以將當(dāng)前Segment
的一部分?jǐn)?shù)據(jù)移動(dòng)到sink
中。需要注意的是,若sink
從limit
位置開始寫入數(shù)據(jù),sink
剩余的容量不足以容納byteCount
個(gè)字節(jié),那么首先會(huì)將sink
原有的數(shù)據(jù)移動(dòng)到數(shù)組pos=0
的位置,再從新的limit位置寫
。若足以容納,則從直接從最初的limit
位置開始寫。
compact 字節(jié)數(shù)組數(shù)據(jù)的合并
compact
方法可以將當(dāng)前Segment
與它的前驅(qū)Segment
合并成一個(gè)Segment
。
public final void compact() { // 若鏈表中只有一個(gè)Segment,無法合并。拋出異常 if (prev == this) throw new IllegalStateException(); // 若待合并的 prev 節(jié)點(diǎn)非宿主,無法進(jìn)行合并操作 if (!prev.owner) return; // Cannot compact: prev isn't writable. // 當(dāng)前 Segment 存儲(chǔ)的字節(jié)數(shù) int byteCount = limit - pos; // prev 剩余的容量,SIZE - (prev.limit- prev.pos) int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos); // 若 prev 剩余的容量不足以容納當(dāng)前 Segment 的數(shù)據(jù),無法合并 if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space. // 將當(dāng)前 Segment 的數(shù)據(jù)移動(dòng)到 prev writeTo(prev, byteCount); // 將當(dāng)前 Segment 從鏈表中移除 pop(); // 回收當(dāng)前的 Segment SegmentPool.recycle(this); }
Segment的回收與復(fù)用
前面我們多次提到,Okio為了節(jié)約內(nèi)存資源,Segment
可以回收和復(fù)用。當(dāng)一個(gè)Segment
中不再有數(shù)據(jù)的時(shí)候(數(shù)據(jù)被讀過或被寫入到輸出流),會(huì)被回收。而當(dāng)要使用Segment
來保存數(shù)據(jù)的時(shí)候,就可以從“池子”中取出一個(gè)Segment
來使用,而不是直接new。SegmentPool
這個(gè)類提供了recycle
和take
兩個(gè)方法,分別對應(yīng)于Segment
的回收與復(fù)用。在SegmentPool
中使用單鏈表結(jié)構(gòu)來保存已回收的Segment
。下面是該類的源碼。
final class SegmentPool { // 池子里最多有 8 個(gè) Segment static final long MAX_SIZE = 64 * 1024; // 64 KiB. // 單鏈表的頭結(jié)點(diǎn) static @Nullable Segment next; // 池子中所有Segment的字節(jié)總數(shù) static long byteCount; private SegmentPool() { } // Segment 復(fù)用,取單鏈表頭結(jié)點(diǎn) static Segment take() { synchronized (SegmentPool.class) { if (next != null) { 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. } // Segment 回收,將其放到單鏈表頭部 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.next = next; segment.pos = segment.limit = 0; next = segment; } } }
本質(zhì)上take
和recycle
方法涉及單鏈表節(jié)點(diǎn)的刪除和添加操作,若需要Segment
,則調(diào)用take
。若要回收某個(gè)Segment
,則調(diào)用recycle
。
Buffer類的設(shè)計(jì)
Buffer
類實(shí)現(xiàn)了BufferedSource
和BufferedSink
接口,最終數(shù)據(jù)的讀取和寫入操作都會(huì)交給這個(gè)類。
Buffer成員變量
head
是循環(huán)雙鏈表的頭結(jié)點(diǎn),每次讀數(shù)據(jù)的時(shí)候,從這個(gè)頭結(jié)點(diǎn)開始讀。因?yàn)槭茄h(huán)雙鏈表,尾結(jié)點(diǎn)就是head.prev
,每次寫數(shù)據(jù),從尾結(jié)點(diǎn)開始寫。size
是Segment
鏈表中保存的字節(jié)總數(shù)。當(dāng)size==0
時(shí),表明該Buffer
緩沖區(qū)已經(jīng)沒有數(shù)據(jù)。
@Nullable Segment head; long size;
Buffer成員方法
write 緩沖區(qū)之間的數(shù)據(jù)移動(dòng)
回顧下Okio的雙流操作。在兩個(gè)緩沖區(qū)之間移動(dòng)數(shù)據(jù),是不會(huì)拷貝字節(jié)的,而是移動(dòng)Segment
的引用。write(Buffer source, long byteCount)
方法可以將source
緩沖區(qū)byteCount
個(gè)字節(jié)移動(dòng)到當(dāng)前緩沖區(qū)。現(xiàn)在詳細(xì)分析下write(Buffer source, long byteCount)
方法的源碼,它到底是如果做到的。
@Override public void write(Buffer source, long byteCount) { // 參數(shù)合法性校驗(yàn) if (source == null) throw new IllegalArgumentException("source == null"); if (source == this) throw new IllegalArgumentException("source == this"); checkOffsetAndCount(source.size, 0, byteCount); // 當(dāng) byteCount > 0 while (byteCount > 0) { // Is a prefix of the source's head segment all that we need to move? // 若 byteCount 個(gè)字節(jié)數(shù)據(jù)存在于 source 的頭部 Segment if (byteCount < (source.head.limit - source.head.pos)) { Segment tail = head != null ? head.prev : null; // 若當(dāng)前緩沖區(qū)尾部的 Segment 不為 null && 是宿主 Segment && 能容納 byteCount 個(gè)字節(jié) if (tail != null && tail.owner && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) { // Our existing segments are sufficient. Move bytes from source's head to our tail. // 直接將 source緩沖區(qū) 頭部 Segment 的數(shù)據(jù)移動(dòng)到當(dāng)前緩沖區(qū)尾部的 Segment source.head.writeTo(tail, (int) byteCount); // 更新 source 緩沖區(qū)的 size source.size -= byteCount; // 更新當(dāng)前緩沖區(qū)的 size size += byteCount; // 結(jié)束程序 return; } else { // We're going to need another segment. Split the source's head // segment in two, then move the first of those two to this buffer. // 若當(dāng)前緩沖區(qū)尾部的 Segment 為 null || 無法容納 byteCount 個(gè)字節(jié) // 將 source 緩沖區(qū)頭部的 Segment 的 byteCount 個(gè)字節(jié)分割出來 source.head = source.head.split((int) byteCount); } } // Remove the source's head segment and append it to our tail. // source 緩沖區(qū)頭部節(jié)點(diǎn) Segment segmentToMove = source.head; // source 緩沖區(qū)頭部節(jié)點(diǎn)的字節(jié)數(shù) long movedByteCount = segmentToMove.limit - segmentToMove.pos; // 將 source 緩沖區(qū)頭部節(jié)點(diǎn)從雙鏈表中移除,并返回它的下一個(gè)節(jié)點(diǎn) source.head = segmentToMove.pop(); // 若當(dāng)前緩沖區(qū)頭部節(jié)點(diǎn)為 null if (head == null) { head = segmentToMove; head.next = head.prev = head; } else { // 若當(dāng)前緩沖區(qū)頭部節(jié)點(diǎn)不為 null,將 source 緩沖區(qū)頭部節(jié)點(diǎn)添加到當(dāng)前緩沖區(qū)尾部 Segment tail = head.prev; tail = tail.push(segmentToMove); // 嘗試合并 tail.compact(); } // 更新 source 緩沖區(qū)的 size source.size -= movedByteCount; // 更新當(dāng)前緩沖區(qū)的 size size += movedByteCount; // 更新 byteCount byteCount -= movedByteCount; } }
從上面源碼可以看出,將數(shù)據(jù)從一個(gè)緩沖區(qū)移動(dòng)到另一個(gè)緩沖區(qū),根據(jù)不同的情況會(huì)采取不同的移動(dòng)策略。
若要移動(dòng)的byteCount
個(gè)字節(jié)存在于源緩沖區(qū)的頭部Segment
- 若目的緩沖區(qū)的尾部
Segment
能容納byteCount
個(gè)字節(jié),則直接將源緩沖區(qū)頭部Segment
的byteCount
字節(jié)移動(dòng)到目的緩沖區(qū)的尾部Segment
,程序就結(jié)束了。這里采用的策略是拷貝字節(jié),而非移動(dòng)引用。 - 若目的緩沖區(qū)的尾部
Segment
不能容納byteCount
個(gè)字節(jié),則將源緩沖區(qū)頭部 Segment 的 byteCount 個(gè)字節(jié)分割(分割操作使用共享拷貝或者非共享拷貝)出來,生成一個(gè)新的Segment
將其添加到目的緩沖區(qū)的尾部,之后嘗試Segment
合并操作。
上述代碼進(jìn)行第一次循環(huán)運(yùn)行后,可能已經(jīng)結(jié)束,可能進(jìn)行下一次循環(huán)。簡單來說,上述代碼并不復(fù)雜。有兩種數(shù)據(jù)移動(dòng)的策略。
- 直接將源緩沖區(qū)頭部
Segment
的byteCount
字節(jié)移動(dòng)到目的緩沖區(qū)的尾部Segment
。這種情況發(fā)生一次程序就結(jié)束了。這里是在拷貝字節(jié)數(shù)組。 - 將源緩沖區(qū)頭部的
Segment
添加到目的緩沖的尾部。因?yàn)樵谘h(huán)內(nèi),這種情況可能進(jìn)行多次。這里是在移動(dòng)**Segment**
的引用。
經(jīng)過上述源碼的講解,想必大家對Okio有了更進(jìn)一步的認(rèn)識。Okio中最精妙的設(shè)計(jì)當(dāng)Segment
所屬。在緩沖區(qū)之間移動(dòng)大塊數(shù)據(jù),是在移動(dòng)**Segment**
的引用。而移動(dòng)小塊數(shù)據(jù),是在拷貝字節(jié)。“不浪費(fèi)CPU和不浪費(fèi)內(nèi)存”。
到此這篇關(guān)于源碼剖析Android中Okio的使用的文章就介紹到這了,更多相關(guān)Android Okio內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
kotlin實(shí)現(xiàn)強(qiáng)制下線功能
這篇文章主要為大家詳細(xì)介紹了kotlin實(shí)現(xiàn)強(qiáng)制下線功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06Android5.0以上實(shí)現(xiàn)全透明的狀態(tài)欄方法(仿網(wǎng)易云界面)
下面小編就為大家分享一篇Android5.0以上實(shí)現(xiàn)全透明的狀態(tài)欄方法(仿網(wǎng)易云界面),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01Android動(dòng)態(tài)加載布局實(shí)現(xiàn)技巧介紹
通過使用LayoutInflater 每次點(diǎn)擊按鈕時(shí)候去讀取布局文件,然后找到布局文件里面的各個(gè)VIEW 操作完VIEW 后加載進(jìn)我們setContentView 方面里面的要放的布局文件里面,每次動(dòng)態(tài)加載文件必需調(diào)用 removeAllViews方法,清除之前的加載進(jìn)來的View2022-12-12Android實(shí)現(xiàn)返回拍攝的圖片功能實(shí)例
這篇文章主要介紹了Android實(shí)現(xiàn)返回拍攝的圖片功能,以實(shí)例形式較為詳細(xì)的分析了Android返回拍攝圖片功能的具體步驟與實(shí)現(xiàn)方法,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07Android實(shí)現(xiàn)button居中的方法
這篇文章主要介紹了Android實(shí)現(xiàn)button居中的方法,涉及Android的XML布局技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09Android:Field can be converted to a local varible.的解決辦法
這篇文章主要介紹了Android:Field can be converted to a local varible.的解決辦法的相關(guān)資料,希望通過本文能幫助到大家,讓大家遇到這樣的問題輕松解決,需要的朋友可以參考下2017-10-10Android開發(fā)技巧之我的菜單我做主(自定義菜單)
Android SDK本身提供了一種默認(rèn)創(chuàng)建菜單的機(jī)制,雖然功能上還不錯(cuò),但是界面的美觀度不是很理想,本結(jié)介紹一種實(shí)現(xiàn)方法:就是通過onKeyDown事件方法和PopupWindow實(shí)現(xiàn)自定義的菜單,感興趣的朋友可以了解下2013-01-01Android通過繼承Binder類實(shí)現(xiàn)多進(jìn)程通信
本篇文章主要介紹了Android通過繼承Binder類實(shí)現(xiàn)多進(jìn)程通信,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03