Kotlin原理-闭包
2022.01.26
林嘉伟
讲kotlin的闭包我想从java的闭包讲起。
Java不完整闭包 Java的闭包是不完整的,它要求匿名内部类使用外部的变量必现是final的,这就使我们不能在匿名内部类里面修改外部变量的值:
1 2 3 4 5 6 7 8 9 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编译器会给它生成一个实际的类,将外部变量保存到这个类的成员变量里,我们可以通过下面的代码打印出这个生成的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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, "}");
输出如下:
1 2 3 4 D closure : class class me.linjw.demo.MainActivity$1 { D closure : final me.linjw.demo.MainActivity this$0; D closure : final java.lang.Integer val$data; D closure : }
可以看到这个类除了用成员变量保存了外部的data的副本之外,还保存了外部类MainActivity的引用。这也是匿名内部类/非静态内部类持有外部类引用的原理。
突破Java不完整闭包限制 知道了Java匿名内部类持有外部对象引用的原理之后,我们其实是可以通过下面的方法绕过不能修改外部对象的限制的:
1 2 3 4 5 6 7 8 9 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的副本,指向了这个数组。
用c++的话来讲就是,指针在方法结束的时候被回收了,但是指针所指向的堆内存没有被回收。
Kotlin闭包原理 Kotlin闭包的原理实际上就是上面讲的突破Java不完整闭包限制的原理。用工具查看下面Kotlin代码生成的Java字节码:
1 2 3 4 5 var data = 0 findViewById<Button>(R.id.button).setOnClickListener { data++ Log.d(TAG,"data = $data") }
可以看到data实际指向了一个IntRef的堆内存,在IntRef的element成员里面保存的才是实际的值:
1 2 3 4 5 6 7 8 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); } }));