https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-2-324a4a50b70
Local functions
在上篇文章中,还有一种函数没有介绍:声明在其他函数里的函数,叫做local function,作用域在外层函数内。
1 | fun someMath(a: Int): Int { |
我们先提一下 local function 的局限:不能声明在 inline 函数内,包含 local function 的函数也不能在 inline 函数内使用。这种方式下没有避免函数调用 cost 的魔法。
编译后,local function 被转换成 Function 对象,和 lambda 一样,上篇文章中描述的限制也同样适用于此。对应的 JAVA 形式如下:
1 | public static final int someMath(final int a) { |
在此有一处性能较之 lambda 稍好:由于函数的实例是 caller 已知的,所以会直接调用确定的方法而不是 Function 接口泛型生成的方法。也就是说在外部函数调用 local function 时,不会有类型转换和对原始类型拆箱装箱。从字节码可以看出:1
2
3
4
5
6
7
8ALOAD 1
ICONST_1
INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I
ALOAD 1
ICONST_2
INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I
IADD
IRETURN
方法被调用两次,接收一个 int
参数,返回一个 int
值,期间没有拆箱操作,方法调用期间仍有一个创建 Function 实例的消耗,可以把 local function 写成 non-capture 形式避免:
1 | fun someMath(a: Int): Int { |
我们现在就有了一个可复用,无类型转换/拆箱消耗的 Funtion 实例。与 class 的私有函数相比,唯一的缺点就是会生成带有几个方法的额外 class。
Local functions 是 private functions 的替代方法,它可以访问外部函数的局部变量。需要付出的代价是每次调用外部函数都会生成 Function 对象,建议将 local function 写成 non-capturing 形式。
Null safety
kotlin 的一大特性就是 nullable 和 non-null 对象之间有着鲜明的界限。编译器禁止将 non-null 对象赋值为 null/nullable 变量,可以有效避免运行时的 NullPointerExceptions
。
Non-null arguments runtime checks
1 | fun sayHello(who: String) { |
对应 java:1
2
3
4
5public static final void sayHello( String who){
Intrinsics.checkParameterIsNotNull(who, "who");
String var1 = "Hello " + who;
System.out.println(var1);
}
Kotlin 编译器还给参数加上了 @NotNull
注解,当传入空值时 java tools 可以据此提示。
然而仅有注解是远远不够的,编译器还在函数起始位置调用了一个静态方法来检查参数,可以抛出 IllegalArgumentException
异常。在入口位置崩溃肯定比逻辑里不知到哪出了 NullPointerException
强。
实际上,每个 public function 都会为其所有非空参数进行 Intrinsics.checkParameterIsNotNull()
调用。private function 不会调用此方法,编译器可以确保 kotlin class 内部代码是 null 安全的。
调用这个静态方法对性能的影响是无关紧要的,对调试测试很有帮助。也就是说,在 release 版本可以把这个调用移除。使用 -Xno-param-assertions
这个编译选项或者在 Proguard 中加入一下规则:
1 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { |
Android 的 ProGuard 默认是关闭的,需要手动开启。
Nullable primitive types
需要提醒一下:一个 nullable 类型永远是一个引用。把原始类型声明为 nullable ,kotlin 会用 Integer
或 Float
替代 int
和 float
这些类型。会产生额外的装箱拆箱操作。
相比于 Java 使用 autoboxing 让你可以将 int 和 Integer 一视同仁,从而写出无视 null safe 的代码,kotlin 强制你在使用 nullable 的类型是对其进行检查,好处显而易见:
1 | fun add(a: Int, b: Int): Int { |
使用原始类型应尽可能声明为 non-null ,以获得更好地可读性和性能。
About arrays
kotlin 有三种 array 类型:
- IntArray, FloatArray and others: 原始类型数组,编译为 int[], float[] and others.
- Array
: non-null 对象引用数组,将对原始类型进行装箱 - Array<T?>:nullable 对象引用数组,也将对原始类型进行装箱
如果数据对象是原始类型,尽量用 IntArray 减少额外的性能损耗。
Varargs
和 Java 一样,我们在 kotlin 可以用变长参数。关键字有点不一样:1
2
3fun printDouble(vararg values: Int) {
values.forEach { println(it * 2) }
}
实际上也和 Java 一样, 变长参数就是一个数组参数,下面三种方式都能正常调用:
1. Passing multiple arguments
1 | printDouble(1, 2, 3) |
2. Passing a single array
1 | val values = intArrayOf(1, 2, 3) |
3. Passing a mix of arrays and arguments
1 | val values = intArrayOf(1, 2, 3) |
没啥意思