作者簡(jiǎn)介:ASCE1885, 《Android 高級(jí)進(jìn)階》作者榜苫。
本文由于潛在的商業(yè)目的铝宵,未經(jīng)授權(quán)不開(kāi)放全文轉(zhuǎn)載許可谦纱,謝謝看成!
本文分析的源碼版本已經(jīng) fork 到我的 Github。
本文我們重點(diǎn)來(lái)介紹 okio 中的 segment 和 SegmentPool跨嘉,segment 是一個(gè)底層的數(shù)據(jù)結(jié)構(gòu)川慌,本質(zhì)上是一個(gè)字節(jié)數(shù)組,同時(shí)由于它被用于鏈表或者循環(huán)鏈表中,因此梦重,segment 中也定義了鏈表的相關(guān)操作兑燥。而 SegmentPool 中保存的是未使用的 segment 的集合,需要使用 segment 時(shí)從這個(gè)池子中取用琴拧,用完后及時(shí)釋放回這個(gè)池子降瞳,兩者的關(guān)系如下圖所示:
segment
接下來(lái)我們首先介紹 segment,從上圖可以看到蚓胸,segment 有兩個(gè)核心概念:
- 讀取指針 pos:segment 數(shù)據(jù)讀取時(shí)下一個(gè)要讀取的字節(jié)位置
- 寫入指針 limit:segment 數(shù)據(jù)寫入時(shí)將會(huì)寫入的第一個(gè)字節(jié)位置
segment 根據(jù)其中的字節(jié)數(shù)據(jù) final byte[] data
是否能夠被多個(gè) segment 共用挣饥,可以分為共享 segment 和非共享 segment,通過(guò)變量 boolean shared
來(lái)標(biāo)識(shí)沛膳;同時(shí)同一份數(shù)據(jù)多個(gè) segment 在使用時(shí)扔枫,通過(guò)變量 boolean owner
來(lái)區(qū)分誰(shuí)是這份數(shù)據(jù)的擁有者,一份數(shù)據(jù)只有一個(gè)擁有者于置,其他的都是使用者茧吊,一般來(lái)說(shuō)贞岭,數(shù)據(jù)擁有者對(duì)共享的 final byte[] data
數(shù)組擁有讀寫權(quán)限八毯,數(shù)據(jù)使用者對(duì)其只有只讀權(quán)限。
從 Segment 類的構(gòu)造方法也可以看出它是否共享 segment:
/** segment 中字節(jié)數(shù)組的大小 */
static final int SIZE = 8192;
/** segment 拆分時(shí)瞄桨,當(dāng)數(shù)據(jù)量少于這個(gè)值時(shí)不做數(shù)據(jù)共享 */
static final int SHARE_MINIMUM = 1024;
/** segment 中實(shí)際存放數(shù)據(jù)的地方 */
final byte[] data;
/** 數(shù)據(jù)讀取指針 */
int pos;
/** 數(shù)據(jù)寫入指針 */
int limit;
/** 為 true 表示有其他 segment 和當(dāng)前 segment 共享 data 數(shù)組 */
boolean shared;
/** 為 true 時(shí)表示當(dāng)前 segment 擁有 data 數(shù)組话速,并能夠進(jìn)行數(shù)據(jù)的寫入 */
boolean owner;
/** 非共享 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;
}
鏈表操作
當(dāng) segment 被用于鏈表或者循環(huán)鏈表時(shí),就會(huì)存在指向鏈表前面一個(gè) segment 的指針 prev
芯侥,或者指向后面一個(gè) segment 的指針 next
泊交,同時(shí)存在鏈表元素的添加和刪除,熟悉數(shù)據(jù)結(jié)構(gòu)的同學(xué)應(yīng)該知道柱查,這是典型的鏈表操作廓俭,鏈表元素的添加和刪除基本上就是指針的指向操作。
雙向鏈表在某個(gè)元素后面插入一個(gè)新元素的步驟如下:
- 將新元素的前向指針 prev 指向當(dāng)前元素
- 將新元素的后向指針 next 指向當(dāng)前元素的后面一個(gè)元素唉工,也就是當(dāng)前元素的 next 指針
- 當(dāng)前元素的前向指針改為指向新元素
- 當(dāng)前元素下一個(gè)元素的前向指針改為指向新元素
代碼如下所示:
/** 鏈表或者雙向循環(huán)鏈表中當(dāng)前元素的后面一個(gè)元素指針 */
Segment next;
/** 雙向循環(huán)鏈表中當(dāng)前元素的前一個(gè)元素指針 */
Segment prev;
/** 在當(dāng)前 segment 后面添加一個(gè)新的 segment */
public Segment push(Segment segment) {
segment.prev = this;
segment.next = next;
next.prev = segment;
next = segment;
return segment;
}
雙向鏈表刪除當(dāng)前元素并返回當(dāng)前元素的后面一個(gè)元素的步驟如下:
- 獲取當(dāng)前元素的后面一個(gè)元素(緩存起來(lái))
- 當(dāng)前元素的前一個(gè)元素的后向指針改為指向當(dāng)前元素的后一個(gè)元素
- 當(dāng)前元素的后一個(gè)元素的前向指針改為指向當(dāng)前元素的前一個(gè)元素
- 當(dāng)前元素的前向指針和后向指針都設(shè)置為空研乒,從而從鏈表中刪除
代碼如下所示:
/** 雙向鏈表刪除當(dāng)前元素并返回當(dāng)前元素的后面一個(gè)元素 */
public @Nullable Segment pop() {
Segment result = next != this ? next : null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
return result;
}
相信工科相關(guān)專業(yè)的同學(xué)應(yīng)該都學(xué)習(xí)過(guò)嚴(yán)蔚敏的那本經(jīng)典的數(shù)據(jù)結(jié)構(gòu)教程,印象不深的可以回味一下淋硝。
segment 的拆分
為了高效的利用內(nèi)存和方便 Buffer 的操作雹熬,segment 支持拆分和合并,拆分指的是將一個(gè) segment 中從讀取指針 pos 開(kāi)始到寫入指針 limit 之間的數(shù)據(jù)拆分到兩個(gè) segment 中谣膳。使用者需要指定一個(gè)字節(jié)數(shù) byteCount 用來(lái)分割這部分?jǐn)?shù)據(jù)竿报,[pos ~ pos+byteCount) 之間的數(shù)據(jù)拆分到新的 segment 中,[pos+byteCount ~ limit) 之間的數(shù)據(jù)保留在原來(lái)的 segment 中继谚。在鏈表的視角看烈菌,新拆分出來(lái)的 segment 位于原來(lái) segment 的前面。
在拆分的過(guò)程中,涉及到新 segment 的生成芽世,為了獲得高性能侨嘀,有如下兩個(gè)理念截然相反的問(wèn)題需要關(guān)注和平衡:
- 盡可能避免數(shù)據(jù)拷貝,這個(gè)可以通過(guò)前面介紹過(guò)的共享 segment 來(lái)實(shí)現(xiàn)
- 當(dāng)拷貝數(shù)據(jù)量很小時(shí)捂襟,避免使用共享 segment咬腕,因?yàn)楣蚕聿糠謹(jǐn)?shù)據(jù)是只讀的,而且可能會(huì)導(dǎo)致鏈表中存在很多數(shù)據(jù)量很小的 segment葬荷,影響性能
因此涨共,在拆分時(shí),只有當(dāng)數(shù)據(jù)量 byteCount 不小于 segment 大小的八分之一(即 1024 字節(jié))時(shí)宠漩,才會(huì)使用共享 segment举反,否則使用非共享 segment,并通過(guò) System.arraycopy
進(jìn)行數(shù)據(jù)的拷貝扒吁。然后修改新產(chǎn)生的 segment 的指針指向并將其插入鏈表中火鼻,代碼如下所示:
public Segment split(int byteCount) {
// 參數(shù)范圍校驗(yàn)
if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
// 拆分出來(lái)的新 segment
Segment prefix;
if (byteCount >= SHARE_MINIMUM) {
// 共享 segment
prefix = new Segment(this);
} else {
// 非共享 segment
prefix = SegmentPool.take();
System.arraycopy(data, pos, prefix.data, 0, byteCount);
}
prefix.limit = prefix.pos + byteCount;
pos += byteCount;
prev.push(prefix);
return prefix;
}
上面代碼中如果拆分出來(lái)的新 segment 走的是共享 segment 分支代碼,那么拆分后新的 segment 和原來(lái)的 segment 在數(shù)據(jù)結(jié)構(gòu)上是共享的雕崩,也就是兩者的數(shù)據(jù)都存放在同一個(gè) final byte[] data
中魁索,兩者不同的只是讀取指針 pos 和寫入指針 limit 的位置,如下圖所示: