【问题标题】:Android TextView leaks with setMovementMethodAndroid TextView 使用 setMovementMethod 泄漏
【发布时间】:2025-12-20 02:20:22
【问题描述】:

我有一个ListView,在它的adaptergetView 方法中,我返回一个RelativeLayout,里面有MyButton

MyButton 有一个textView,里面有可点击的字词 (ClickableSpan)。

为了完成这项工作,我从以下行开始: textView.setMovementMethod(LinkMovementMethod.getInstance());

一切正常,但 MAT 显示 MyButton 泄漏是因为 textView。当我注释掉上面的行时,没有任何泄漏。

我应该将movementMethod 设置为null 吗?但即使是这样,我也无法知道将其设置为 null 的按钮的破坏时刻,因为它位于许多其他视图中。

我做错了什么?如何防止这种泄漏?

更新

通过在onDetachedFromWindow 中将文本设置为空字符串解决了泄漏问题,但我仍在尝试查找与此行为相关的文档。为什么要将textview 设置为""

【问题讨论】:

  • 试试 View.onDetachedFromWindow()
  • @pskink 谢谢,将movementMethod 设置为null 不起作用,但将文本设置为"" 确实有效。 (在 onDetachedFromWindow 中)。如果您也知道此泄漏的原因,请将其作为答案发布,以便我将其标记为已接受的答案。我仍然很好奇为什么会发生这种泄漏。没有与此行为相关的文档。
  • 我一直在使用LeakCanary 来追踪内存泄漏,最终发现TextView.setMovementMethod() 是罪魁祸首。对我来说不幸的是,将移动方法设置为 null 并将 onDetachedFromWindow() 中的文本设置为 "" 并没有解决问题。 LeakCanary 表明它与 ViewTreeObserver 没有清除 preDraw 侦听器有关。鉴于TextView 实现了OnPreDrawListener,我想知道这是否在做一些时髦的事情?

标签: android listview memory-leaks textview linkmovementmethod


【解决方案1】:

Fragment 中创建超链接时,我遇到了TextViewClickableSpanLinkMovementMethod 的另一个内存泄漏。第一次点击设备的超链接和旋转后,由于NPE,无法再次点击。

为了弄清楚发生了什么,我进行了调查,结果如下。

TextViewonSaveInstanceState() 期间将包含ClickableSpan 的字段mText 保存到静态内部类SavedState 的实例中。它仅在某些条件下发生。在我的例子中,可点击部分是Selection,在第一次点击跨度后由LinkMovementMethod设置。

接下来,如果有保存状态,TextViewonRestoreInstanceState() 期间从TextView.SavedState.text 对字段mText 执行恢复,包括所有跨度。

这是一个有趣的部分。何时调用onRestoreInstanceState()?它在onStart() 之后调用。我在onCreateView() 中设置了一个ClickableSpan 的新对象,但是在onStart() 之后,旧对象替换了新对象,这导致了大问题。

因此,解决方案非常简单,但没有记录在案——在onStart() 期间执行ClickableSpan 的设置。

您可以在我的博客TextView, ClickableSpan and memory leak 上阅读完整的调查,并与sample project 一起玩。

【讨论】:

    【解决方案2】:

    即使在高于KitKat 的版本上,使用ClickableSpan 仍可能导致泄漏。如果您查看ClickableSpan 的实现,您会注意到它没有扩展NoCopySpan,因此它在onSaveInstanceState() 中泄漏,就像@DmitryKorobeinikov 和@ChrisHorner 的答案中所述。所以解决方案是创建一个扩展 ClickableSpanNoCopySpan 的自定义类。

    class NoCopyClickableSpan(
        private val callback: () -> Unit
    ) : ClickableSpan(), NoCopySpan {
    
        override fun onClick(view: View) {
            callback()
        }
    }
    

    编辑 事实证明,当启用辅助功能服务时,此修复程序会导致某些设备崩溃。

    【讨论】:

    • 在我问这个问题的那天,我不得不继续我的肮脏解决方案。很快我会再试一次。谢谢你分享这个。
    • 添加了NoCopySpan,不再有泄漏。感谢您的解决方案
    • 在此修复后,当显示具有此类跨度的视图并且可访问性服务开启时,我开始收到应用程序崩溃。检查三星设备。出于历史目的提及这一点,因为网络上没有这样的崩溃。
    • @Lingviston 感谢您的回复!由于这些崩溃,我最终也恢复了修复。我将编辑我的答案。
    【解决方案3】:

    您的问题很可能是由NoCopySpan 引起的。在 KitKat 之前,TextView 会复制 span 并使用 SpannableString 将其放在 onSaveInstanceState() 的 Bundle 中。 SpannableString 出于某种原因不会删除 NoCopySpans,因此保存的状态包含对原始 TextView 的引用。这是fixed 用于后续版本。

    将文本设置为 "" 可解决问题,因为包含 NoCopySpan 的原始文本已正确 GC。

    LeakCanary 建议的解决方法是...

    Hack:要解决此问题,您可以覆盖 TextView.onSaveInstanceState(),然后使用反射访问 TextView.SavedState.mText 并清除 NoCopySpan 跨度。

    LeakCanary 对此泄漏的排除条目可以是found here

    【讨论】:

    • 我也可以在 Android 5.1.1 上重现这个。
    【解决方案4】:

    在花了几个小时尝试这些答案后,我想出了自己的答案,最终奏效了。

    我不确定这有多准确,也不明白为什么会这样,但事实证明在onDestroy() 中将我的TextView 的movementMethod 设置为null 解决了这个问题。

    如果有人知道为什么,请告诉我。我很困惑,因为 LinkMovementMethod.getInstance() 似乎没有对 TextView 或活动的引用。

    这是代码

    override fun onStart() {
        ...
        text_view.text = spanString
        text_view.movementMethod = LinkMovementMethod
    } 
    
    override fun onDestroy() {
        text_view.text = ""
        text_view.movementMethod = null
    }
    

    它在没有设置 text_view.text = "" 的情况下工作,但我保留了它,因为 @Chris Horner 回答说在 KitKat 之前可能存在问题。

    【讨论】:

      【解决方案5】:

      尝试在onStart()method.Like中初始化ClickableSpan

      onStart(){
      super.onStart()
      someTextView.setText(buildSpan());
      }
      

      在某些 Android 版本上,Span 存在问题。有时它会导致内存泄漏。本文中的更多信息TextView, ClickableSpan and memory leak

      我希望它会有所帮助。

      【讨论】: