airbnb / epoxy

Epoxy is an Android library for building complex screens in a RecyclerView
https://goo.gl/eIK82p
Apache License 2.0
8.5k stars 733 forks source link

Possible memory leak in EpoxyRecyclerView due to removeAdapterRunnable #909

Open mwajeeh opened 4 years ago

mwajeeh commented 4 years ago

Should listen to activity lifecycle and invoke clearRemovedAdapterAndCancelRunnable() when activity is destroyed. I got this memory leak report from LeakCanary.

1672448 bytes retained
┬
├─ android.os.MessageQueue
│    Leaking: NO (MessageQueue#mQuitting is false)
│    GC Root: Input or output parameters in native code
│    ↓ MessageQueue.mMessages
│                   ~~~~~~~~~
├─ android.os.Message
│    Leaking: UNKNOWN
│    ↓ Message.callback
│              ~~~~~~~~
├─ com.airbnb.epoxy.EpoxyRecyclerView$removeAdapterRunnable$1
│    Leaking: UNKNOWN
│    Anonymous class implementing java.lang.Runnable
│    ↓ EpoxyRecyclerView$removeAdapterRunnable$1.this$0
│                                                ~~~~~~
╰→ com.spruce.messenger.ui.DisablePoolEpoxyRecyclerView
​     Leaking: YES (View.mContext references a destroyed activity and ObjectWatcher was watching this)
​     mContext instance of com.spruce.messenger.ui.MainActivity with mDestroyed = true
​     View#mParent is set
​     View#mAttachInfo is null (view detached)
​     View.mWindowAttachCount = 1
​     key = b6ffc8e0-9178-46ad-94c0-71e9b5b0d8cd
​     watchDurationMillis = 207
​     retainedDurationMillis = -1
​     key = 33364902-1f22-47e7-87b7-e9d8a227a309

METADATA

Build.VERSION.SDK_INT: 28
Build.MANUFACTURER: Google
LeakCanary version: 2.0
App process name: xxxxxxxxx
Analysis duration: 10093 ms

DisablePoolEpoxyRecyclerView is a subclass with only one method:

class DisablePoolEpoxyRecyclerView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : EpoxyRecyclerView(context, attrs, defStyleAttr) {

    override fun shouldShareViewPoolAcrossContext(): Boolean {
        return false
    }
}

Using Epoxy: 3.8.0

manthesepeasrock commented 4 years ago

I'm experiencing something similar, even with the recycleChildrenOnDetach on the LayoutManager set to true and the setDelayMsWhenRemovingAdapterOnDetach set to 0 for all EpoxyRecyclerViews LeakCanary is still reporting leaks on the EpoxyRecyclerView.mRecycler

┬───
│ GC Root: System class
│
├─ android.view.inputmethod.InputMethodManager class
│    Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking)
│    ↓ static InputMethodManager.sInstance
├─ android.view.inputmethod.InputMethodManager instance
│    Leaking: NO (DecorView↓ is not leaking and InputMethodManager is a singleton)
│    ↓ InputMethodManager.mCurRootView
├─ com.android.internal.policy.DecorView instance
│    Leaking: NO (EpoxyRecyclerView↓ is not leaking and View attached)
│    mContext instance of com.android.internal.policy.DecorContext, wrapping activity pt.i9.app.presentation.MainActivity with mDestroyed = false
│    Parent android.view.ViewRootImpl not a android.view.View
│    View#mParent is set
│    View#mAttachInfo is not null (view attached)
│    View.mWindowAttachCount = 1
│    ↓ DecorView.mAttachInfo
├─ android.view.View$AttachInfo instance
│    Leaking: NO (EpoxyRecyclerView↓ is not leaking)
│    ↓ View$AttachInfo.mScrollContainers
├─ java.util.ArrayList instance
│    Leaking: NO (EpoxyRecyclerView↓ is not leaking)
│    ↓ ArrayList.elementData
├─ java.lang.Object[] array
│    Leaking: NO (EpoxyRecyclerView↓ is not leaking)
│    ↓ Object[].[0]
├─ com.airbnb.epoxy.EpoxyRecyclerView instance
│    Leaking: NO (View attached)
│    mContext instance of pt.i9.app.presentation.MainActivity with mDestroyed = false
│    View.parent androidx.coordinatorlayout.widget.CoordinatorLayout attached as well
│    View#mParent is set
│    View#mAttachInfo is not null (view attached)
│    View.mID = R.id.recyclerView
│    View.mWindowAttachCount = 1
│    ↓ EpoxyRecyclerView.mRecycler
│                        ~~~~~~~~~
├─ androidx.recyclerview.widget.RecyclerView$Recycler instance
│    Leaking: UNKNOWN
│    ↓ RecyclerView$Recycler.mRecyclerPool
│                            ~~~~~~~~~~~~~
├─ com.airbnb.epoxy.UnboundedViewPool instance
│    Leaking: UNKNOWN
│    ↓ UnboundedViewPool.scrapHeaps
│                        ~~~~~~~~~~
├─ android.util.SparseArray instance
│    Leaking: UNKNOWN
│    ↓ SparseArray.mValues
│                  ~~~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    ↓ Object[].[6]
│               ~~~
├─ java.util.LinkedList instance
│    Leaking: UNKNOWN
│    ↓ LinkedList.first
│                 ~~~~~
├─ java.util.LinkedList$Node instance
│    Leaking: UNKNOWN
│    ↓ LinkedList$Node.next
│                      ~~~~
├─ java.util.LinkedList$Node instance
│    Leaking: UNKNOWN
│    ↓ LinkedList$Node.item
│                      ~~~~
├─ com.airbnb.epoxy.EpoxyViewHolder instance
│    Leaking: UNKNOWN
│    ↓ EpoxyViewHolder.itemView
│                      ~~~~~~~~
├─ pt.i9.app.presentation.views.swipelayout.SwipeLayout instance
│    Leaking: UNKNOWN
│    mContext instance of pt.i9.app.presentation.MainActivity with mDestroyed = false
│    View#mParent is null
│    View#mAttachInfo is null (view detached)
│    View.mID = R.id.swipeLayout
│    View.mWindowAttachCount = 20
│    ↓ SwipeLayout.mChildren
│                  ~~~~~~~~~
├─ android.view.View[] array
│    Leaking: UNKNOWN
│    ↓ View[].[1]
│             ~~~
├─ androidx.constraintlayout.widget.ConstraintLayout instance
│    Leaking: YES (View detached and has parent)
│    mContext instance of pt.i9.app.presentation.MainActivity with mDestroyed = false
│    View#mParent is set
│    View#mAttachInfo is null (view detached)
│    View.mID = R.id.content
│    View.mWindowAttachCount = 20
│    ↓ ConstraintLayout.mListenerInfo
├─ android.view.View$ListenerInfo instance
│    Leaking: YES (ConstraintLayout↑ is leaking)
│    ↓ View$ListenerInfo.mOnClickListener
├─ pt.i9.app.base.presentation.models.transactions.GroupedTransactionModel$bind$1 instance
│    Leaking: YES (ConstraintLayout↑ is leaking)
│    Anonymous class implementing android.view.View$OnClickListener
│    ↓ GroupedTransactionModel$bind$1.this$0
├─ pt.i9.app.base.presentation.models.transactions.GroupedTransactionModel_ instance
│    Leaking: YES (ConstraintLayout↑ is leaking)
│    ↓ GroupedTransactionModel_.onActionClickListener
├─ pt.i9.app.dashboard.presentation.DashboardFragment$dashboardController$2$11 instance
│    Leaking: YES (ConstraintLayout↑ is leaking)
│    Anonymous subclass of kotlin.jvm.internal.Lambda
│    ↓ DashboardFragment$dashboardController$2$11.this$0
├─ pt.i9.app.dashboard.presentation.DashboardFragment$dashboardController$2 instance
│    Leaking: YES (ConstraintLayout↑ is leaking)
│    Anonymous subclass of kotlin.jvm.internal.Lambda
│    ↓ DashboardFragment$dashboardController$2.this$0
╰→ pt.i9.app.dashboard.presentation.DashboardFragment instance
​     Leaking: YES (ObjectWatcher was watching this because pt.i9.app.dashboard.presentation.DashboardFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​     key = 28528e9b-c360-4410-ab49-2889938f6847
​     watchDurationMillis = 6960
​     retainedDurationMillis = 1960
​     key = 3be8dc75-786e-4d60-b94c-bc7d950c1536

Using Epoxy: 3.9.0

elihart commented 4 years ago

@mwajeeh the leakcanary trace you reported links the removeAdapterRunnable Runnable, which is post delayed for 2 seconds. I am guessing that this is just delayed for longer than LeakCanary waits. you can control this with setRemoveAdapterWhenDetachedFromWindow and setDelayMsWhenRemovingAdapterOnDetach