欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

源碼剖析Android中Okio的使用

 更新時(shí)間:2023年02月17日 10:53:14   作者:程序員小北  
這篇文章主要將從源碼出發(fā),帶大家剖析一下Android中Okio的具體使用,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以了解一下

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ù)寫入BufferSegment中,來看看這個(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ì)交給它來完成。而RealBufferedSourceRealBufferedSink更像是中間人,負(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)的prevnext引用指向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)的刪除操作類似,也是改變prevnext引用的指向來實(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ū)別是poslimit的值會(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中。需要注意的是,若sinklimit位置開始寫入數(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è)類提供了recycletake兩個(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ì)上takerecycle方法涉及單鏈表節(jié)點(diǎn)的刪除和添加操作,若需要Segment,則調(diào)用take。若要回收某個(gè)Segment,則調(diào)用recycle。

Buffer類的設(shè)計(jì)

Buffer類實(shí)現(xiàn)了BufferedSourceBufferedSink接口,最終數(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)開始寫。
  • sizeSegment鏈表中保存的字節(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ū)頭部SegmentbyteCount字節(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ū)頭部SegmentbyteCount字節(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)制下線功能

    kotlin實(shí)現(xiàn)強(qiáng)制下線功能

    這篇文章主要為大家詳細(xì)介紹了kotlin實(shí)現(xiàn)強(qiáng)制下線功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-06-06
  • Android5.0以上實(shí)現(xiàn)全透明的狀態(tài)欄方法(仿網(wǎng)易云界面)

    Android5.0以上實(shí)現(xiàn)全透明的狀態(tài)欄方法(仿網(wǎng)易云界面)

    下面小編就為大家分享一篇Android5.0以上實(shí)現(xiàn)全透明的狀態(tài)欄方法(仿網(wǎng)易云界面),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-01-01
  • Android動(dòng)態(tài)加載布局實(shí)現(xiàn)技巧介紹

    Android動(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)來的View
    2022-12-12
  • Android實(shí)現(xiàn)返回拍攝的圖片功能實(shí)例

    Android實(shí)現(xiàn)返回拍攝的圖片功能實(shí)例

    這篇文章主要介紹了Android實(shí)現(xiàn)返回拍攝的圖片功能,以實(shí)例形式較為詳細(xì)的分析了Android返回拍攝圖片功能的具體步驟與實(shí)現(xiàn)方法,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-07-07
  • Android實(shí)現(xiàn)button居中的方法

    Android實(shí)現(xiàn)button居中的方法

    這篇文章主要介紹了Android實(shí)現(xiàn)button居中的方法,涉及Android的XML布局技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-09-09
  • Android:Field can be converted to a local varible.的解決辦法

    Android:Field can be converted to a local varible.的解決辦法

    這篇文章主要介紹了Android:Field can be converted to a local varible.的解決辦法的相關(guān)資料,希望通過本文能幫助到大家,讓大家遇到這樣的問題輕松解決,需要的朋友可以參考下
    2017-10-10
  • 你必須掌握在Flutter中添加資源文件的方法

    你必須掌握在Flutter中添加資源文件的方法

    這篇文章主要介紹了你必須掌握在Flutter中添加資源文件的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-04-04
  • Android開發(fā)技巧之我的菜單我做主(自定義菜單)

    Android開發(fā)技巧之我的菜單我做主(自定義菜單)

    Android SDK本身提供了一種默認(rèn)創(chuàng)建菜單的機(jī)制,雖然功能上還不錯(cuò),但是界面的美觀度不是很理想,本結(jié)介紹一種實(shí)現(xiàn)方法:就是通過onKeyDown事件方法和PopupWindow實(shí)現(xiàn)自定義的菜單,感興趣的朋友可以了解下
    2013-01-01
  • Android通過繼承Binder類實(shí)現(xiàn)多進(jìn)程通信

    Android通過繼承Binder類實(shí)現(xiàn)多進(jìn)程通信

    本篇文章主要介紹了Android通過繼承Binder類實(shí)現(xiàn)多進(jìn)程通信,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。
    2017-03-03
  • android使用SoundPool播放音效的方法

    android使用SoundPool播放音效的方法

    本篇文章主要介紹了android使用SoundPool播放音效的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-11-11

最新評論