Okio之Segment和SegmentPool

Segment

官方解釋

  • Segment 是 buffer 切割后的組成部分.
  • 每個 buffer 中的 Segment 都是循環(huán)鏈表中的節(jié)點,持有上一個 Segment 和下一個 Segment 的引用.
  • 每個緩存池中的 Segment 都是單鏈表中的節(jié)點.
  • Segment 底層數組可以被 buffer 和字符串共享.當一個 Segment 是被共享狀態(tài)時不可以被回收,其字節(jié)數據只能讀不可寫.
    • 唯一例外的是當 owner 為 true 時,數據區(qū)間為 [limit,SIZE) 中可以做寫操作.
  • 每個數組對應一個 Segment 作為持有者.
  • Positions, limits, prev, next 不可以被共享.

成員變量

// Segment.java

// segment 數據字節(jié)數最大值為 8kb
static final int SIZE = 8192;

/* SHARE_MINIMUM 是調用 split() 時根據操作字節(jié)大小(byteCount)判斷使用共享 segment 實現還是
   使用數據復制實現的標準 */
static final int SHARE_MINIMUM = 1024;

// 存放數據的字節(jié)數組
final byte[] data;

// 下一個可讀字節(jié)的下標
int pos;

// 第一個可寫字節(jié)的下標,即最后一個可讀數據下標為 limit-1
int limit;

// 是否與其他 segment 持有同一個數組 data 對象
boolean shared;

// 是否為 data 的所有者
//  true  對 data 有讀寫的權限,可寫的范圍與 shared 有關
//  false 只讀
boolean owner;

// 下一個 segment
Segment next;

// 上一個 segment
Segment prev;
  • 字節(jié)數組中有兩個特殊區(qū)域,分別是已讀區(qū)域 [pos, limit-1] 和可寫區(qū)域 [limit, SIZE)
  • shared owner 共同作用限制了 Segment 的讀寫權限以及可寫的范圍,下文閱讀 writeTo() 會介紹他們的作用.

構造方法

// Segment.java
Segment() {
  this.data = new byte[SIZE];
  this.owner = true;
  this.shared = false;
}

Segment(byte[] data, int pos, int limit, boolean shared, boolean owner) {
  this.data = data;
  this.pos = pos;
  this.limit = limit;
  this.shared = shared;
  this.owner = owner;
}
  • 無參構造方法構造默認的 Segment ,data 大小是默認 8k, onwer 為 true, shared 為 false,此時 Segment 沒有數據.

  • 有參構造可以構造自定義的 Segment,并設置 data pos limit shared owner 的值.


sharedCopy 和 unsharedCopy

// Segment.java

/**
 * 返回一個新的 segment 并與當前 segment 使用同一個 data 引用,相當于淺克隆
 */
Segment sharedCopy() {
  // 設置當前 segment 的 shared 為 true 可以防止 segment 被回收
  shared = true;
  return new Segment(data, pos, limit, true, false);
}

/** 
 * 返回一個新的 segment , data 是 segment.data 深克隆得到的對象 
 */
Segment unsharedCopy() {
  return new Segment(data.clone(), pos, limit, false, true);
}

兩個方法都是復制當前 Segment 且當前 Segment 和復制品的 pos 和 limit 值相同,兩個方法區(qū)別:

  • sharedCopy() 被調用時,當前 Segment 和復制得到的 Segment 會持有相同的數組對象 data ,所以兩個 Segment 的 shared 都要設置為 true 且復制的 Segment 的 owner 值為 false.
  • unsharedCopy() 被調用時,復制的 Segment 持有的是當前 Segment 數組 data 的深克隆得到的對象,所以他是克隆對象 data 的持有者, owner 為 true 且 shared 為 false.

由此可見只有最初持有 data 的 Segment 是數組的持有者 owner 為 true,其他調用 sharedCopy 復制的 Segment 都為 false.


push

// Segment.java
public Segment push(Segment segment) {
  segment.prev = this;
  segment.next = next;
  next.prev = segment;
  next = segment;
  return segment;
}

在當前 Segment 與上一個節(jié)點之間插入一個 Segment 并返回被插入的 Segment.


pop

public @Nullable Segment pop() {
  /* 如果當前 segment 下一個節(jié)點就是指向它自己,那么鏈表只有一個 segment,result 為 null,
     且下面兩行代碼的執(zhí)行毫無意義. */
  Segment result = next != this ? next : null;
  prev.next = next;
  next.prev = prev;
  next = null;
  prev = null;
  return result;
}

把當前 Segment 從循環(huán)鏈表中移除并返回,如果當前 Segment 本身就不在循環(huán)鏈表內就返回 null.


writeTo

// Segment.java
public void writeTo(Segment sink, int byteCount) {
  // 1
  if (!sink.owner) throw new IllegalArgumentException();
  // 2
  if (sink.limit + byteCount > SIZE) {
    // 2.1
    if (sink.shared) throw new IllegalArgumentException();
    // 2.2
    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 數據并寫入目標 Segment (sink) 中.

  1. 如果 sink.owner 為 false 即 sink 的數據只讀不寫,直接拋異常.

當前數組 data 可以分為三個部分 [0, pos) [pos, limit) [limit, SIZE) ,第一個區(qū)域在非共享狀態(tài)下是可回收區(qū)域,第二第三個就是之前說的可讀和可寫區(qū)域.

  1. 首先判斷 sink 的可寫區(qū)域是否足夠:
  • true 如果足夠直接從 sink 數組下標 limit 開始寫入數據.
  • false 如果不足夠可以通過移動數組中的可讀數據到 [0,limit-pos) ,把第一個區(qū)域的空間回收到可寫區(qū)域中,但實現該方法需要考慮 sink.shared 的值:
    • true sink 是共享狀態(tài)(2.1)數據是不可以移動的只能拋異常.
    • false sink 不是共享狀態(tài)數組數據可移動,但移動之前還需計算判斷移動后的可寫區(qū)域大小是否足夠(2.2):
      • false 移動之后還是不夠空間,拋異常.
      • true 移動后有足夠空間,通過數組復制方式往 sink 中寫入數據.

數據通過調用 System.arraycopy 寫進目標 Segment 后還需要設置當前 pos 和目標 Segment 的 limit.

該方法還表明了 owner 與 shared 之間的聯系:

  • owner = true && shared = true : 數組 [0, limit-1] 范圍內只讀不可寫,可寫范圍 [limit, SIZE] ,寫大小為 SIZE - limit.
  • owner = true && shared = false : 整個數組 data 都是可讀可寫的,可以把可讀數據在數組中隨意移動,可寫大小為 SIZE - (limit - pos).
  • owner = false 時 shared 只能是 true, Segment 持有的數據不可以做任何修改,只讀不寫.

split

// Segment.java
public Segment split(int byteCount) {
  // byteCount 不可以 <= 0 || byteCount 必須大于有效數據的大小,不然沒必要拆分
  if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
  Segment prefix;

  // 兩個性能指標:
  // - 避免復制操作.可以使用共享 segment 達到目的.
  // - 避免共享數據量小的 segment,這樣會在鏈表中出現一串數據量小的 segment 且他們都是只讀,會影響性能.
  // 為了得到平衡,只會在復制操作代價足夠大的時候才使用共享 segment
  if (byteCount >= SHARE_MINIMUM) {
    prefix = sharedCopy();
  } else {
    prefix = SegmentPool.take();
    System.arraycopy(data, pos, prefix.data, 0, byteCount);
  }

  prefix.limit = prefix.pos + byteCount;
  pos += byteCount;
  prev.push(prefix);
  return prefix;
}

split() 以 byteCount 為數據大小分界線,把當前 Segment 一分為二.

從數據內容角度看是把下標區(qū)間 [pos, limit) 分為 [pos, pos+byteCount) 和 [pos+byteCount, limit).

分割的實現有兩種方法:

  • 調用 sharedCopy 構建新的 Segment 且兩個 Segment 共享同一個數組 data 對象,然后設置兩個 Segment 的 pos 和 limit.
  • 從緩存池中獲取 Segment 并用 System.arraycopy 復制數據,然后設置兩個 Segment 的 pos 和 limit.

優(yōu)點:

  • share 方案可以減少 System.arraycopy 的調用提高了性能,共享 Segment 持有同一個數組 data 對象減少內存消耗.

缺點:

  • share 方案中的 Segment 且非數組持有者都只是可讀不可寫的,即使是數組持有者可寫范圍也受到限制,當循環(huán)鏈表中存在大量共享狀態(tài)且數據量小的 Segment 的時候,這些 Segment 對象會占用過多內存資源.
  • 數組復制方案涉及到底層方法,占用 CPU 資源,操作的字節(jié)數越大時性能損耗越明顯.

所以 Segment 規(guī)定當操作數據大小小于 1k 時用數據復制方案,超過 1k 用共享方案.


compact

// Segment.java
public void compact() {
  if (prev == this) throw new IllegalStateException();
  // 當 prev 是只讀的時候不可以合并
  if (!prev.owner) return; 
  // 操作字節(jié)數就是當前 segment 的數據大小
  int byteCount = limit - pos;
  // 根據是否共享狀態(tài)計算 prev 的可寫范圍大小:
  //    true  共享狀態(tài)可寫范圍是 [limit, SIZE),
  //    false 非共享可寫范圍是 [0, pos) + [limit, SIZE)
  int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
  // 如果 prev 可寫大小小于當前 segment 數據大小,就不可以合并
  if (byteCount > availableByteCount) return; 
  // 把當前 segment 數據寫進 prev 中
  writeTo(prev, byteCount);
  // 把當前 segment 從循環(huán)鏈表中移除
  pop();
  // 回收當前 segment
  SegmentPool.recycle(this);
}

條件允許情況下合并當前 Segment 數據到上一個 Segment 中,可以減少循環(huán)鏈表的節(jié)點數且盡可能地保證所有節(jié)點的數據占用率在 50% 以上.

  • 當前 Segment 的上一個 Segment 最大可寫大小 >= 當前 Segment 數據大小的時候,合并這兩個 Segment 中的數據到上一個 Segment 中并把當前 Segment 從循環(huán)鏈表中移除然后添加到緩存池.
  • 通常是由鏈表中的尾結點 tail 調用該方法.

SegmentPool

// SegmentPool.java
/**
 * 無用 Segment 的集合,防止被 GC 和零填充
 * 緩存池是靜態(tài)單例的保證了線程安全
 */
final class SegmentPool {
  // 緩存池最大容量 64kb
  static final long MAX_SIZE = 64 * 1024; 

  // 指向單鏈表中下一個節(jié)點,就是單鏈表的頭結點
  static @Nullable Segment next;

  // 記錄緩存池中所有 Segment 數據大小之和
  static long byteCount;

  private SegmentPool() {
  }

  // 從緩存池中獲取一個 Segment
  static Segment take() {
    synchronized (SegmentPool.class) {
      if (next != null) {
        Segment result = next;
        next = result.next;
        result.next = null;
        // 減去一個 Segment 容量
        byteCount -= Segment.SIZE;
        return result;
      }
    }
    return new Segment(); 
  }

  // 回收一個 Segment
  static void recycle(Segment segment) {
    // segment 必須不存在上一個節(jié)點和下一個節(jié)點的引用,否則報錯
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    // 如果該 segment 是被共享狀態(tài),不可以被回收
    if (segment.shared) return; 
    synchronized (SegmentPool.class) {
      // 緩存池已滿,return
      if (byteCount + Segment.SIZE > MAX_SIZE) return;
      // byteCount 添加一個 segment 的容量
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
}
  • SegmentPool 通過一個單鏈表實現,緩存最大容量是 64kb 個(有點多).
  • SegmentPool 不會回收共享狀態(tài)的 Segment.
  • SegmentPool 只回收指向上一個節(jié)點和下一個節(jié)點都為 null 的 Segment.
  • SegmentPool 中獲取的 Segment 可能保留著上次使用時的數據.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 簡介 先看看源碼中該類的簡介: 大概意思是:1.緩沖區(qū)的組成單位結構 2.每一個Segment是一個雙向循環(huán)鏈表,...
    OkCoco閱讀 1,116評論 0 1
  • 自從Google官方將OkHttp作為底層的網絡請求之后,作為OkHttp底層IO操作的Okio也是走進開發(fā)者的視...
    sheepm閱讀 11,673評論 13 75
  • 一、溫故而知新 1. 內存不夠怎么辦 內存簡單分配策略的問題地址空間不隔離內存使用效率低程序運行的地址不確定 關于...
    SeanCST閱讀 8,125評論 0 27
  • square在開源社區(qū)的貢獻是卓越的,這里是square在Android領域貢獻的開源項目。 1. okio概念 ...
    王英豪閱讀 1,350評論 0 2
  • 簡介 okio 補充了 java.io 和 java.nio 的內容,使得數據訪問、存儲和處理更加便捷。本文將簡單...
    MrFengZH閱讀 3,010評論 0 1

友情鏈接更多精彩內容