epoxy
epoxy copied to clipboard
ACTIVITY_RECYCLER_POOL leak when use Carousel inside EpoxyModelGroup
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
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.
I have this problem too
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