划重点(Takeaway):
- Collection自己提供的处理函数(forEach/map...)都支持inline,能用就别自己写循环
- 在严格执行了上条后,你基本上就不怎么需要了解/写inline functions了
- 升级第三方库后,务必重新编译你的项目
- 自己写for/while循环,且循环的N值很大时,是使用inline function的一个切入点
- 大函数不使用inline
- 理解不了的时候看反编译生成的Java有奇效
- 看源代码可以增进了解
基本介绍
Inline functions,中文大概就是内联/内嵌函数,字面的意思就是把内部(偷偷的)把被调用函数的代码连接(Copy)过来,具体看下代码和反编译的结果:
源代码:
fun main(args: Array<String>) {
val localGreeting = "Hello from main"
Demo().withPublicField { println(localGreeting) }
}
class Demo() {
val title = "title in demo"
inline fun withPublicField(otherFun: () -> Unit) {
println("Call from withPublicField, title: $title")
otherFun()
}
}
查看反编译的Java:(IntelliJ IDEA/Android Studio Tools -> Kotlin -> Show Kotlin Bytecode
,别忘了点击Decompile
按钮)
public final class MainKt {
public static final void main(@NotNull String[] args) {
final String localGreeting = "Hello";
(new Demo()).withPublicField((Function0)(new Function0() {
...
public final void invoke() {
String var1 = localGreeting;
System.out.println(var1);
}
}));
}
}
public final class Demo {
private final String title = "title_private";
@NotNull
public final String getTitle() { return this.title; }
public final void withPublicField(@NotNull Function0 otherFun) {
String var2 = "Call from withPublicField, title: " + this_$iv.getTitle();
System.out.println(var2);
otherFun.invoke();
}
}
由于Java的function不是first-class member,所以其高阶函数(higher-order function)的使用,实际上存在着封装function成对应的wrapper class的实例,并对可见范围内(closure)的变量存在引用/访问。这实际上是以一定的资源损耗换来的便利性。对于我们上述的代码:
-
第4行
(Function0)(new Function0() {...}
即是function转化成wrapper class实例 -
第7行
String var1 = localGreeting;
即是对可见范围变量的引用
在平常使用中,这个损耗是微小可忽略的,但如果是被一个N次(N值很大)循环中调用,那积少成多的损耗就是一个值得重视的问题了。
对于这个问题,Kotlin的处理方式是直接在编译期将原来的higher-order function的执行/实现代码,copy到调用处。具体看代码(唯一的改动点:原函数前添加了inline关键字):
inline fun withPublicField(otherFun: () -> Unit) {
...
}
然后查看生成的Java二进制:
public final class MainKt {
public static final void main(@NotNull String[] args) {
String localGreeting = "Hello";
Demo this_$iv = new Demo();
String var3 = "Call from withPublicField, title: " + this_$iv.getTitle();
System.out.println(var3);
System.out.println(localGreeting);
}
}
注意对比两次生成的java代码:原先的L4~L10,现在的L4~L7。可以看到,新的Java代码很简单粗暴的把withPublicField的实现代码copy进入了main函数(自然移除了对withPublicField的调用)。
好处
- 不需要额外创建wrapper class instance
- 不需要维护可见范围内的变量引用/调用
代价肯定有的,不然就所有的方法都设成inline不就完了:-)
使用注意点
任何inline function的修改,必须重新编译后才生效
这个从之前反编译的代码可以推断出,毕竟调用inline function的代码已经被抹除,替换成了inline function的实现代码。重新编译才能再次重复这个替换过程。这个对于调用第三方库的inline函数尤其需要注意。
private变量
之前我们看到植入的代码中有这么一行代码(L5):
String var3 = "Call from withPublicField, publicTitle: " + this_$iv.getPublicTitle();
这里,getPublicTitle()
如果是个private会怎样,相信大家可以猜测到。具体可以用代码验证:
class Demo() {
private val title = "title in demo"
...
}
修改成private可见性后,代码编译出错(this_$iv.getPublicTitle()在main函数是不可见了)
当然inline函数的写法,除了这个变量的可见性还有其他,譬如:Non-local returns,但只要理解了其编译时的copy行为后,很多就水落石出了。(好的IDE其实还是给了很多有用的提示的,所以尽管写,让IDE去操这个心吧)
源代码查看/验证
Iterator.forEach
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
可以看到,自带的Collection的函数都早已支持了inline了。所以,能使用自带的Collection函数就别写自己的循环了。
Bonus
- 一个轻巧的反编译GUI:bytecode-viewer
- 关于inline functions带来的性能提升,可以看这篇文章:Effective Kotlin: Consider inline modifier for higher-order functions
网友评论