iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
Mobile Development

Android 性能戰爭:從 Profiler 開始的 30 天優化實錄系列 第 21

# Day 21:【流暢度戰爭】主執行緒的守護者:嚴禁 I/O 操作

  • 分享至 

  • xImage
  •  

各位戰士,歡迎來到第二十一天的戰場。在過去的一週,我們投身於慘烈的 UI 流暢度陣地戰。我們學會了用 ConstraintLayout 攻克佈局的堡壘,用 ListAdapter 精準打擊 RecyclerView 的敵人,還學會了在 Jetpack Compose 的新戰場上打贏 Recomposition 戰爭。

所有這些戰術,無論多麼精妙,都服務於一個最終的戰略目標。今天,我們就要頒布這條統率整個流暢度戰爭的最高軍法,一條無論在傳統 View 體系還是 Jetpack Compose 中都神聖不可侵犯的鐵律:

任何時候,任何情況下,都絕不允許阻塞主執行緒 (Main Thread / UI Thread)!

回顧我們奮戰至今的一切,Jank 的根源無一例外都是主執行緒被佔用太久,導致它錯過了 16ms 一次的 VSYNC 軍令。今天,我們就要把那些最常導致阻塞的「頭號通緝犯」揪出來,並學習如何用我們最強的「特種部隊」——Kotlin 協程——來制服它們。


主執行緒的頭號通緝犯名單

以下是在主執行緒上執行時,會立刻引發性能災難的「慣犯」。請務必將它們刻入你的 DNA。

罪犯一:網路操作 (Network Operations)

  • 犯罪行為:發起 API 請求、下載圖片、上傳檔案等。
  • 犯罪後果:網路延遲極不穩定,從幾十毫秒到數秒不等。在主執行緒上發起網路請求,會導致 App 畫面完全凍結,直到網路回應。在現代 Android 系統上,系統會直接拋出 NetworkOnMainThreadException 讓你的 App 崩潰,這是一種強制性的保護措施。

罪犯二:磁碟 I/O (Disk I/O)

  • 犯罪行為:讀寫資料庫 (Room/SQLite)、讀寫 SharedPreferences、讀寫手機內部或外部儲存的任何檔案。
  • 犯罪後果:雖然比網路快,但磁碟讀寫的速度依然是毫秒級的,尤其是在儲存空間將滿或設備老舊時。一次稍微複雜的資料庫查詢,就足以輕鬆突破 16ms 的幀預算,引發肉眼可見的卡頓。

罪犯三:大量計算 (Heavy Computation)

  • 犯罪行為:處理大型 JSON 檔案的解析、對一個巨大的列表進行排序或篩選、對圖片點陣圖 (Bitmap) 進行濾鏡處理等複雜演算法。
  • 犯罪後果:這會讓 CPU 滿載運行,主執行緒被計算任務佔滿,完全沒有時間去處理 UI 的測量、佈局和繪製工作,導致畫面嚴重掉幀。

王牌部隊:Kotlin 協程 (Coroutines)

既然主執行緒如此寶貴,我們該如何處理這些耗時的「髒活累活」?答案就是:將它們交給背景執行緒

在 Android 開發中,處理非同步任務的工具有很多(傳統 Thread、RxJava 等),但目前官方最推薦、也是最現代化的武器,就是 Kotlin 協程

為何協程是最佳選擇?

  1. 輕量:協程被稱為「輕量級執行緒」,你可以輕鬆啟動成千上萬個而不用擔心記憶體耗盡。
  2. 結構化並行 (Structured Concurrency):這是協程的王牌。協程的生命週期會與一個 CoroutineScope 綁定(例如 viewModelScope)。當 ViewModel 被銷毀時,viewModelScope 會被取消,所有在這個範圍內啟動的協程也會被自動取消。這從根本上解決了非同步任務中常見的記憶體洩漏問題。
  3. 程式碼可讀性:使用 async/awaitsuspend 關鍵字,可以讓我們用近乎同步的語法寫出清晰易讀的非同步程式碼,徹底告別「回呼地獄 (Callback Hell)」。

標準作戰範式

讓我們看一個在 ViewModel 中載入資料的標準作戰流程:

錯誤示範 (阻塞主執行緒):

// 在 ViewModel 中
fun loadDataFromDatabase() {
    // 致命錯誤!直接在主執行緒上查詢資料庫
    val data = database.userDao().getAll() 
    _uiState.value = data
}

正確示範 (使用協程):

// 在 ViewModel 中
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

fun loadDataFromDatabase() {
    // 1. 在 viewModelScope 中啟動一個協程,它會跟隨 ViewModel 的生命週期
    viewModelScope.launch {
        // 2. 使用 withContext 切換到 IO 執行緒池,專門處理磁碟/網路操作
        val data = withContext(Dispatchers.IO) {
            // 在這裡執行耗時的資料庫查詢,不會阻塞主執行緒
            database.userDao().getAll()
        }
        // 3. withContext 結束後,自動切回主執行緒來更新 UI
        _uiState.value = data
    }
}

這個範式必須成為你的肌肉記憶:在 viewModelScope 中啟動協程,用 withContext 將耗時任務切到背景執行緒 (Dispatchers.IODispatchers.Default),然後在主執行緒更新 UI。

第二場戰役總結

今天,我們為 UI 流暢度攻防戰畫下了句點,並確立了其最高指導原則:守護主執行緒。

  • 我們學會了識別那些會阻塞主執行緒的頭號通緝犯:網路、磁碟 I/O 和大量計算。

  • 我們掌握了使用 Kotlin 協程 這支王牌部隊,將所有耗時任務安全地移出主執行緒的標準作戰流程。

回顧這一週,我們打的是一場組合拳:一方面,透過佈局優化、RecyclerView 技巧、管理 Compose 重組,來讓我們在主執行緒上必須做的 UI 工作變得更快;另一方面,透過協程,來將所有可以在主執行緒之外做的非 UI 工作全部移走。只有兩者兼顧,才能取得 UI 流暢度的最終勝利。

陣地戰已經結束。接下來,戰爭將進入更宏觀的 【持久戰 —— 資源管理與自動化監控】。我們將把目光從使用者眼前的卡頓,轉向那些隱藏在應用背後的敵人:記憶體洩漏、過大的 APK 體積、以及如何建立自動化的哨兵來防止性能再次劣化。

我們明天見!


上一篇
# Day 20:【流暢度戰爭】Compose 中的 Recomposition 戰爭
下一篇
# Day 22:【資源戰爭】圖片的最適化載入 (Image Loading)
系列文
Android 性能戰爭:從 Profiler 開始的 30 天優化實錄22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言