Android TextView设置setMovementMethod方法会导致内存泄漏问题。

7
我有一个ListView,它的适配器中的getView方法返回一个RelativeLayout,里面包含了MyButton。
MyButton有一个textView,我在其中放置了可点击的单词(ClickableSpan)。
为了使这个工作起来,我从以下一行开始: textView.setMovementMethod(LinkMovementMethod.getInstance());
一切都可以正常工作,但MAT显示MyButton泄漏,因为textView。当我注释掉上面的那一行时,就没有泄漏了。
我应该将movementMethod设置为null吗?但即使如此,我也无法知道按钮销毁的时刻,以便将其设置为null,因为它在许多其他视图内部。
我做错了什么?如何预防这种泄漏?
更新:
通过在onDetachedFromWindow中将文本设置为空字符串解决了泄漏问题,但我仍在尝试找到与此行为相关的文档。为什么我要将textview设置为空字符串?

1
尝试使用 View.onDetachedFromWindow()。 - pskink
@pskink 谢谢,将 movementMethod 设置为 null 没有起作用,但将文本设置为 "" 确实起了作用(在 onDetachedFromWindow 中)。如果您也知道这个泄漏的原因,请发布一个答案,这样我就可以将其标记为已接受的答案。我仍然很好奇为什么会发生这种泄漏,没有与此行为相关的文档。 - frankish
1
我一直在使用LeakCanary来追踪内存泄漏,最终发现TextView.setMovementMethod()是罪魁祸首。不幸的是,将移动方法设置为null并在onDetachedFromWindow()中将文本设置为""并没有解决问题。LeakCanary显示它与ViewTreeObserver没有清除preDraw监听器有关。考虑到TextView实现了OnPreDrawListener,我想知道它是否在底层执行了某些奇怪的操作? - Chris Horner
5个回答

8
我在创建片段内的超链接时,遇到了与 TextView、ClickableSpan 和 LinkMovementMethod 相关的另一个内存泄漏问题。在第一次点击超链接并旋转设备后,由于 NPE 的原因再也无法点击它。
为了弄清楚发生了什么,我进行了调查,以下是结果。
TextView 在 onSaveInstanceState() 中将包含 ClickableSpan 的字段 mText 的副本保存到静态内部类 SavedState 的实例中。这只会在特定条件下发生。在我的情况下,它是可点击部分的 Selection,在单击跨度后由 LinkMovementMethod 设置。
接下来,如果有已保存的状态,则 TextView 将在 onRestoreInstanceState() 期间从 TextView.SavedState.text 中恢复字段 mText,包括所有跨度。
这里有一个有趣的部分。什么时候调用onRestoreInstanceState()?它在onStart()之后被调用。我在onCreateView()中设置了一个新的ClickableSpan对象,但是在onStart()之后,旧对象替换了新对象,导致了大问题。
因此,解决方案非常简单,但没有文档 - 在onStart()期间执行ClickableSpan的设置。
您可以在我的博客TextView,ClickableSpan和内存泄漏上阅读完整的调查,并使用示例项目进行测试。

谢谢解释,我已经尝试过了,似乎问题解决了。通过创建继承自ClickableSpan()和NoCopySpan的自定义类,问题消失了。 - Thân Hoàng

3

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

class NoCopyClickableSpan(
    private val callback: () -> Unit
) : ClickableSpan(), NoCopySpan {

    override fun onClick(view: View) {
        callback()
    }
}

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


我不得不在我提出这个问题的那一天继续使用我的临时解决方案。很快我会再次尝试这个。谢谢你的分享。 - frankish
添加了NoCopySpan,不再有泄漏问题。感谢您提供的解决方案。 - calvert
2
在此修复后,当显示具有这种跨度的视图并且辅助功能服务已打开时,我开始遇到应用程序崩溃。 在三星设备上进行了检查。 为历史目的提及此事,因为在网络上没有任何此类崩溃。 - Lingviston
@Lingviston 感谢您的回复!由于这些崩溃,我最终也撤销了修复。我会编辑我的答案。 - mol

2
您的问题很可能是由于NoCopySpan引起的。在KitKat之前,TextView会复制该span并将其放置在一个Bundle中,在onSaveInstanceState()中使用SpannableString。出于某种原因,SpannableString不会删除NoCopySpans,因此保存的状态保留对原始TextView的引用。这在随后的版本中已经得到了修复
将文本设置为“”可以解决该问题,因为包含NoCopySpan的原始文本被妥善地GC'd。 LeakCanary建议的解决方法是...

黑科技:要解决这个问题,您可以重写TextView.onSaveInstanceState(),然后使用反射访问TextView.SavedState.mText并清除NoCopySpan spans。

这个泄漏的LeakCanary排除条目可以在这里找到。

1
我也可以在Android 5.1.1上重现这个问题。 - Alex

2

在尝试了几个答案之后,我自己想出了一个最终有效的方法。

我不确定这有多准确,也不明白为什么会这样,但事实证明,在onDestroy()中将我的TextViewmovementMethod设置为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之前可能会出现问题。


1

尝试在 onStart() 方法中初始化 ClickableSpan。例如:

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

在一些Android版本上,Span存在问题。有时会导致内存泄漏。更多信息请参见此文章 TextView,ClickableSpan和内存泄漏

我希望这能有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接