Android 效能優化系列 — 18 使用 Coroutine 減少 Memory leak

Evan Chen
6 min readOct 6, 2022

--

Photo by Marianna Lutkova on Unsplash

在背景執行緒執行一段任務,未完成時就離開 Activity 是有可能造成 Memory leak 的。如下程式碼是一段使用背景執行緒請求網路資料:

  1. 呼叫 networkCall() 模擬 10 秒後取得資料。
  2. 使用 runOnUiThread 將取得的更新到 UI 上。
//未執行完離開App會造成memory leak
object : Thread() {
override fun run() {
//背景執行緒取得資料
val data = networkCall()
//更新到 UI 上
runOnUiThread {
binding.result.text = data
}
}
}.start()
//模擬網路請求資料
private fun networkCall(): String {
sleep(10000)
return "Data1"
}

上面這段程式碼,如果我們在背景執行緒未執行完成時就離開 Activity,就會因為背景執行緒仍在執行,Activity 無法被釋放,造成 Memory leak。

使用 Coroutine 處理非同步

這個問題將使用 Coroutine 的非同步處理來解決。Coroutine 是在 Kotlin 用來方便處理非同步需求的一個框架。有著易開發、好管理的好處,而且符合結構化並發 (Structured Concurrency) 架構。讓你寫非同步就跟同步一樣的簡單。另外 Coroutine 也是 Android 進行非同步程式設計時,官方的推薦解決方案。

首先修改網路請求資料的 networkCall function。

  1. 將 function 加上 suspend,使變成 suspending function。
  2. 使用 withContext(Dispatchers.IO) 切換至背景執行緒
  3. 回傳資料
//模擬網路請求資料
private suspend fun networkCall(): String {
val data = withContext(Dispatchers.IO){
delay(10000)
"Data1"
}
return data
}

接著在 Activity 就可以透過 lifecycleScope.launch 來呼叫 networkCall 取得資料,當執行 networkCall 時會切換至背景執行緒,取得資料後就會再回到 UI 執行緒,這種寫法非常方便,讓寫非同步就跟寫同步一樣。

使用 lifecycleScope.launch 建立一個 Coroutine 來執行 networkCall。

lifecycleScope.launch {
//取得資料
val data = networkCall()
//回到UI執行緒
binding.result.text = data
}

在lifecycleScope 裡的這段程式碼的生命週期將與 Activity 的生命週期一致,所以當 Activity 被銷毀,Coroutine 執行中的任務也將被取消,也就不會發生當離開 Activity 時,背景執行緒仍在執行。

Coroutine 的階層管理

Coroutine 在處理多個執行緒時,尤其是有階層關系時,比起使用 Thread,更來得容易管理,也能減少發生 Memory leak 的機會,

如下例,我們新增了兩個 job 分別處理網路請求 networkCall 與其子任務 networkCall2。當我們使用 job.join 等待所有的 Coroutine 工作完成時。像這樣的階層關系,使用 Coroutine 就非常方便,會幫你處理好等到所有的子 Coroutine 都完成才算是完成。

job = lifecycleScope.launch {
try {
val data = networkCall()
val job2 = launch {
val data2 = networkCall2()
}
} catch (e: CancellationException) {
println("Cancel done")
}
}
lifecycleScope.launch {
job?.join()
println("All done")
}

再看另一個範例是 Coroutine 的取消。我們對 job 呼叫了 job.cancelAndJoin 來取消這個 Coroutine 的執行,子 Coroutine 也會被取消。這是 Coroutine 的一個很棒的地方,不用擔心會有子任務在父任務取消後仍在背景執行。

job = lifecycleScope.launch {
try {
val data = networkCall()
//child
val job2 = launch {
val data2 = networkCall2()
}
} catch (e: CancellationException) {
println("Cancel done")
}
}
lifecycleScope.launch {
job?.cancelAndJoin()
println("All done")
}

如果父 Coroutine 取消或失敗了,我們不會希望要還有在背地裡執行的執行緒,因為這容易產生 Memory leak。以上舉的這幾個 Coroutine 範例,其實都是在確保當一個任務不再需要被執行,其子任務也都將被取消。

最後,Coroutine 已經是現在開發 Android 一定會使用的,好處是非同步的處理更方便、不易出錯、減少 Memory leak 機會。這邊只是初步的介紹 Coroutine 如何減少 Memory leak,Coroutine 的詳細介紹請見 Coroutine官網

參考:
https://kotlinlang.org/docs/coroutines-guide.html
https://developer.android.com/kotlin/coroutines

下一編:使用 LeakCanary 找出 Memory leak

--

--