kotlin中闭包的概念和原理

时间:2023-02-03 11:11:05

kotlin中闭包的概念和原理

问题背景

安卓开发,kotlin学习和使用过程中,kotlin闭包肯定算是个新概念,尤其从安卓java开发转kotlin的同学,现在一起看看kotlin的闭包到底是什么意思? 闭包的概念关键:外部函数调用之后其变量对象本应该被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象,这就是闭包的重要概念。简单的说就是,一个函数A可以访问另一个函数B的局部变量,即便另一个函数B执行完成了也没关系。 直接看概念估计不是很好理解,一起从实例看看具体是怎么回事?

问题分析

(1)java闭包相关

1、不完整闭包 安卓开发中,java匿名内部类使用外部的变量必现是final的,这就使我们不能在匿名内部类里面修改外部变量的值,代码如下所示: ``` final Integer data = 0; // 必须是final

View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { Log.d(TAG, " data = " + data); } }; findViewById(R.id.button).setOnClickListener(listener); ``` 根本原因在于方法内的临时变量是存放在栈区的,一旦方法调用完成,这部分的内存就会被释放。如果我们再去修改这块的内存就会造成不可预期的后果。那么问题又来了,如果一旦方法调用完成,上面例子的data这个引用变量的内存就会被回收。那么在onClick的时候为什么还能读取到值呢? java实现匿名内部类的原理是Java编译器会给它生成一个实际的类,将外部变量保存到这个类的成员变量里,我们可以通过下面的代码打印出这个生成的类:

Log.d(TAG, "class " + listener.getClass() + " { ");
for (Field field : listener.getClass().getDeclaredFields()) {
 StringBuilder sb = new StringBuilder();
 sb.append("\t");

 if (Modifier.isFinal(field.getModifiers())) {
     sb.append("final ");
 }

 sb.append(field.getType().getName())
         .append(" ")
         .append(field.getName())
         .append(";");

 Log.d(TAG, sb.toString());
}
Log.d(TAG, "}");

可以看到这个类除了用成员变量保存了外部的data的副本之外,还保存了外部类MainActivity的引用。这也是匿名内部类/非静态内部类持有外部类引用的原理。 2、突破java不完整闭包 知道了Java匿名内部类持有外部对象引用的原理之后,我们其实是可以通过下面的方法绕过不能修改外部对象的限制的:

final Integer[] data = new Integer[]{0};

View.OnClickListener listener = new View.OnClickListener() {
 @Override
 public void onClick(View v) {
     data[0]++;
     Log.d(TAG, " data = " + data[0]);
 }
};

这种做法的原理在于,data这个引用本身的内存在栈区,方法调用完会被回收,但是它所指向的数组的内存在堆区,只要还有引用指向它就不会被回收。而Java编译器生成的这个类巧合就有个成员变量保存了data的副本,指向了这个数组。

(2)Kotlin闭包原理

Kotlin闭包的原理实际上就是上面讲的突破Java不完整闭包限制的原理。可以查看Kotlin代码生成的Java字节码。 1、Android studio查看生成的java字节码的步骤 kotlin查看kotlin字节码 kotlin中闭包的概念和原理 将kotlin字节码反编译为java代码 kotlin中闭包的概念和原理 2、kotlin代码如下:

var data = 0
findViewById<Button>(R.id.button).setOnClickListener {
    data++
    Log.d(TAG,"data = $data")
}

3、查看对应编译的java代码如下:

final IntRef data = new IntRef();
data.element = 0;
((Button)this.findViewById(1000095)).setOnClickListener((OnClickListener)(new OnClickListener() {
   public final void onClick(View it) {
      int var10001 = data.element++;
      Log.d(MainActivity.this.TAG, "data = " + data.element);
   }
}));

(3)Kotlin闭包函数

现在看一个kotlin闭包函数的例子,代码如下:

fun returnFun(): () -> Int {
    var count = 0
    return { count++ }
}

fun main() {
    val function = returnFun()
    val function2 = returnFun()
    println(function()) // 0
    println(function()) // 1

    println(function2()) // 0
    println(function2()) // 1
}

分析上面的代码,returnFun返回了一个函数,这个函数没有入参,返回值是Int。可以用变量接收它,还可以调用它。function和function2分别是创建的两个函数实例。 可以看到,每调用一次function(),count都会加一,说明count 被function持有了而且可以被修改。而function2和function的count是独立的,不是共享的。 通过(2)中介绍方法反编译可以看到:

public final class ClosureKt {
    @NotNull
    public static final Function0<Integer> returnFun() {
        IntRef intRef = new IntRef();
        intRef.element = 0;
        return (Function0) new 1<>(intRef);
    }

    public static final void main() {
        Function0 function = returnFun();
        Function0 function2 = returnFun();
        System.out.println(((Number) function.invoke()).intValue());
        System.out.println(((Number) function.invoke()).intValue());
        System.out.println(((Number) function2.invoke()).intValue());
        System.out.println(((Number) function2.invoke()).intValue());
    }
}

被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRef,FloatRef 等,如果是非基础类型,就统一用 ObjectRef 即可。

问题总结

再总结一下闭包的概念,外部函数调用之后其变量对象本应该被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象。Kotlin 的闭包可以获取上下文的局部变量,并可以修改它。实现办法是 Kotlin 编译器给引用的局部变量封装了一层引用。