I'm a newbie to fixing memory leaks and I don't really understand how I should remove them, especially in custom views. In this particular case I have a custom MapLegendView, which is being used in a MapPageFragment.
MapLegendView code:
class MapLegendView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
private var scrollLeftButton: ImageButton? = null
private var scrollRightButton: ImageButton? = null
private var closeLegendButton: ImageButton? = null
private var legendScrollView: HorizontalScrollView? = null
private var horizontalScrollAmount = 0
private var scrollDelta = 0
private val scrollLeft: Runnable
private val scrollRight: Runnable
var onCloseButtonClickedListener: (() -> Unit)? = null
init {
View.inflate(context, R.layout.view_map_legend, this)
scrollLeftButton = findViewById(R.id.scrollLeftButton)
scrollRightButton = findViewById(R.id.scrollRightButton)
closeLegendButton = findViewById(R.id.closeLegendButton)
legendScrollView = findViewById(R.id.legendScrollView)
scrollLeft = object : Runnable {
override fun run() {
legendScrollView?.let { it.scrollTo(it.scrollX - scrollDelta, 0) }
handler.postDelayed(this, 10)
}
}
scrollRight = object : Runnable {
override fun run() {
legendScrollView?.let { it.scrollTo(it.scrollX + scrollDelta, 0) }
handler.postDelayed(this, 10)
}
}
orientation = VERTICAL
}
@SuppressLint("ClickableViewAccessibility")
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val dp = dpInPx(1)
viewTreeObserver.addOnGlobalLayoutListener {
scrollDelta = 10 * dp
legendScrollView?.let {
horizontalScrollAmount = it.getChildAt(0).width - it.width
it.setFadingEdgeLength(it.width / 8)
}
handleScrollButtonsVisibility()
}
closeLegendButton?.setOnClickListener { onCloseButtonClickedListener?.let { it1 -> it1() } }
legendScrollView?.viewTreeObserver?.addOnScrollChangedListener { handleScrollButtonsVisibility() }
scrollLeftButton?.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> handler.post(scrollLeft)
MotionEvent.ACTION_UP -> handler.removeCallbacks(scrollLeft)
}
return@setOnTouchListener true
}
scrollRightButton?.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> handler.post(scrollRight)
MotionEvent.ACTION_UP -> handler.removeCallbacks(scrollRight)
}
return@setOnTouchListener true
}
}
public override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scrollLeftButton = null
scrollRightButton = null
closeLegendButton = null
legendScrollView = null
onCloseButtonClickedListener = null
}
private fun handleScrollButtonsVisibility() {
scrollRightButton?.visibility =
if (legendScrollView?.scrollX!! >= horizontalScrollAmount - scrollDelta) {
View.INVISIBLE
} else {
View.VISIBLE
}
scrollLeftButton?.visibility =
if (legendScrollView?.scrollX!! <= scrollDelta) {
View.INVISIBLE
} else {
View.VISIBLE
}
}
}
MapPageFragment:
class MapPageFragment : BaseFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_stations_map, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
appComponent.inject(this)
showLegendButton.setOnClickListener {
showLegendButton.hide()
mapLegend.setVisible()
}
mapLegend.onCloseButtonClickedListener = {
showLegendButton.show()
mapLegend.setGone()
}
}
override fun onDestroyView() {
super.onDestroyView()
mapLegend.onDetachedFromWindow()
}
}
And a heap dump data:
┬───
│ GC Root: Input or output parameters in native code
│
├─ com.yandex.runtime.view.internal.GLTextureView$RenderThread instance
│ Leaking: NO (PlatformGLTextureView↓ is not leaking)
│ Thread name: 'Thread-24'
│ ↓ GLTextureView$RenderThread.this$0
├─ com.yandex.runtime.view.PlatformGLTextureView instance
│ Leaking: NO (View attached)
│ View is part of a window view hierarchy
│ View.mAttachInfo is not null (view attached)
│ View.mWindowAttachCount = 1
│ mContext instance of com.example.android.ui.main.MainActivity with
│ mDestroyed = false
│ ↓ View.mAttachInfo
│ ~~~~~~~~~~~
├─ android.view.View$AttachInfo instance
│ Leaking: UNKNOWN
│ Retaining 864.5 kB in 14847 objects
│ ↓ View$AttachInfo.mTreeObserver
│ ~~~~~~~~~~~~~
├─ android.view.ViewTreeObserver instance
│ Leaking: UNKNOWN
│ Retaining 863.4 kB in 14813 objects
│ ↓ ViewTreeObserver.mOnGlobalLayoutListeners
│ ~~~~~~~~~~~~~~~~~~~~~~
├─ android.view.ViewTreeObserver$CopyOnWriteArray instance
│ Leaking: UNKNOWN
│ Retaining 145 B in 7 objects
│ ↓ ViewTreeObserver$CopyOnWriteArray.mData
│ ~~~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ Retaining 108 B in 5 objects
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 88 B in 4 objects
│ ↓ Object[].[1]
│ ~~~
├─ com.example.android.core.view.MapLegendView$onAttachedToWindow$1 instance
│ Leaking: UNKNOWN
│ Retaining 16 B in 1 objects
│ Anonymous class implementing android.view.
│ ViewTreeObserver$OnGlobalLayoutListener
│ ↓ MapLegendView$onAttachedToWindow$1.this$0
│ ~~~~
├─ com.example.android.core.view.MapLegendView instance
│ Leaking: UNKNOWN
│ Retaining 423.5 kB in 6520 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.mapLegend
│ View.mWindowAttachCount = 1
│ mContext instance of com.example.android.ui.main.MainActivity with
│ mDestroyed = false
│ ↓ View.mParent
│ ~~~~~
╰→ android.widget.FrameLayout instance
Leaking: YES (ObjectWatcher was watching this because com.example.
android.ui.main.stations.StationsMapPageFragment received
Fragment#onDestroyView() callback (references to its views should be
cleared to prevent leaks))
Retaining 374.6 kB in 6021 objects
key = 431af589-b15f-427d-8f65-9121904807bf
watchDurationMillis = 10811
retainedDurationMillis = 5689
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
mContext instance of com.example.android.ui.main.MainActivity with
mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: Google
LeakCanary version: 2.6
App process name: com.example.android.debug
Stats: LruCache[maxSize=3000,hits=6360,misses=77615,hitRate=7%]
RandomAccess[bytes=4045939,reads=77615,travel=23706579087,range=18083691,size=23
068377]
Heap dump reason: user request
Analysis duration: 57078 ms
As you can see I've tried to set Views references to null and also calling onDetachedFromWindow() method in the Fragment, but it still gives me leaks :(
Also I've attempted usage of WeakReference on a context in a view file, but it didn't change anything either.
If someone else is wondering too, as it was said in a comments section, I should have removed listeners right in the OnDetachedFromWindow() method(before the super call of it!!). Also I've cleared onClickListeners in necessary fragments and called this method for my custom view in OnDestroyView(), so now it looks like:
override fun onDestroyView() {
mapLegend.setOnClickListener(null)
mapLegend.onDetachedFromWindow()
super.onDestroyView()
}
Hope it'll help