如何解决自定义视图中的内存泄漏?

How to resolve a memory leak in a custom view?

我是修复内存泄漏的新手,我真的不明白应该如何删除它们,尤其是在自定义视图中。在这种特殊情况下,我有一个自定义 MapLegendView,它正在 MapPageFragment 中使用。

地图图例视图代码:

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[=12=]
├─ 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 instance
│    Leaking: UNKNOWN
│    Retaining 16 B in 1 objects
│    Anonymous class implementing android.view.
│    ViewTreeObserver$OnGlobalLayoutListener
│    ↓ MapLegendView$onAttachedToWindow.this[=12=]
│                                         ~~~~
├─ 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 并还在 Fragment 中调用 onDetachedFromWindow() 方法,但它仍然给我泄漏:(

我还尝试在视图文件的上下文中使用 Wea​​kReference,但它也没有改变任何东西。

如果其他人也想知道,正如评论部分所说,我应该在 OnDetachedFromWindow() 方法中删除侦听器(在它的超级调用之前!!)。我还清除了必要片段中的 onClickListeners 并在 OnDestroyView() 中为我的自定义视图调用了此方法,所以现在它看起来像:

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

        super.onDestroyView()
    }

希望对您有所帮助