文章目錄
- 一、問題背景
- 二、代碼實現
一、問題背景
經常做 Android
應用的小伙伴應該會有經驗,就是如果應用在寫入文件的時候,即使寫文件的動作是在子線程,也會出現 UI
上的卡頓,這是因為文件的 IO
是由內核去完成的,此時 CPU
會從 用戶態
切換到 內核態
,應用將無法獲得 CPU
時間片。如果 IO
較少的時候,這個切換不頻繁,應用能有足夠的時間片去執行繪制。但如果 IO
頻繁,例如高速寫入的場景下,此時 IO
占用的時間片增加,而我們知道 CPU
資源是有限的,此時會導致應用獲得 時間片
將減少,導致 UI
繪制線程出現卡頓,進而影響 UI
上的卡頓。
我們可以使用 top
命令,查看 IO
占用 CPU
的時間片(即 iow
):
因此,如果不能從其它方式去解決此問題(內核層的 IO
調度 和 CPU
調度機制),可以運用簡單的方式:在寫入文件的時候,限制寫入速度,減緩 IO
占用率。
二、代碼實現
例如,一個標準的拷貝文件的 Kotlin
方法如下:
originFile.inputStream().buffered().use { inputStream ->targetFile.outputStream().buffered().use { outputStream ->val buffer = ByteArray(1024)var bytesRead: Intwhile (inputStream.read(buffer).also { bytesRead = it } != -1) {outputStream.write(buffer, 0, bytesRead)}outputStream.flush()}
}
這個方法對 源文件
開啟了 InputStream
,并轉換為 BufferInputStream
,而對 目標文件
開啟了 OutputStream
,并轉換為 BufferOutputStream
,然后再從 InputStream
讀取流,輸出到 OutputStream
,完成文件的讀寫。
而我們為了實現限制其寫入速度,我們可以定義一個速率,例如每秒寫入 20M
,即每秒寫入的字節數為 20 * 1024 * 1024 = 20, 971, 520
。在每次寫入量達到 20M
的時候,檢查一次寫入這 20M
使用的時長,如果這個時長 < 1s
,則表示在這 1s
的時間里,寫入的速率已經超過了 20M
,需要等待一定時長,使其滿足 1s
內只寫入 20M
的需求。代碼如下:
// 限制拷貝時的速率, 每秒最多拷貝的字節數
val MAX_COPY_SPEED = 20 * 1024 * 1024L// 記錄已拷貝的文件大小
var copiedSize = 0L// 記錄上次檢查性能的時間 (用來計算拷貝速率 限制拷貝速率)
var lastCheckPerformanceTime = System.currentTimeMillis()
// 記錄上次檢查時 已拷貝的數據量
var lastCheckPerformanceSize = 0LoriginFile.inputStream().buffered(1024 * 1024).use { inputStream ->targetFile.outputStream().buffered().use { outputStream ->val buffer = ByteArray(DEFAULT_BUFFER_SIZE)var bytesRead: Intwhile (inputStream.read(buffer).also { bytesRead = it } != -1) {outputStream.write(buffer, 0, bytesRead)// 增加已拷貝文件的大小copiedSize += bytesRead// 計算拷貝速率 每滿 MAX_COPY_SPEED 計算一次速率if (copiedSize - lastCheckPerformanceSize >= MAX_COPY_SPEED) {lastCheckPerformanceSize = copiedSizeval time = System.currentTimeMillis()// 計算寫入 MAX_COPY_SPEED 字節數所使用的時間val deltaTime = time - lastCheckPerformanceTimelastCheckPerformanceTime = time// 如果速率大于 MAX_COPY_SPEED 則暫停 等待下次計算if (deltaTime < 1000) {// 延遲 1000 - deltaTime 使滿足 1s 內只寫入指定量delay(1000 - deltaTime)}}}outputStream.flush()}
}