5

I have a ListView and in it's adapter's getView method, I return a RelativeLayout with MyButton inside it.

MyButton has a textView and I have clickable words inside it (ClickableSpan).

To make this work, I start with thew following line: textView.setMovementMethod(LinkMovementMethod.getInstance());

Everything works perfectly but MAT shows that MyButton leaks because of textView. When I comment out the line above, nothing leaks.

Shall I set movementMethod to null? But even if so, I can't know the destruction moment of the button to set that to null as it is inside of many other views.

What am I doing wrong? How to prevent this leak?

enter image description here

update

Solved the leak by setting text to empty string inside onDetachedFromWindow, but I am still trying to find a documentation related to this behaviour. Why should I set the textview to ""?

frankish
  • 6,347
  • 5
  • 43
  • 93
  • 1
    try View.onDetachedFromWindow() – pskink Feb 16 '15 at 10:34
  • @pskink Thank you, setting `movementMethod` to `null` did not work but setting text to `""` did work. (in onDetachedFromWindow). If you also know the reason of this leak, please post as an answer so I can mark it as accepted answer. I am still curious why this leak happens. There are no documentation related to this behaviour. – frankish Feb 16 '15 at 10:42
  • I've been using [LeakCanary](https://github.com/square/leakcanary) to track down a memory leak, and ultimately found `TextView.setMovementMethod()` was the culprit. Unfortunately for me, setting the movement method to null and the text to "" in `onDetachedFromWindow()` hasn't solved the problem. LeakCanary shows that it has something to do with `ViewTreeObserver` not having a preDraw listener cleared. Given that `TextView` implements `OnPreDrawListener`, I wonder if that's doing something funky under the hood? – Chris Horner Jun 10 '15 at 00:11

5 Answers5

5

I faced another memory leak with TextView, ClickableSpan, and LinkMovementMethod while making hyperlinks inside a Fragment. After the first click on the hyperlink and rotation of the device, it was impossible to click it again due to NPE.

In order to figure out what's going on, I made an investigation and here is the result.

TextView saves a copy of the field mText, that contains ClickableSpan, during the onSaveInstanceState() into the instance of static inner class SavedState. It happens only under certain conditions. In my case, it was a Selection for the clickable part, which is set by LinkMovementMethod after the first click on the span.

Next, if there is a saved state, TextView performs restoration for the field mText, including all spans, from TextView.SavedState.text during onRestoreInstanceState().

Here is a funny part. When onRestoreInstanceState() is called? It’s called after onStart(). I set a new object of ClickableSpan in onCreateView() but after onStart() the old object replaces new one which leads to the big problems.

So, the solution is quite simple but is not documented – perform setup of ClickableSpan during onStart().

You can read the full investigation on my blog TextView, ClickableSpan and memory leak and play with the sample project.

3

Using ClickableSpan may still cause leaks even on versions higher than KitKat. If you look into implementation of the ClickableSpan you will notice that it doesn't extend NoCopySpan, so it leaks in onSaveInstanceState() like described in @DmitryKorobeinikov and @ChrisHorner answers. So the solution would be to create a custom class that extends ClickableSpan and NoCopySpan.

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

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

EDIT It turned out that this fix leads to crashes on some devices when Accessibility services are enabled.

mol
  • 2,348
  • 4
  • 18
  • 34
  • I had to move on with my dirty solution in the day I asked this question. Soon I will try this one again. Thank you for sharing this. – frankish Nov 15 '18 at 13:50
  • Added `NoCopySpan`, no more leaks. Thank you for the solution – calvert Aug 19 '19 at 17:56
  • 2
    After this fix I start getting app crashes when view with such span is shown and accessability service is on. Checked on Samsung devices. Mentioning this for historical purpose as there's no such crash anywhere in the web. – Lingviston Nov 20 '19 at 14:26
  • @Lingviston Thanks for the response! I ended up reverting the fix too due to these crashes. I'll edit my answer. – mol Nov 25 '19 at 07:55
2

Your issue is most likely caused by NoCopySpan. Prior to KitKat, TextView would make a copy of the span and place it in a Bundle in onSaveInstanceState() using a SpannableString. SpannableString does not drop NoCopySpans for some reason, so the saved state holds a reference to the original TextView. This was fixed for subsequent releases.

Setting the text to "" fixes the issue because the original text containing the NoCopySpan is GC'd properly.

LeakCanary's suggested work around for this is...

Hack: to fix this, you could override TextView.onSaveInstanceState(), and then use reflection to access TextView.SavedState.mText and clear the NoCopySpan spans.

LeakCanary's exclusion entry for this leak can be found here.

Chris Horner
  • 1,806
  • 2
  • 13
  • 22
1

Try to initialize the ClickableSpan in onStart() method.Like

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

There is problem with Span on some Android versions. Sometimes it causes memory leaks. More info in this article TextView, ClickableSpan and memory leak

I hope it will help.

Djek-Grif
  • 1,071
  • 9
  • 16
1

After spending a few hours trying these answers out I came up with my own that finally worked.

I'm not sure how accurate this is and don't understand why this is but it turned out that setting my TextView's movementMethod to null in onDestroy() solved the problem.

If anyone knows why please tell me. I'm so boggled because it doesn't seem like LinkMovementMethod.getInstance() has a reference to the TextView or the activity.

Here's the code

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

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

It worked without setting text_view.text = "" but I kept it their because of @Chris Horner answer that there might be a problem prior to KitKat.

조일현
  • 39
  • 1
  • 3