2016年,杰克·沃顿(Jake Wharton)就 Java 的隐性成本进行了一系列有趣的演讲。 在同一时期,他也开始倡导使用 Kotlin 语言进行 Android 开发,但除了推荐大家使用这门语言的内联函数特性之外,他几乎没有提到说使用该语言会产生什么隐藏成本。 现在 Kotlin 在 Android Studio 3 中得到了 Google 的正式支持,而我想着通过研究它所生成的字节码来写写这个方面的东西也许是个好主意。
Kotlin 是一种现代编程语言,与 Java 相比,它拥有更多的语法糖,而因此也会拥有更多藏起来的“黑魔法”,其中一些要用的话就会有不可忽略的成本,特别是对于那些年头比较久还有版本比较低的 Android 终端设备。
这里并不是要抵制 Kotlin: 我非常喜欢这门语言,它提高了生产效率,而我也相信作为一个好的开发也需要去了解语言特性内部是如何运作的,以此才能更加好的利用它们。Kotlin 功能强大,而正所谓:
“能力越强责任越大。”
这些文章只会关注 Kotlin 1.1 在 JVM/Android 的实现,不会涉及其 Javascript 的实现。
这是了解 Kotlin 如何被翻译成字节码的首选工具。只要在 Android Studio 安装好 Kotlin 插件,然后选择 “Show Kotlin Bytecode” 操作,来打开一个面板显示出当前 class 文件的字节码。然后你就能按下 “Decompile” 按钮来读取出对应的 Java 代码。
这里要特别提一下,我每次提到一个 Kotlin 特性都要涉及如下几个方面的内容:
装箱的原生类型,他们会分配给生存时间较短的对象。
实例化附加的对象不会直接在代码中显示出来。
生成附加方法。你也许会了解,在 Android 应用程序中,一个 dex 文件中方法的数量是有限制的,而且在上述情况下,考虑到多个 dex 会带来限制于性能损失的叠加影响,特别是在 Lollipop 之前版本的 Android 系统之上。
我特意选择不发布任何微基准,因为大部分都是没有意义的,或者有缺陷,或者两者皆有,并且不能应用于所有的代码变化和运行环境。 在循环或嵌套循环中使用相关代码时,通常会放大负面性能影响。
此外,执行时间并不是衡量的唯一事项:也必须把新增内存使用纳入考量,因为最终所有分配的内存都要回收,而垃圾收集的成本取决于诸多因素,如可用内存和平台所使用的 GC(Garbage Collection,垃圾收集)算法。
简而言之,如果你想知道一个 Kotlin 构造是否具有一些显著的速度或内存影响,那就要在你自己的目标平台上测量你自己的代码。
Kotlin 支持把函数赋值给变量并传递变量作为其他函数的参数。接受其他函数作为参数的函数称为高阶函数。一个 Kotlin 函数可以由它的名字加前缀 :: 而引用,或直接在代码块中声明一个匿名函数,或使用 lambda 表达式语法。此中第三种是描述一个函数的最紧凑的方式。
Kotlin 是给 Java 6/7 JVM 和 Android 提供 lambda 表达式支持的最佳方式之一。
考虑下面的实用函数,它在数据库事务中执行任意操作,并返回受影响行的数量:
transaction(db: Database, body: (Database) -> Int): Int {
db.beginTransaction()
{
result = body(db)
db.setTransactionSuccessful()
result
} {
db.endTransaction()
}
}
我们可以通过使用类似于 Groovy 的语法传递一个 lambda 表达式作为最后的参数:
deletedRows = transaction(db) .delete(, , )
但 Java 6 JVM 不直接支持 lambda 表达式。那么它们是如何转换成字节码的呢?也许正如你所期望的, lambda 表达式和匿名函数被编译为函数对象。
这是 Lambda 表达式编译后对应的 Java 呈现。
MyClass$myMethod$1Function1 {
Object invoke(Object var1) {
Integer.(.invoke((Database)var1));
}
invoke(@NotNull Database it) {
Intrinsics.(it, );
db.delete(, , );
}
}
在 Android dex 文件中,每个 Lambda 表达式都被编译成函数,实际上它会导致增加 3 到 4 个方法。
好消息是这些函数对象的新实例只会在必要的时候创建,这在实践中就意味着:
对于捕获(capturing)表达式,每次将 Lambda 作为参数传递的时候都会创建新的函数实现,它会在执行后被垃圾回收器回收;
对于非捕获(non-capturing)表达式(纯函数),则会创建一个单例函数实例,以便后面用到的时候可以复用。
[译者注:捕获 Lambda 表达式指在表达式内部访问了表达式外的非静态变量或者对象的表达式,非捕获 Lambda 表达式反之]
既然我们示例中的调用代码使用了非捕获 Lambda,它会被编译成单例而不是一个内部类:
this.transaction(db, (Function1)MyClass$myMethod$1.INSTANCE);
如果标准(非内联)的高阶函数调用捕获 Lambda,应该避免对其进行重复调用以减少垃圾回收器的压力。
与 Java 8 不同,Java 8 大约有43 个不同的特定函数接口用于尽可能地避免装箱和拆箱。由 Kotlin 编译出来的函数对象只实现完全通用的接口,高效地将 Object 类型用于输入或输出值。
Function1<P1, R> : Function<R> {
invoke(p1: P1): R
}
这表示在高阶函数中调用作为参数传入的函数,如果函数涉及基本类型(比如 Int 或 Long),实际上会对输入值和返回值有计划地装箱和拆箱。这可能会对性能产生不可忽视的影响,尤其是在 Android 中。
在我们上面编译的 Lambda 中,你可以看到结果是装箱成 Integer 对象的。调用者代码随后会对其进行拆箱操作。
在写一个函数作为参考的标准(非内联)高阶函数时,如果这个作为参数的函数使用的是基本类型的输入或输出值,那就要小心了。调用这个作为参数的函数会因为装箱和拆箱操作给垃圾收集器带来更大的压力。
幸好,在使用 Lambda 表达式的时候,Kotlin 有一个神奇的技巧来避免这些开销:将高级函数声明为 inline。编译器会将函数体直接内联到调用代码内部,完全避免了调用。对于高阶函数来说,更大的好处在于,作为参数的 Lambda 表达式也会被内联。实际的影响包括:
声明 Lambda 不会产生函数对象实例;
Lambda 的输入或输出有基本类型时,不会产生装箱或拆箱动作;
不会造成方法数量的增加;
没有实际调用函数。这对于需要多次调用而且重度使用 CPU 的代码来说可以提升性能。
我们在将 transaction() 函数声明为内联函数之后,调用代码的 Java 呈现就变成了:
db.beginTransaction();
var5;
{
result$iv = db.delete(, , );
db.setTransactionSuccessful();
var5 = result$iv;
} {
db.endTransaction();
}
这个杀手锏有一些需要注意的地方:
内部函数不能调用自身,也不能通过其它内联函数调用自身。[译者注:简单地说就是不能用于内联的递归];
类中声明为 public 的内联函数只能访问这个类的公有函数和字段;
代码大小会有所增长。内联一个被使用多次的长函数会使生成的代码相当大,如果这个长函数引用了其它内联的长函数,就更不得了。
如果可能,就将高阶函数内联。保持简短,若有必要,可将大块代码搬到非内联函数中。
对于要求性能的代码所调用的函数,也可以声明内联。
我们会在以后的文章中讨论内联函数其它性能方面的优点。
原文来自:开源中国社区
声明:所有来源为“聚合数据”的内容信息,未经本网许可,不得转载!如对内容有异议或投诉,请与我们联系。邮箱:marketing@think-land.com
支持识别各类商场、超市及药店的购物小票,包括店名、单号、总金额、消费时间、明细商品名称、单价、数量、金额等信息,可用于商品售卖信息统计、购物中心用户积分兑换及企业内部报销等场景
涉农贷款地址识别,支持对私和对公两种方式。输入地址的行政区划越完整,识别准确度越高。
根据给定的手机号、姓名、身份证、人像图片核验是否一致
通过企业关键词查询企业涉讼详情,如裁判文书、开庭公告、执行公告、失信公告、案件流程等等。
IP反查域名是通过IP查询相关联的域名信息的功能,它提供IP地址历史上绑定过的域名信息。