如何解决自定义视图中的内存泄漏?
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() 方法,但它仍然给我泄漏:(
我还尝试在视图文件的上下文中使用 WeakReference,但它也没有改变任何东西。
如果其他人也想知道,正如评论部分所说,我应该在 OnDetachedFromWindow() 方法中删除侦听器(在它的超级调用之前!!)。我还清除了必要片段中的 onClickListeners 并在 OnDestroyView() 中为我的自定义视图调用了此方法,所以现在它看起来像:
override fun onDestroyView() {
mapLegend.setOnClickListener(null)
mapLegend.onDetachedFromWindow()
super.onDestroyView()
}
希望对您有所帮助
我是修复内存泄漏的新手,我真的不明白应该如何删除它们,尤其是在自定义视图中。在这种特殊情况下,我有一个自定义 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() 方法,但它仍然给我泄漏:(
我还尝试在视图文件的上下文中使用 WeakReference,但它也没有改变任何东西。
如果其他人也想知道,正如评论部分所说,我应该在 OnDetachedFromWindow() 方法中删除侦听器(在它的超级调用之前!!)。我还清除了必要片段中的 onClickListeners 并在 OnDestroyView() 中为我的自定义视图调用了此方法,所以现在它看起来像:
override fun onDestroyView() {
mapLegend.setOnClickListener(null)
mapLegend.onDetachedFromWindow()
super.onDestroyView()
}
希望对您有所帮助