Reputation: 6967
I'm trying to figure out how to resolve my memory leak. I have a CustomActivity
, which adds a Fragment
that contains a com.example.CustomViewGroup
, which has a reference to a com.example.CustomView
.
class CustomFragment : Fragment() {
override fun onViewCreated(view:View, savedInstanceState:Bundle?)
{
view.findViewById<CustomViewGroup>(R.id.custom_view_group).also { vg ->
lifecycleScope.launch {
...
lifecycleScope.launch {
vg.doSomethingInASuspendMethod()
}
}
}
}
}
class CustomViewGroup : ConstraintLayout {
internal var customView: CustomView? = null
init {
View.inflate(context, R.layout.custom_view_group, this)
customView = findViewById(R.id.custom_view)
...
}
//I've tried numerous things below, but none of them seem to help
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
customView?.onDestroy(owner)
super.onDetachedFromWindow()
customView = null
removeAllViews()
}
}
I've tried the approaches in onDestroy
but nothing seems to help. I think it is related to the nested coroutine scopes though.
58913 bytes retained by leaking objects
Displaying only 1 leak trace out of 2 with the same signature
┬───
│ GC Root: Java local variable
│
├─ kotlinx.coroutines.scheduling.CoroutineScheduler$Worker thread
│ Leaking: UNKNOWN
│ Thread name: 'DefaultDispatcher-worker-1'
│ ↓ CoroutineScheduler$Worker.<Java Local>
│ ~~~~~~~~~~~~
├─ com.example.CustomView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ mContext instance of com.example.CustomActivity with mDestroyed = true
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.null
│ View.mWindowAttachCount = 1
│ ↓ CustomView.mContext
╰→ com.example.CustomActivity instance
Leaking: YES (ObjectWatcher was watching this because com.example.CustomActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
Upvotes: 3
Views: 1620
Reputation: 6967
Turned out I was doing something in a while
loop, and I needed to check if the coroutine was still active.
internal suspend fun doSomethingInASuspendMethod() {
withContext(Dispatchers.Default) {
while (isActive) { // was while(true) before the fix
...
}
}
}
Cancellation Cooperative:
Just like Android's AsyncTask, coroutines are cancellation cooperative. When calling cancel on a coroutine job, the isActive flag is set to false, but the job will continue to run. This means that if you have a long running loop inside of a coroutine but do not check for the isActive flag to exit out on, the loop will continue until complete. In this example, a good place to check for the isActive flag might be after each iteration of the loop.
Upvotes: 2