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