VIN
VIN

Reputation: 6967

LeakCanary View.mContext references a destroyed activity (Custom View)

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

Answers (1)

VIN
VIN

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.

https://proandroiddev.com/cancelling-kotlin-coroutines-1030f03bf168#:~:text=Cancellation%20Cooperative&text=This%20means%20that%20if%20you,loop%20will%20continue%20until%20complete.

Upvotes: 2

Related Questions