注:来自Medium上的一位Android工程师所写,做者从字节码的层面分析了kotlin一些隐性的性能成本,以及若是避免这些。这个文章有3个部分,这是第一部分。英文原版 medium.com/@BladeCoder…java
在2016年, Jake Wharton针对Java的隐性成本进行了一系列有趣的谈话。在同一时期,他也开始倡导使用Kotlin语言进行Android开发,但几乎没有提到该语言在开发中的隐藏成本,除了推荐使用内联函数。如今,Kotlin在Android Studio 3.0中获得Google的正式支持,我认为经过研究它产生的字节码来写一些这方面的文章是一个不错的主意。数据库
Kotlin是一种现代化的编程语言,与Java相比,它具备更多的语法糖,并且在后台还有更多的“黑魔法”,可是其中有一些性能的成本是不可忽略的,尤为是针对一些低端的Android设备。编程
这里并非反对Kotlin,我很是喜欢这个语言由于它极大的提升了开发效率。但我也认为一个好的开发人员须要知道语言的内部工做原理,以便更明智地使用它。kotlin是很是强大的,有这样一种名言:“能力越大,责任也越大”。缓存
这些文章尽基于Kotlin 1.1的JVM / Android实现,而不是Javascript实现。bash
这个工具会为你将kotlin代码转换为字节码文件,在Android Studio中安装了Kotlin插件后,选择“Show Kotlin Bytecode”打开当前类的字节码文件,而后,能够点击“Decompile”按钮阅读等效的Java代码。
(译者注:在Android studio中 首先选中你要打开的类,而后 tools->kotlin->Show Kotlin Bytecod 便可查看当前类编译的字节码文件。)app
Kotlin能够为变量分配函数,并能够将这些函数做为参数传递给其余函数。接受其余函数做为参数的函数被称为高阶函数。
kotlin函数可用经过带有::符号的函数名来引用(译者注:通俗点讲Kotlin 中双冒号操做符 表示把一个方法当作一个参数,传递到另外一个方法中进行使用),或者直接声明为匿名函数,或使用lambda表达式语法,lambda表达式是描述函数的最简洁的方式。编程语言
Kotlin是为Java 6/7和Android提供lambda支持的最佳方式之一。函数
下面这段代码是在数据库中经过事务执行任意操做,返回值为受影响的行数:工具
fun transaction(db: Database, body: (Database) -> Int): Int {
db.beginTransaction()
try {
val result = body(db)
db.setTransactionSuccessful()
return result
} finally {
db.endTransaction()
}
}复制代码
咱们能够经过使用相似于Groovy的语法将lambda表达式做为最后一个参数来调用此函数性能
val deletedRows = transaction(db) {
it.delete("Customers", null, null)
}复制代码
可是Java 6不能直接支持lambda表达式,那么他们如何翻译成字节码呢?如你所料,lambda和匿名函数会被编译为函数对象。
下面这段代码是上述lambda表达式编译后的Java表示形式
class MyClass$myMethod$1 implements Function1 {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
return Integer.valueOf(this.invoke((Database)var1));
}
public final int invoke(@NotNull Database it) {
Intrinsics.checkParameterIsNotNull(it, "it");
return it.delete("Customers", null, null);
}
}复制代码
在Android dex文件中,每一个lambda表达式编译为函数后,应用的方法总数会增长3到4个。
这样作的好处是这些函数对象的实例只有在使用时才会被建立,这意味着:
(译者注:当Lambda表达式访问一个定义在表达式体外的非静态变量或者对象时,这个Lambda表达式称为“捕获的”。好比,下面这个lambda表达式捕捉了变量x:
int x = 5; return y -> x + y; 为了保证这个lambda表达式声明是正确的,被它捕获的变量必须是“有效final”的。因此要么它们须要用final修饰符号标记,要么保证它们在赋值后不能被改变。)
因为示例中的调用者代码使用的是非捕获lambda表达式的形式,所以它会被编译为单例,而不是内部类。
this.transaction(db, (Function1)MyClass$myMethod$1.INSTANCE);复制代码
建议:若是使用捕获lambda表达式,避免重复调用高阶函数以便减小垃圾收集器的压力。
在java8 中大约有43个特殊的函数接口用来最大限度的避免装箱和拆箱操做,而kotlin编译的函数对象仅实现了通用接口,同时使用Object类型做为任何类型的输入或输出值。
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}复制代码
这意味高阶函数中做为参数传递的函数的输入值或返回值存在基本数据类型(好比Int,Long)时调用高阶函数的过程将将涉及到系统的装箱和拆箱操做,而这在Android上这会对性能形成不可忽视的影响。
在上面被编译过的代码中,你能够看到结果被包装成了Integer对象,可是最后调用代码又当即对齐进行了拆箱操做(译者注:调用代码最后返回的是Int)。
建议:看成为参数的函数中的输入输出值涉及到基本数据类型时,请谨慎调用高阶函数,频繁的调用将会给系统带来更大的压力。
幸运的是Kotlin使用了一个很棒的技巧,以免在使用lambda表达式时形成的额外开销,那就是将高阶函数声明为内联。声明为内联的函数会被编译器直接插入到调用者内部。而这对于高阶函数的好处是做为其参数的lambda表达式也将会被内联,实际效果是这样的:
当把咱们上面的transaction()函数声明为内联类型时,调用者的代码就会变成下面的形式:
db.beginTransaction();
try {
int result$iv = db.delete("Customers", null, null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}复制代码
注意事项:
建议:可能的话,尽可能将高阶函数声明为内联,保持代码行数为一个较小的数字,将大块代码移动到非内联函数中。
在之后的文章中咱们将讨论内联函数的其余性能优点。
Kotlin类中没有静态字段或者方法,这些字段和方法能够在类中的伴随对象中声明。
思考如下代码:
class MyClass private constructor() {
private var hello = 0
companion object {
fun newInstance() = MyClass()
}
}复制代码
编译时,伴随对象会被声明为单例。这意味着就像任何须要从其余类访问本类私有字段的Java类同样,从伴随对象访问外部类的私有字段(或构造函数)将会生成额外的setter和getter方法。对类字段的每一个读写操做都将致使伴随对象中静态方法的调用。
ALOAD 1
INVOKESTATIC be/myapplication/MyClass.access$getHello$p (Lbe/myapplication/MyClass;)I
ISTORE 2复制代码
在Java中咱们能够经过这些字段在包中的可见性来避免生成这些setter或getter方法,可是在kotlin中不存在包的可见性。使用public 或者internal 都会致使kotlin生成默认的getter和setter方法,使外部能够访问这些字段,而且调用实例方法在技术上比调用静态方法代价更大,因此不要由于优化缘由而改变这些字段的可见性。
建议:若是须要从伴随对象中重复读写类字段,能够将其值缓存在局部变量中,以免重复的隐藏方法调用。
在kotlin中通常会将静态常量声明在伴随对象中。
好比:
class MyClass {
companion object {
private val TAG = "TAG"
}
fun helloWorld() {
println(TAG)
}
}复制代码
代码看起来整齐简单,但幕后发生的事情至关难看。
因为与上面相同的缘由,访问伴随对象中声明的私有常量实际上将在伴随对象实现类中生成一个额外的getter方法。
GETSTATIC be/myapplication/MyClass.Companion : Lbe/myapplication/MyClass$Companion;
INVOKESTATIC be/myapplication/MyClass$Companion.access$getTAG$p (Lbe/myapplication/MyClass$Companion;)Ljava/lang/String;
ASTORE 1复制代码
更糟糕的是该方法并不直接返回值,而是调用由kotlin生成的另外一个getter方法。
ALOAD 0
INVOKESPECIAL be/myapplication/MyClass$Companion.getTAG ()Ljava/lang/String;
ARETURN复制代码
而当常量被声明为public而不是private,这个getter方法也是public的,能够直接调用,因此不须要上一步的合成方法。可是Kotlin仍然须要调用getter方法来读取一个常量。
事实证实,为了存储常量kotlin编译器会在主类中生成一个私有静态常量字段而不是在伴随对象中,可是由于静态字段在类中被声明为私有的这就须要须要另外一种合成方法来从伴随对象访问它。
INVOKESTATIC be/myapplication/MyClass.access$getTAG$cp ()Ljava/lang/String;
ARETURN复制代码
最后,该合成方法读取字段的实际值
GETSTATIC be/myapplication/MyClass.TAG : Ljava/lang/String;
ARETURN复制代码
换句话说,当你访问伴随对象中的私有常量字段时,代码的执行流程是这样的:
等效的Java代码:
public final class MyClass {
private static final String TAG = "TAG";
public static final Companion companion = new Companion();
// synthetic
public static final String access$getTAG$cp() {
return TAG;
}
public static final class Companion {
private final String getTAG() {
return MyClass.access$getTAG$cp();
}
// synthetic
public static final String access$getTAG$p(Companion c) {
return c.getTAG();
}
}
public final void helloWorld() {
System.out.println(Companion.access$getTAG$p(companion));
}
}复制代码
那么咱们能够对其进行优化吗?能够,但不是每次都行。
首先,能够经过使用const关键字将其声明为编译时常量,能够彻底避免任何方法调用,这将直接在调用代码中进行内联,但这只能将其用于原始数据类型和字符串。
class MyClass {
companion object {
private const val TAG = "TAG"
}
fun helloWorld() {
println(TAG)
}
}复制代码
其次,能够在伴随对象的public字段上使用@JvmField注解来指示编译器不生成任何getter或setter方法,并将其做为类中的静态字段公开,就像纯Java常量。实际上,这个注解就是为了兼容Java的缘由而建立的。此外,它只能用于public字段。
最后,你还可使用ProGuard工具优化字节码,但这种方式的兼容性较差。
建议:合理使用const关键字来声明原始数据类型和String常量避免读取这些常量带来的额外开销
对于其余类型的常量若是你须要频繁的访问它,请将它缓存在局部变量中
此外,全局公共常量最好存储在本类对象中而不是伴随对象中
这就是第一篇文章,但愿能够帮助你更好地理解这些Kotlin功能,理解这一点你才会在写出更智能的代码的同时不会牺牲代码的可读性及软件性能。