Как устранить утечку памяти в пользовательском представлении?

Я новичок в устранении утечек памяти и не очень понимаю, как их удалять, особенно в пользовательских представлениях. В этом конкретном случае у меня есть собственный MapLegendView, который используется в MapPageFragment.

Код MapLegendView:

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()
    }
}

И данные дампа кучи:

┬───
│ 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

Как видите, я пытался установить для ссылок Views значение null, а также вызвать метод onDetachedFromWindow() во фрагменте, но он все равно дает мне утечки :(

Также я попытался использовать WeakReference в контексте в файле представления, но это тоже ничего не изменило.


person ladamengh    schedule 28.02.2021    source источник
comment
Вы получаете такой же результат при запуске на чем-то другом, кроме Яндекс-устройства?   -  person CommonsWare    schedule 01.03.2021
comment
Это не яндекс-устройство, в этом фрагменте есть карта Яндекса, я просто удалил ее из кода для этого вопроса.   -  person ladamengh    schedule 01.03.2021
comment
Итак, похоже, вам нужно вызвать removeOnGlobalLayoutListener(), чтобы отменить действие вашего вызова addOnGlobalLayoutListener().   -  person CommonsWare    schedule 01.03.2021


Ответы (1)


Если кому-то тоже интересно, как было сказано в разделе комментариев, я должен был удалить слушателей прямо в методе OnDetachedFromWindow() (до его супервызова!!). Также я очистил onClickListeners в необходимых фрагментах и ​​вызвал этот метод для своего пользовательского представления в OnDestroyView(), так что теперь это выглядит так:

override fun onDestroyView() {
        mapLegend.setOnClickListener(null)
        mapLegend.onDetachedFromWindow()

        super.onDestroyView()
    }

Надеюсь, это поможет

person ladamengh    schedule 04.03.2021