Android TextView leaks with setMovementMethod
Asked Answered
N

5

7

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 ""?

Nelle answered 16/2, 2015 at 10:23 Comment(3)
try View.onDetachedFromWindow()Threedecker
@Threedecker 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.Nelle
I've been using 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?Tanika
D
8

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.

Desmid answered 13/1, 2018 at 17:49 Comment(1)
Thanks for the explanation, i've tried but it seem happens. By creating custom class extends from ClickableSpan(), NoCopySpan the issue is goneNaturalistic
M
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.

Montague answered 8/11, 2018 at 6:27 Comment(4)
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.Nelle
Added NoCopySpan, no more leaks. Thank you for the solutionForte
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.Sarracenia
@Sarracenia Thanks for the response! I ended up reverting the fix too due to these crashes. I'll edit my answer.Montague
T
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.

Tanika answered 10/6, 2015 at 0:35 Comment(1)
I can reproduce this on Android 5.1.1 too.Dara
L
2

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.

Lajoie answered 16/1, 2019 at 8:9 Comment(0)
V
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.

Venusian answered 6/10, 2018 at 21:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.