2017-08-29 | translate

[翻译]kotlin-hidden-cost-part-2

https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-2-324a4a50b70

Local functions

在上篇文章中,还有一种函数没有介绍:声明在其他函数里的函数,叫做local function,作用域在外层函数内。

1
2
3
4
5
fun someMath(a: Int): Int {
fun sumSquare(b: Int) = (a + b) * (a + b)

return sumSquare(1) + sumSquare(2)
}

我们先提一下 local function 的局限:不能声明在 inline 函数内,包含 local function 的函数也不能在 inline 函数内使用。这种方式下没有避免函数调用 cost 的魔法。

编译后,local function 被转换成 Function 对象,和 lambda 一样,上篇文章中描述的限制也同样适用于此。对应的 JAVA 形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static final int someMath(final int a) {
Function1 sumSquare$ = new Function1(1) {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
return Integer.valueOf(this.invoke(((Number)var1).intValue()));
}

public final int invoke(int b) {
return (a + b) * (a + b);
}
};
return sumSquare$.invoke(1) + sumSquare$.invoke(2);
}

在此有一处性能较之 lambda 稍好:由于函数的实例是 caller 已知的,所以会直接调用确定的方法而不是 Function 接口泛型生成的方法。也就是说在外部函数调用 local function 时,不会有类型转换和对原始类型拆箱装箱。从字节码可以看出:

1
2
3
4
5
6
7
8
ALOAD 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
2
3
4
5
fun someMath(a: Int): Int {
fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)

return sumSquare(a, 1) + sumSquare(a, 2)
}

我们现在就有了一个可复用,无类型转换/拆箱消耗的 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
2
3
fun sayHello(who: String) {
println("Hello $who")
}

对应 java:

1
2
3
4
5
public static final void sayHello(@NotNull 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
2
3
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}

Android 的 ProGuard 默认是关闭的,需要手动开启。

Nullable primitive types

需要提醒一下:一个 nullable 类型永远是一个引用。把原始类型声明为 nullable ,kotlin 会用 IntegerFloat 替代 intfloat 这些类型。会产生额外的装箱拆箱操作。

相比于 Java 使用 autoboxing 让你可以将 int 和 Integer 一视同仁,从而写出无视 null safe 的代码,kotlin 强制你在使用 nullable 的类型是对其进行检查,好处显而易见:

1
2
3
4
5
6
fun add(a: Int, b: Int): Int {
return a + b
}
fun add(a: Int?, b: Int?): Int {
return (a ?: 0) + (b ?: 0)
}

使用原始类型应尽可能声明为 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
3
fun 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
2
val values = intArrayOf(1, 2, 3)
printDouble(*values)

3. Passing a mix of arrays and arguments

1
2
val values = intArrayOf(1, 2, 3)
printDouble(0, *values, 42)

没啥意思


part 3

Ref