Search code examples
androidkotlinmemory-managementmemory-leakstextwatcher

Memory leak in kotlin due to Closures?


I have a basic understanding of memory management in Kotlin i.e. local variables are stored in the stack and are managed by the OS and the objects are created on the heap and JVM manages them. Moreover, objects are passed as copy of reference and primitive types are passed as copy in functions.

In the below code, I have created a TextEditOperation function that creates an EditText programmatically and sets its properties using an instruction object and added a TextWatcher to it and at last added the edittext view to parent relative layout.

public fun TextEditOperation (pInstructionSetIndex: Int, pInstructionIndex: Int): View? 
{
    val instruction: InstructionKernelAndroid
    val context: Context?
    val activity: Activity
    val textedit: EditText
    val parent_view: RelativeLayout

    // fetch the instruction
    instruction = InstructionSetMgr.uAndroidOSInstructionSetList[pInstructionSetIndex][pInstructionIndex]

    // get activity's Context
    context = ProcessStates.GetMainActivityContext ()

    // cast activity's context to activity to use it's method
    activity = context as Activity

    // get parent view
    parent_view = activity.findViewById (instruction.GetParentID ()) as RelativeLayout

    // create view
    textedit = EditText (context)

    // set view properties

    textedit.id = instruction.GetElementID ()
    textedit.hint = instruction.GetHintText ()
    textedit.layoutParams = RelativeLayout.LayoutParams (
        instruction.GetWidth (),
        instruction.GetHeight ()
    )
    textedit.x = instruction.GetX ()
    textedit.y = instruction.GetY ()
    textedit.setBackgroundColor (Color.parseColor (instruction.GetBackgroundColor ()))
    textedit.setTextColor (Color.parseColor (instruction.GetTextColor ()))
    
    // create and add a TextWatcher to the EditText

        val text_watcher: TextWatcher

    text_watcher = object : TextWatcher {
    
        override fun beforeTextChanged (s: CharSequence, start: Int, count: Int, after: Int)
        {
            
        }
    
        override fun onTextChanged (s: CharSequence, start: Int, before: Int, count: Int) 
        {
            TextChanged (pInstructionSetIndex + 1, textedit.length (), textedit.id)
        }
    
        override fun afterTextChanged (s: Editable)
        {
            
        }
    } 
    // add the textwatcher
    textedit.addTextChangedListener (text_watcher)
    
    // add view to parent view
    parent_view.addView (textedit);

    return textedit;  
}

Now, what I understand is if the text_watcher object wouldn't have been there, then for line

textedit = EditText (context)

EditText(context) object would have been created on the heap(managed by JVM) and variable textedit (managed by OS) refering the object would have been on stack. So the textedit variable would have been cleared from the stack memory by the OS once the function scope ends.

However, now as I am using the textedit variable inside the OnTextChanged function of the text_watcher:

override fun onTextChanged (s: CharSequence, start: Int, before: Int, count: Int) 
{
     TextChanged (pInstructionSetIndex + 1, textedit.length (), textedit.id)
}

And the onTextChanged function will get called much after the TextEditOperation's function scope has ended still How I'm able to access the length and id of the EditText using textedit variable which should have been removed by the OS by now?

Is it because of closures in kotlin, than I'm able to access the varible that's present in the outer scope and hence it not getting free by the OS thus being a memory leak, if yes how to fix it or is there any other reason behind it?


Solution

  • Disclaimer: This answer focuses on Kotlin when targeting the JVM. The general concepts should be applicable to other platforms, but the specifics are likely to differ.


    Closures & Variable Capturing

    When a closure makes use of a variable from the enclosing scope, that closure "captures" the value of the variable at the time the closure is created.

    In Kotlin, this is implemented by implicitly adding a class property to the class generated for the closure object. The property is initialized with the value of the captured variable during instantiation of the closure. Within the closure's scope, any time it looks like you are using the captured variable you are actually using the property. All this is done for you by the compiler behind the scenes.

    So, using textedit inside your TextWatcher implementation is perfectly okay.

    • The value of that variable is captured when the TextWatcher is created, not when the variable is first "used" in the implementation.

    • All references to textedit within the scope of the TextWatcher implementation are actually referencing a hidden, implicitly defined property of said implementation. Meaning there is no attempt to access a local variable after it has been popped off the stack.

    Note that when a closure references a class property of its enclosing class, the closure actually captures the instance of the enclosing class (i.e., this) instead of the value of the property directly. Also, unlike Java, Kotlin is capable of capturing mutable local variables (i.e., var), which it accomplishes by using a hidden "wrapper class" that wraps the actual value, then captures that wrapper object instead.

    Conceptual Example

    Here is some code demonstrating the concept of what the compiler is doing. This is not necessarily exactly how variable capturing is implemented, but it should hopefully help with understanding the idea.

    If you have something like this:

    interface Printer {
        fun print()
    }
    
    fun createPrinter(): Printer {
        val message = "Hello, World" // local variable
        return object : Printer {
            override fun print() = println(message)
        }
    }
    

    After compilation, it would be as if you wrote the following:

    interface Printer {
        fun print()
    }
    
    fun createPrinter() : Printer {
        val message = "Hello, World" // local variable
        class AnonymousPrinterImpl(
            private val message: String // class property
        ): Printer {
            // Here, 'message' is referring to the class property,
            // not the local variable.
            override fun print() = println(message)
        }
        return AnonymousPrinterImpl(message)
    }
    

    If you want to see what the compiler actually generates then you can inspect the Java byte-code via the javap tool. Note that the implementation for an anonymous object is different than for a functional interface or lambda expression, but the end result is the same: A class property is implicitly added to the closure's class.


    Memory Management

    In Kotlin, heap memory is managed by a garbage collector. At a high level, the way a garbage collector works is by periodically traversing the current object graph, starting at designated root objects. Any object that cannot be reached from those root objects is eligible for garbage collection. In languages with a garbage collector, a "memory leak" occurs when an object remains strongly referenced after the program no longer needs it. The strong reference prevents the unneeded object from being collected by the garbage collector.

    You seem to be worried the TextWatcher is erroneously keeping the TextEdit object in memory after your function returns. But you are returning the TextEdit object from the function and adding it to some "parent view". Both those things strongly indicate the TextEdit object is still needed by your program after the function returns.

    Additionally, the only strong reference to the TextWatcher after your function returns is by the TextEdit. In this case, it is not the TextWatcher keeping the TextEdit in memory, but rather the other way around. When the TextEdit becomes eligible for garbage collection, so will the TextWatcher. The circular reference between the two does not change that. 

    In short, there is no memory leak in your code; or at least not one caused by the TextWatcher based on what you have shown us.