Java中的内存泄漏,但Kotlin(相同的代码库)没有...为什么?

时间:2018-01-08 12:02:05

标签: android memory-leaks kotlin

我在活动中有一段简单的代码......

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();
    }
}

如果活动终止,将会有内存泄漏(Leak Canary证实)。

然而,当我将此代码转换为相同的Kotlin代码(使用shift-alt-command-k)时,它如下所示

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()
    }
}

内存泄漏不再发生。为什么?是因为匿名类对象被转换为Lambda?

3 个答案:

答案 0 :(得分:6)

这两个版本之间的区别非常简单。

AnimatorUpdateListener的Java版本包含对外部类的隐式引用(在您的情况下为MainActivity)。因此,如果动画在不再需要活动时继续运行,则侦听器会持续保持对活动的引用,从而防止对其进行垃圾回收。

Kotlin试图在这里变得更聪明。它看到传递给ValueAnimator的lambda不引用外部作用域中的任何对象(即MainActivity),因此它创建了一个单个实例 {{3}每次你重新开始动画时都会重复使用它。并且此实例没有对外部作用域的任何隐式引用。

旁注:如果您将对外部作用域的某个对象的引用添加到lambda,Kotlin将生成代码,每次动画[re]时都会创建更新侦听器的新实例已启动,并且这些实例将保留对MainActivity的隐式引用(为了访问您决定在lambda中使用的对象而需要)。

另一方面注意事项:我强烈建议您阅读名为" Kotlin in Action"因为它包含了很多关于Kotlin的有用信息,并且我对Kotlin编译器如何选择是否将外部作用域的隐式引用放入SAM转换后创建的对象中的解释来自本书。

答案 1 :(得分:2)

1。检查实际发生的事情

我认为你会发现“Show Kotlin Bytecode”视图非常有助于确切了解发生了什么。见here for the InteliJ shortcut(本来会为你做这件事,但如果没有更好的申请背景,那就很难)

2。 JVM有相似之处,但Kotlin明显存在差异

但是由于Kotlin在与Java相同的JVM上运行(因此使用与Java相同的垃圾收集器),您应该期望一个类似安全的运行时环境。话虽这么说,当涉及到Lamdas和明确的引用时,它们被转换。并且它可以在不同的情况下变化:

与Java一样,Kotlin的情况因情况而异。

  
      
  • 如果将lambda传递给内联函数并且没有标记为noinline,那么整个事情就会消失,并且没有额外的类   或者创建对象。
  •   
  • 如果lambda没有捕获,那么它将作为一个单独的类发出,其实例一次又一次地被重用(一个类+一个   对象分配)。
  •   
  • 如果lambda捕获,则每次使用lambda时都会创建一个新对象。
  •   

来源:http://openjdk.java.net/jeps/8158765

3。总结,并进一步阅读

这个答案应该解释你所看到的,不能自己解释得更好:https://stackoverflow.com/a/42272484/979052 不同的问题,我知道,但背后的理论是一样的 - 希望有所帮助

答案 2 :(得分:1)

正如您已经建议的那样,addUpdateListener的论点在两个版本中实际上都是不同的。

我们来看一个例子。我用一个抽象方法JavaAbstract

创建了一个类foo
public interface JavaInterface {
     void foo();
}

这用于JavaInterfaceClient

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

让我们看看如何从 Kotlin 中调用useInterfaceInstance

首先,在示例中使用简单的lambda(SAM Conversion):

JavaInterfaceClient().useInterfaceInstance {}

用Java表示的结果字节码:

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

如您所见,非常简单,没有对象实例化。

第二次,带有匿名实例:

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

    }
})

用Java表示的结果字节码:

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

在这里,我们可以观察到新的对象实例化,这与SAM转换/ lambda方法有关。您应该尝试代码中的第二个示例。