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字节码反编译为java代码 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 编译器给引用的局部变量封装了一层引用。