airbnb / epoxy

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

ACTIVITY_RECYCLER_POOL leak when use Carousel inside EpoxyModelGroup #1331

Open bitvale opened 1 year ago

bitvale commented 1 year ago

Tested on Android API 30 and API 33, Epoxy version 5.1.1, Leakcanary version 2.10

Simple app with default Carousel inside EpoxyModelGroup:

CarouselEpoxyModel:

@EpoxyModelClass
abstract class CarouselExampleModel(
    carouselModel: EpoxyModel<out Carousel>
) : EpoxyModelGroup(R.layout.epoxy_carousel_model, carouselModel)

R.layout.epoxy_carousel_model

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

BannerView:

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
internal class BannerView : MaterialCardView {

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    @set:ModelProp
    var model: Data? = null

    @AfterPropsSet
    fun bind() {
        // no ops 
    }
}

buildModels():

override fun buildModels(data: List<Data>) {
    val models = data.mapIndexed { index, it ->
            BannerViewModel_()
            .id(index)
            .model(it)
    }

    val carouselModel = CarouselModel_()
                .id("carousel", "id")
                .models(models)

     carouselExample(carouselModel) {
          id("carousel_exaple", "example_id")
     }
}

And in Fragment onDestroyView I add this line: epoxyRecycler.clear() (without this line behavior the same)

Leak happens when I navigate to another fragment.

But there is no leak if I don' t use EpoxyModelGroup:

override fun buildModels(data: List<Data>) {
    val models = data.mapIndexed { index, it ->
            BannerViewModel_()
            .id(index)
            .model(it)
    }

    val carouselModel = CarouselModel_()
                .id("carousel", "id")
                .models(models)

     add(carouselModel) // No leak if I just add model without EpoxyModelGroup
}

PS: the code is simplified, the EpoxyModelGroup is needed for a more complex ui, but the leak is reproducible with this simple EpoxyModelGroup.

Leak info:

┬───
│ GC Root: Thread object
│
├─ android.net.ConnectivityThread instance
│    Leaking: NO (PathClassLoader↓ is not leaking)
│    Thread name: 'ConnectivityThread'
│    ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│    Leaking: NO (EpoxyRecyclerView↓ is not leaking and A ClassLoader is never
│    leaking)
│    ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│    Leaking: NO (EpoxyRecyclerView↓ is not leaking)
│    ↓ Object[241]
├─ com.airbnb.epoxy.EpoxyRecyclerView class
│    Leaking: NO (a class is never leaking)
│    ↓ static EpoxyRecyclerView.ACTIVITY_RECYCLER_POOL
│                               ~~~~~~~~~~~~~~~~~~~~~~
├─ com.airbnb.epoxy.ActivityRecyclerPool instance
│    Leaking: UNKNOWN
│    Retaining 52 B in 3 objects
│    ↓ ActivityRecyclerPool.pools
│                           ~~~~~
├─ java.util.ArrayList instance
│    Leaking: UNKNOWN
│    Retaining 40 B in 2 objects
│    ↓ ArrayList[0]
│               ~~~
├─ com.airbnb.epoxy.PoolReference instance
│    Leaking: UNKNOWN
│    Retaining 44 B in 2 objects
│    ↓ PoolReference.viewPool
│                    ~~~~~~~~
├─ com.airbnb.epoxy.UnboundedViewPool instance
│    Leaking: UNKNOWN
│    Retaining 592,5 kB in 11066 objects
│    ↓ UnboundedViewPool.scrapHeaps
│                        ~~~~~~~~~~
├─ android.util.SparseArray instance
│    Leaking: UNKNOWN
│    Retaining 592,0 kB in 11054 objects
│    ↓ SparseArray.mValues
│                  ~~~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    Retaining 591,9 kB in 11052 objects
│    ↓ Object[2]
│            ~~~
├─ java.util.LinkedList instance
│    Leaking: UNKNOWN
│    Retaining 573,8 kB in 10473 objects
│    ↓ LinkedList[0]
│                ~~~
├─ com.airbnb.epoxy.EpoxyViewHolder instance
│    Leaking: UNKNOWN
│    Retaining 573,8 kB in 10471 objects
│    ↓ EpoxyViewHolder.epoxyHolder
│                      ~~~~~~~~~~~
├─ com.airbnb.epoxy.ModelGroupHolder instance
│    Leaking: UNKNOWN
│    Retaining 571,7 kB in 10426 objects
│    ↓ ModelGroupHolder.modelGroupParent
│                       ~~~~~~~~~~~~~~~~
├─ com.airbnb.epoxy.EpoxyRecyclerView instance
│    Leaking: UNKNOWN
│    Retaining 571,6 kB in 10423 objects
│    View not part of a window view hierarchy
│    View.mAttachInfo is null (view detached)
│    View.mID = R.id.epoxyRecycler
│    View.mWindowAttachCount = 1
│    mContext instance of com.myapp.MyActivity with mDestroyed = false
│    ↓ View.mParent
│           ~~~~~~~
├─ android.widget.FrameLayout instance
│    Leaking: UNKNOWN
│    Retaining 4,7 kB in 103 objects
│    View not part of a window view hierarchy
│    View.mAttachInfo is null (view detached)
│    View.mID = R.id.root
│    View.mWindowAttachCount = 1
│    mContext instance of com.myapp.MyActivity with mDestroyed = false
│    ↓ View.mParent
│           ~~~~~~~
╰→ androidx.coordinatorlayout.widget.CoordinatorLayout instance
      Leaking: YES (ObjectWatcher was watching this because com.myapp.
      MyFragment received Fragment#onDestroyView()
      callback (references to its views should be cleared to prevent leaks))
      Retaining 538,6 kB in 9486 objects
      key = 3f91241b-7fd8-412a-a0ae-3c5346fedd32
      watchDurationMillis = 5534
      retainedDurationMillis = 533
      View not part of a window view hierarchy
      View.mAttachInfo is null (view detached)
      View.mID = R.id.rootView
      View.mWindowAttachCount = 1
      mContext instance of com.myapp.MyActivity with mDestroyed = false
bitvale commented 1 year ago

This leak happens only with default Carousel, when using EpoxyModelGroup with other non Carousel models all works fine without leaks. Use inflater from activity while creating fragment's view nor set adapter to null in onDestroyView() doesn't help.

juckrit commented 9 months ago

I have this problem too

aijones commented 9 months ago

calling my_recycler.recycler_viewpool.clear() directly fixed this for me. Im using a fragment & i noticed that the pool only clears when the activity is destroyed. cc @juckrit