Showing posts with label Memory Leaks. Show all posts
Showing posts with label Memory Leaks. Show all posts

Common Scenarios & Examples of Coroutine Memory Leaks

 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! ๐Ÿ’ป✨