7

I have a piece of simple code below in an activity...

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

            }
        });
        valueAnimator.start();
    }
}

If the activity got terminated, there will be memory leak (as proven by Leak Canary).

However, when I covert this code to identical Kotlin code (using shift-alt-command-k), it is as below

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
        valueAnimator.repeatCount = ValueAnimator.INFINITE
        valueAnimator.addUpdateListener { }
        valueAnimator.start()
    }
}

Memory leak no longer happen. Why? Is it because the anonymous class object got converted to Lambda?

Elye
  • 53,639
  • 54
  • 212
  • 474
  • lambda expressions are just simplified syntactical way of declaring Anonymous classes, so definitely that won't be the reason and also LeakCanary uses RefWatcher and ReferenceQueue to calculate leaks using byte code, as kotlin and java has same byte code . I'm not sure why it happened . Curious to find out – Rajan Kali Jan 08 '18 at 12:25
  • Are you sure that the leaks aren't temperamental, and so irrelevant whether it is Kotlin or Java? I'm not convinced, but I've given an answer presuming that it is due to Kotlin anyway – Alicia Sykes Jan 08 '18 at 12:28
  • If you give a pastebin of some of the Byte code, or give a link to your repository, then I'll look into it further :) – Alicia Sykes Jan 08 '18 at 12:28
  • I'd be curious if `valueAnimator.addUpdateListener(animation -> {})` leaks as well in Java. – tynn Jan 08 '18 at 14:52

3 Answers3

7

The difference between these 2 versions is pretty simple.

Java version of the AnimatorUpdateListener contains implicit reference to the outer class (MainActivity in your case). So, if the animation keeps running when the activity is not needed anymore, the listener keeps holding the reference to the activity, preventing it from being garbage-collected.

Kotlin tries to be more clever here. It sees that the lambda which you pass to the ValueAnimator does not reference any objects from the outer scope (i.e. MainActivity), so it creates a single instance of AnimatorUpdateListener which will be reused whenever you [re]start the animation. And this instance does not have any implicit references to the outer scope.

Side note: if you add the reference to some object from outer scope to your lambda, Kotlin will generate the code which creates a new instance of the update listener every time the animation is [re]started, and these instances will be holding the implicit references to MainActivity (required in order to access the object(s) which you decide to use in your lambda).

Another side note: I strongly recommend to read the book called "Kotlin in Action" as it contains lots of useful information on Kotlin in general, and my explanation of how Kotlin compiler make a choice about whether to put the implicit reference to outer scope into the object created after SAM conversion or not comes from this book.

Valentin Michalak
  • 2,089
  • 1
  • 14
  • 27
aga
  • 27,954
  • 13
  • 86
  • 121
2

1. Check what's actually going on

I think you'll find the “Show Kotlin Bytecode” view very helpfull in seeing exactly what is going on. See here for the InteliJ shortcut. (Would have done this for you, but hard without greater context of your application)

2. JVM similarities, but Kotlin explicit differences

But since Kotlin runs on the same JVM as Java (and so uses the same garbage collector as Java) you should expect a similarly safe runtime environment. That being said, when it comes to Lamdas and explicit references, when they're converted. And it can vary in different cases:

Like in Java, what happens in Kotlin varies in different cases.

  • If the lambda is passed to an inline function and isn’t marked noinline, then the whole thing boils away and no additional classes
    or objects are created.
  • If the lambda doesn’t capture, then it’ll be emitted as a singleton class whose instance is reused again and again (one class+one object allocation).
  • If the lambda captures then a new object is created each time the lambda is used.

Source: http://openjdk.java.net/jeps/8158765

3. Summary, and further reading

This answer should explain what your seeing, couldn't have explained it better myself: https://stackoverflow.com/a/42272484/979052 Different question, I know, but the theory behind it is the same - hope that helps

Alicia Sykes
  • 5,997
  • 7
  • 36
  • 64
1

As you already suggested, the argument to addUpdateListener is actually different in both versions.

Let's see an example. I've created a class JavaAbstract with a single abstract method foo:

public interface JavaInterface {
     void foo();
}

This is used in JavaInterfaceClient:

public class JavaInterfaceClient {
    public void useInterfaceInstance(JavaAbstract inst){
        inst.foo();
    }
}

Let's see how we can call useInterfaceInstance from Kotlin:

First, with a simple lambda as in your example (SAM Conversion):

JavaInterfaceClient().useInterfaceInstance {}

The resulting bytecode represented in Java:

(new JavaInterfaceClient()).useInterfaceInstance((JavaInterface)null.INSTANCE);

As you can see, very simple, no object instantiations.

Second, with an anonymous instance:

JavaInterfaceClient().useInterfaceInstance(object : JavaInterface {
    override fun foo() {

    }
})

The resulting bytecode represented in Java:

(new JavaInterfaceClient()).useInterfaceInstance((JavaInterface)(new JavaInterface() {
     public void foo() {
     }
 }));

Here we can observe new object instantiations, which defers from the SAM conversion / lambda approach. You should try the second example in your code.

s1m0nw1
  • 76,759
  • 17
  • 167
  • 196