What is a Memory Leak?
A memory leak occurs when memory that is no longer needed is not released because the program still holds references to it. In Android, this means the garbage collector (GC) cannot reclaim that memory, potentially leading to OutOfMemoryError and app crashes.
How Do Memory Leaks Occur in a Coroutine?
Coroutines are lightweight threads, but if not scoped and managed correctly, they can continue running even when the component (like an Activity or ViewModel) is destroyed. This can lead to memory leaks.
Common Scenarios & Examples of Coroutine Memory Leaks
Here are some common scenarios where memory leaks can happen with coroutines, what causes them, what happens afterward, and how to fix them:
1. Coroutine Scope Tied to a Destroyed Component
❌ Problem:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
delay(10_000) // 10-second delay
Log.d("Coroutine", "Finished!")
}
}
}
What Happens:
Even if the user rotates the screen or navigates away, the coroutine keeps running unless cancelled. If it references this
, the Activity
is retained in memory.
✅ Fix:
Use a lifecycle-aware scope, like lifecycleScope
, and ensure the task is cancellable:
lifecycleScope.launch {
withTimeoutOrNull(5000) {
delay(10_000)
}
}
2. GlobalScope Misuse
❌ Problem:
GlobalScope.launch {
val bitmap = loadLargeBitmap()
imageView.setImageBitmap(bitmap)
}
What Happens:
-
GlobalScope
lives for the entire app lifecycle. -
If
imageView
belongs to a destroyed Activity, it is retained in memory.
✅ Fix:
Use viewModelScope
, lifecycleScope
, or a custom CoroutineScope
that is cancelled appropriately:
class MyViewModel : ViewModel() {
fun loadImage() {
viewModelScope.launch {
val bitmap = loadLargeBitmap()
_bitmapLiveData.value = bitmap
}
}
}
3. ViewModel or Activity Holds Long-lived Coroutine with UI Reference
❌ Problem:
class MyViewModel : ViewModel() {
var activity: MainActivity? = null
fun doWork() {
viewModelScope.launch {
delay(10_000)
activity?.updateUI() // Leaks MainActivity
}
}
}
What Happens:
Even after MainActivity
is destroyed, the coroutine keeps a reference to it via activity
.
✅ Fix:
Avoid passing UI references. Use LiveData
, StateFlow
, or a callback interface that is weakly referenced.
4. Job not Cancelled in onDestroy()
❌ Problem:
class MyActivity : AppCompatActivity() {
private val job = Job()
private val scope = CoroutineScope(Dispatchers.Main + job)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
scope.launch {
fetchData()
}
}
// Missing job.cancel()
}
What Happens:
The coroutine continues running even after the Activity is destroyed.
✅ Fix:
Cancel the job:
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
5. Flow Collection Without Proper Scope
❌ Problem:
viewModel.dataFlow.onEach {
textView.text = it
}.launchIn(GlobalScope) // WRONG
What Happens:
This will outlive the lifecycle of the view that textView
belongs to.
✅ Fix:
viewModel.dataFlow
.onEach { textView.text = it }
.launchIn(lifecycleScope)
Or use:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.dataFlow.collect {
textView.text = it
}
}
}
Best Practices to Avoid Coroutine Memory Leaks
Practice | Explanation |
---|---|
Use viewModelScope , lifecycleScope |
These scopes are lifecycle-aware and cancel automatically. |
Avoid GlobalScope for UI tasks |
GlobalScope never cancels, leading to leaks. |
Always cancel custom scopes | Manually call job.cancel() in onDestroy() or similar. |
Avoid long-lived references to Context/UI | Don’t store Activities/Views inside ViewModels or background coroutines. |
Use structured concurrency | Always launch coroutines inside a well-defined scope. |
Make coroutines cancellable | Use withTimeout , isActive , and ensure long-running tasks honor cancellation. |
Use repeatOnLifecycle for Flow |
Ensures collection only happens during active lifecycle state. |
Use Case Example: Fetch User Profile and Update UI
❌ Incorrect (Leaky)
class ProfileActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch {
val user = fetchUserProfile()
runOnUiThread {
profileTextView.text = user.name
}
}
}
}
✅ Correct (Leak-safe)
class ProfileActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val user = fetchUserProfile()
profileTextView.text = user.name
}
}
}
Summary
๐ Don't Do This | ✅ Do This Instead |
---|---|
Launch coroutine in GlobalScope |
Use viewModelScope or lifecycleScope |
Hold references to Activity/View | Use LiveData /StateFlow for updates |
Ignore coroutine cancellation | Make coroutine cancellable |
Launch coroutine in onCreate w/o guard |
Use repeatOnLifecycle or cancel scope |
๐ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! ๐ please leave a comment below. I’d love to hear from you! ๐
Happy coding! ๐ป✨
0 comments:
Post a Comment