# IO
程序與運行時數據在內存中駐留,由CPU負責執行,涉及到數據交換的地方,如磁盤、網絡等,就需要IO接口。IO包括輸入流(Input Stream)和輸出流(Output Stream),來表達數據從一端到另一端的過程。
Input Stream和Output Stream可以以內存為參照標準,加載到內存的是輸入流,從內存輸出到別的地方的是輸出流。比如File存于磁盤中,程序獲取File數據用來進行其它操作,這是將數據讀入內存中的過程,所以為輸入流。反之,程序將各種信息保存入File中,是將數據讀出內存的過程,所以為輸出流。
再比如,網絡操作,請求從客戶端來到服務端,也就是數據從客戶端到達了服務端,那么對于客戶端,是輸出流,對服務端,是輸入流,響應則相反。如圖:

## IO原理

對于操作系統而言,JVM只是一個用戶進程,處于用戶態空間中,處于用戶態空間的進程是不能操作底層硬件的(如磁盤、網卡)。
用戶態的進程要訪問磁盤和網卡,必須通過系統調用,從用戶態切換到內核態才行。因此Java IO讀取數據時,用戶進程發起讀操作,會導致“syscall read”系統調用來從磁盤或網絡讀取數據;用戶進程發起寫操作,會導致“syscall write”系統調用來寫入到磁盤中或發送到網絡中。
1、由于局部性原理,操系統不會每次只讀取一個字節(代價太大),而是一次性讀取一片(一個或者若干個磁盤塊)的數據。
2、用戶態與內核態的轉化是一個耗時操作,因此IO操作時應盡量減少轉化操作。
基于以上兩點,需要有一個“中間緩沖區”——即內核緩沖區。用戶(JVM)發起讀操作時,系統先把數據從磁盤讀到內核緩沖區中,然后再把數據從內核緩沖區搬到用戶緩沖區;用戶(JVM)發起寫操作時,先把數據從用戶緩沖區搬到內核緩沖區,再把數據從內核緩沖區寫入到磁盤或網絡中。
# Java IO
數據在兩個設備之前的傳輸序列稱為流,設備可以為文件、網絡、內存,傳輸的內容可以為基本類型、序列化對象、本地化字符集
## 分類
根據處理數據類型不同分為:字節流和字符流。
* 字節流:以字節為單位(1Byte)。字節流能處理所有類型的數據
* 字符流:以字符為單位,根據編碼格式一次可能讀取多個字節。字符流只能處理字符類型的數據
根據流的方向不同可分為:輸入流和輸出流
* 輸入流:表示從一個設備源讀取數據,只能進行讀操作
* 輸出流:表示向一個目標設備寫數據,只能進行寫操作
Java 相關類:
類|說明
---|---
InputStream|字節輸入流
OutputStream|字節輸出流
Reader|字符輸入流
Writer|字符輸出流
Java 流類圖結構:

## InputStream
InputStream 類是所有輸入字節流的父類。
其常用子類包括:ByteArrayInputstream、StringBufferInputStream 和 FileInputStream。分別代表從 Byte 數組、StringBuffer 和 本地文件中讀取數據
## OutputStream
OutputStream 類是所有輸出字節流的父類。
其常用子類包括:ByteArrayOutputstream 和 FileOutputStream。分別代表向 Byte 數組和本地文件中寫入數據
## Reader
Reader 類是所有輸入字符流的父類。
其常用子類包括:CharReader 和 StringReader。分別代表從 Char 數組和 String 中讀取數據
## Writer
Writer 類是所有輸出字符流的父類。
其常用子類包括:CharArrayWriter 和 StringWriter。分別代表向 Char 數組和 String 中寫入數據
## BufferedOutputStream與BufferedInputStream
### BufferedInputStream
構建一個輸入流,示例如下:
```java
DataInputStream dataInputStream = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
```
使用FileInputStream的read方法讀取一個文件時,每次讀取一個字節,就需要訪問一次磁盤,頻繁操作磁盤,效率低下。為了減少訪問磁盤的次數,提高文件讀取的性能, Java提供了BufferedInputstream類,為其他輸入流提供緩沖功能。
創建BufferedInputStream時,會通過構造函數為其指定某個輸入流為參數,BufferedInputStream會將該輸入流分批讀取,每次讀取一部分到緩沖區中。當程序需要讀取數據時,直接從緩沖區進行讀取,由于是從內存緩沖區中讀取數
據,比每次從磁盤讀取效率高很多。
BufferedInputStream的緩沖區:
```java
// 默認緩沖區大小:8Kb
private static final int DEFAULT_BUFFER_SIZE = 8192;
// 存儲數據的緩沖區
protected volatile byte buf[];
// 當前緩沖區的數據索引
protected int pos;
// 構造方法
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
```
下面來看看從緩沖區讀取數據的代碼:
```java
public synchronized int read(byte b[], int off, int len) throws IOException {
// 通過查看緩沖區buf是否為null,來判斷流是否被釋放
getBufIfOpen();
// 檢查b[]是否放得下數據
if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int n = 0;
for (;;) {
// 調用私有方法read1來讀取數據,nread為已讀取到的字節數,沒有讀取或讀取不到數據返回-1
int nread = read1(b, off + n, len - n);
// 讀不到數據了,返回
if (nread <= 0)
return (n == 0) ? nread : n;
n += nread;
// 已經讀到了目標長度的數據,返回
if (n >= len)
return n;
// 如果輸入流已關閉,返回
InputStream input = in;
if (input != null && input.available() <= 0)
return n;
}
}
private int read1(byte[] b, int off, int len) throws IOException {
// 緩沖區的有效數據量
int avail = count - pos;
if (avail <= 0) {
// 需要讀取的數據量大于緩沖區的大小,此時使用緩沖區無意義
if (len >= getBufIfOpen().length && markpos < 0) {
// 直接交給InputStream去讀取
return getInIfOpen().read(b, off, len);
}
// 緩沖區已經沒有數據可以讀取,進行填充數據
fill();
// 緩沖區有效數據量
avail = count - pos;
// InputsStream中已經沒有有效數據可以讀取
if (avail <= 0) return -1;
}
int cnt = (avail < len) ? avail : len;
// 將緩沖區的數據讀入b[]
System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
// 更新緩沖區索引位置
pos += cnt;
return cnt;
}
```
其中填充緩沖區方法fill的代碼如下:
```java
private void fill() throws IOException {
// 獲取緩沖區
byte[] buffer = getBufIfOpen();
if (markpos < 0) {
pos = 0;
} else if (pos >= buffer.length) {
if (markpos > 0) {
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1;
pos = 0;
} else if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
} else { /* grow buffer */
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
}
count = pos;
// 從InputStream讀取數據到緩沖區
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0) {
count = n + pos;
}
}
```
### BufferedOutputStream
構建一個輸出流,示例如下:
```java
DataOutputStream dataOutputStream = new?DataOutputStream(new?BufferedOutputStream(new?FileOutputStream("filePath")));
```
BufferedOutputStream與BufferedInputStream類似,只不過方向是相反的。創建BufferedOutputStream時,也會在構造函數中為其指定一個輸出流作為參數,BufferedOutputStream會從OutputStream中分批接收數據并放入緩沖區中,當緩沖區已滿時,調用flush方法觸發系統調用,將緩沖區數據寫入文件或網絡。
來看看分批將數據寫入緩沖區的write方法:
```java
public synchronized void write(byte b[], int off, int len) throws IOException {
// 要寫出的數據大于緩沖區的容量,就不使用緩沖區策略了
if (len >= buf.length) {
// 先將緩沖區數據寫出
flushBuffer();
// 調用OutputStream的write方法直接將數據寫出
out.write(b, off, len);
return;
}
// 要寫出的數據大于緩沖區的剩余容量
if (len > buf.length - count) {
// 先將緩沖區的數據寫出
flushBuffer();
}
// 將要寫出的數據寫入到緩沖區
System.arraycopy(b, off, buf, count, len);
// 更新緩沖區已添加的數據容量
count += len;
}
```
看看flushBuffer方法:
```java
private void flushBuffer() throws IOException {
if (count > 0) {
out.write(buf, 0, count);
count = 0;
}
}
```
代碼很簡單,就是將緩沖區中的數據寫入到輸出流中。
## Java IO總結
IO緩沖區的存在,減少了系統調用的次數,提高了效率,但有緩沖區存在必然存在copy的過程,當涉及到雙流操作時,比如從一個輸入流讀入,寫入到一個輸出流中,就會存在冗余copy的操作,如下:
* 從輸入流讀出到緩沖區
* 從緩沖區copy到b[]
* 將b[] copy到輸出流緩沖區
* 輸出緩沖區的數據再讀出數據到輸出流
上面的情況存在冗余copy操作,我們來看看Okio是怎么處理的。
# Okio
Okio使用Segment來做數據存儲,代碼如下:
```kotlin
// 存儲具體數據的數組
@JvmField val data: ByteArray
// 有效數據索引起始位置
@JvmField var pos: Int = 0
// 有效數據索引結束位置
@JvmField var limit: Int = 0
// 指示Segment是否為共享狀態
@JvmField var shared: Boolean = false
// 指示當前Segment是否為數據擁有者
@JvmField var owner: Boolean = false
// 指向下一個Segment
@JvmField var next: Segment? = null
// 指向上一個Segment
@JvmField var prev: Segment? = null
companion object {
// 默認容量大小
const val SIZE = 8192
// 最小分享數據量
const val SHARE_MINIMUM = 1024
}
```
Segment被設計成可以分割的,用pos和limit來標記有效的數據范圍,用owner和shared來標識Segment是owner還是被共享者;同時,Segment可以采用雙向鏈表結構進行連接。
# 參考
[Java IO深入理解BufferedInputStream]([https://blog.csdn.net/yhl\_jxy/article/details/79318713](https://blog.csdn.net/yhl_jxy/article/details/79318713))
[Okio好在哪里?](https://url.cn/5xHNb05)
- 導讀
- Java知識
- Java基本程序設計結構
- 【基礎知識】Java基礎
- 【源碼分析】Okio
- 【源碼分析】深入理解i++和++i
- 【專題分析】JVM與GC
- 【面試清單】Java基本程序設計結構
- 對象與類
- 【基礎知識】對象與類
- 【專題分析】Java類加載過程
- 【面試清單】對象與類
- 泛型
- 【基礎知識】泛型
- 【面試清單】泛型
- 集合
- 【基礎知識】集合
- 【源碼分析】SparseArray
- 【面試清單】集合
- 多線程
- 【基礎知識】多線程
- 【源碼分析】ThreadPoolExecutor源碼分析
- 【專題分析】volatile關鍵字
- 【面試清單】多線程
- Java新特性
- 【專題分析】Lambda表達式
- 【專題分析】注解
- 【面試清單】Java新特性
- Effective Java筆記
- Android知識
- Activity
- 【基礎知識】Activity
- 【專題分析】運行時權限
- 【專題分析】使用Intent打開三方應用
- 【源碼分析】Activity的工作過程
- 【面試清單】Activity
- 架構組件
- 【專題分析】MVC、MVP與MVVM
- 【專題分析】數據綁定
- 【面試清單】架構組件
- 界面
- 【專題分析】自定義View
- 【專題分析】ImageView的ScaleType屬性
- 【專題分析】ConstraintLayout 使用
- 【專題分析】搞懂點九圖
- 【專題分析】Adapter
- 【源碼分析】LayoutInflater
- 【源碼分析】ViewStub
- 【源碼分析】View三大流程
- 【源碼分析】觸摸事件分發機制
- 【源碼分析】按鍵事件分發機制
- 【源碼分析】Android窗口機制
- 【面試清單】界面
- 動畫和過渡
- 【基礎知識】動畫和過渡
- 【面試清單】動畫和過渡
- 圖片和圖形
- 【專題分析】圖片加載
- 【面試清單】圖片和圖形
- 后臺任務
- 應用數據和文件
- 基于網絡的內容
- 多線程與多進程
- 【基礎知識】多線程與多進程
- 【源碼分析】Handler
- 【源碼分析】AsyncTask
- 【專題分析】Service
- 【源碼分析】Parcelable
- 【專題分析】Binder
- 【源碼分析】Messenger
- 【面試清單】多線程與多進程
- 應用優化
- 【專題分析】布局優化
- 【專題分析】繪制優化
- 【專題分析】內存優化
- 【專題分析】啟動優化
- 【專題分析】電池優化
- 【專題分析】包大小優化
- 【面試清單】應用優化
- Android新特性
- 【專題分析】狀態欄、ActionBar和導航欄
- 【專題分析】應用圖標、通知欄適配
- 【專題分析】Android新版本重要變更
- 【專題分析】唯一標識符的最佳做法
- 開源庫源碼分析
- 【源碼分析】BaseRecyclerViewAdapterHelper
- 【源碼分析】ButterKnife
- 【源碼分析】Dagger2
- 【源碼分析】EventBus3(一)
- 【源碼分析】EventBus3(二)
- 【源碼分析】Glide
- 【源碼分析】OkHttp
- 【源碼分析】Retrofit
- 其他知識
- Flutter
- 原生開發與跨平臺開發
- 整體歸納
- 狀態及狀態管理
- 零碎知識點
- 添加Flutter到現有應用
- Git知識
- Git命令
- .gitignore文件
- 設計模式
- 創建型模式
- 結構型模式
- 行為型模式
- RxJava
- 基礎
- Linux知識
- 環境變量
- Linux命令
- ADB命令
- 算法
- 常見數據結構及實現
- 數組
- 排序算法
- 鏈表
- 二叉樹
- 棧和隊列
- 算法時間復雜度
- 常見算法思想
- 其他技術
- 正則表達式
- 編碼格式
- HTTP與HTTPS
- 【面試清單】其他知識
- 開發歸納
- Android零碎問題
- 其他零碎問題
- 開發思路