Unavoidable memory leak when using coroutines

You eat the elephant by the wrong side.

  1. Bitmap is not GC-unavailable. Repeat backgroundFunction() billion times with just 50 ms delay - you don’t get the OOM (without delay you don’t give a chance for GC).
  2. You have MainActivity rooted references. Not coroutine rooted reference. Not couroutineContext rooted reference. Should coroutine free your code-designed reference from MainActivity to own instance? Also you can see that kind of reference if you just start new thread from your MainActivity. Even in Java. It’s not strong, but here is it.

But what shold we do to stay calm? Just don’t create your async allocations in Activity class. Make fabric or smth. Bitmap.createBitmap() actually is a fabric method, and it dont need any context. Or at least allocate it right in coroutine (any context you like, you can use globalScope, for example you save the result on sd and you really need it run to the end):

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_test_leak)
    textView = findViewById(R.id.textView)
    button = findViewById(R.id.button)

    button.setOnClickListener {
        doCoroutines()
    }
}

private fun doCoroutines() {
    GlobalScope.launch(Dispatchers.Default) {
        val bitmap = Bitmap.createBitmap(4096, 4096, Bitmap.Config.ARGB_8888)
        withContext(Dispatchers.Main) {
            textView?.text = "BITMAP SIZE: ${bitmap.width} x ${bitmap.height}"
        }
    }
}

Voila:


You have only GC ref to it. And yes, you can create lifecycle-binded scope to avoid call non-existing textView but it’s another story.

1 Like

And the third story. Why different coroutine scopes is not panacea? Because if you have several scopes for massive allocations (like 4k bitmaps) you whatever gets OOM if you run coroutines at the same or about the same time. It must be the same number of scopes with single allocations as repeat count in one global scope lanch. In both cases GC can’t make it’s work before next allocation and you get OOM. So there is no differences to use global or local scope in activity for just one activity bitmap allocation, but both ways can be dangerous when you produce a lot of bitmaps and don’t know how much consumers you have. In other words: if you have one box for 3 cubes(your heap) - there is no differences you have 4 hands with one cube in each (single scope and 4 allocation consumers) or you have friend (you - scope1, and friend - scope2) you both two-handed and you both hold cube in each hand. When you put all cubes into the box at the same time you got collision (OOM) anyway.
The good news: if you just need one picture for activity you don’t need even think about it. The bad news: if you have large bitmaps producer for udentified number of consumers - you need to organize it in a right way. Like queue with delayed next allocation, and not after the allocation itself, but after your bitmap consumed and reference dropped. In coroutine terms: Channel with forEach { doBitmapWork(it); delay(aLittle); } manager.

I’m used to this scope mangement. Is this not the preferred way to have single parent job as CoroutineContext? Cancel it and all its child jobs get canceled too.

class Something: CoroutineScope{
   private val job = Job()
   override val coroutineContext: CoroutineContext get() = job
...
   override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }