kotlin编译加速缓存
1. android启动执行多个runtime
深入理解Android Runtime
申国骏
2022-08-08 12:31·字数:2599·阅读:2340
image.png
上图是Android整体的架构,Android Runtime之于Android而言相当于心脏之于人体,是Android程序加载和运行的环境。这篇文章主要针对Android Runtime部分进行展开,探讨Android Runtime的发展以及目前现状,并介绍应用Profile-Guided Optimization(PGO)技术对应用启动速度进行优化的可行性。转载请注明来源“申国骏”
App运行时演进
JVM
Android原生代码使用java或者Kotlin编写,这些代码会通过javac或者kotlinc编译成.class文件,在Android之前,这些.class文件会被输入到JVM中执行。JVM可以简单分为三个子系统,分别是Class Loader、Runtime Data Area以及Execution Engine。其中Class Loader主要负责加载类、校验字节码、符号引用链接及对静态变量和静态方法分配内存并初始化。Runtime Data负责存储数据,分为方法区、堆区、栈区、程序计数器以及本地方法栈。Execution Engine负责二进制代码的执行以及垃圾回收。
image.png
Execution Engine中,会采用Interpreter或者JIT执行。其中Interpreter表示在运行的过程中对二进制代码进行解释,每次执行相同的二进制代码都进行解释比较浪费资源,因此对于热区的二进制代码会进行JIT即时编译,对二进制代码编译成机器码,这样相同的二进制代码执行时,就不用再次进行解释。
image.png
DVM(Android 2.1/2.2)
JVM是stack-based的运行环境,在移动设备中对性能和存储空间要求较高,因此Android使用了register-based的Dalvik VM。从JVM转换到DVM我们需要将.class文件转换为.dex文件,从.class转换到.dex的过程需要经过 desugar -> proguard -> dex compiler三个过程,这三个过程后来逐步变成 proguard -> D8(Desugar) 直到演变到今天只需要一步R8(D8(Desugar))。
image.png
我们主要关注Android中Runtime Engine与JVM的区别。在Android早期的版本里面,只存在Interpreter解释器,到了Android2.2版本将JIT引入,这个版本Dalvik与JVM的Runtime Engine区别不大。
image.png
ART-AOT(Android 4.4/5.0)
为了加快应用的启动速度和体验,到了Android4.4,Google提供了一个新的运行时环境ART(Android Runtime),到了Android5.0,ART替换Dalvik成为唯一的运贺键高行时环境。
image.png
ART运行时环境中,采用了AOT(Ahead-of-time)编译方式,即在应用安装的时候就将.dex提前编译成机器码,经过AOT编译之禅尺后.dex文件会生成.oat文件。这样在应用启动执行的时候,因为不需要进行解释编译,大大加快了启动速度。
image.png
然而AOT带来了以下两个问题:
应用安装时间大幅增加,由于在安装的过程中同时需要编译成机器码,应用安装时间会比较长,特别在系统升级的时候,需要对所有应用进行重新编译,出现了经典的升级等待噩梦。
image.png
应用占用过多的存储空间,由于所有应用都被编译成.oat机器码,应用所占的存储亮冲空间大大增加,使得本来并不充裕的存储空间变得雪上加霜。
进一步思考对应用全量进行编译可能是没有必要的,因为用户可能只会用到一个应用的部分常用功能,并且全量编译之后更大的机器码加载会占用IO资源。
ART-PGO(Android 7.0)
从Android7.0开始,Google重新引入了JIT的编译方式,不再对应用进行全量编译,结合AOT、JIT、Interpreter三者的优势提出了PGO(Profile-guided optimization)的编译方式。
在应用执行的过程中,先使用Interpreter直接解释,当某些二进制代码被调用次数较多时,会生成一个Profile文件记录这些方法存储起来,当二进制代码被频繁调用时,则直接进行JIT即时编译并缓存起来。
当应用处于空闲(屏幕关闭且充电)的状态时,编译守护进程会根据Profile文件进行AOT编译。
当应用重新打开时,进行过JIT和AOT编译的代码可以直接执行。
这样就可以在应用安装速度以及应用打开速度之间取得平衡。
image.png
image.png
JIT 工作流程:
image.png
ART-Cloud Profile(Android 9.0)
不过这里还是有一个问题,就是当用户第一次安装应用的时候并没有进行任何的AOT优化,通常会经过用户多次的使用才能使得启动速度得到优化。
image.png
考虑到一个应用通常会有一些用户经常使用执行的代码(例如启动部分以及用户常用功能)并且大多数时候会有先行版本用于收集Profile数据,因此Google考虑将用户生成的Profile文件上传到Google Play中,并在应用安装时同时带上这个Profile文件,在安装的过程中,会根据这个Profile对应用进行部分的AOT编译。这样当用户安装完第一次打开的时候,就能达到较快的启动速度。
image.png
image.png
Profile in cloude 需要系统应用市场支持,在国内市场使用Google Play的占比非常低,因此cloud profile的优化在国内几乎是没有作用的,不过Profile的机制提供了一个可以做启动优化的思路。早在2019年,支付宝就在秒开技术的回应的里面提到过profile-based compile的技术,参考:如何看待今日头条自媒体发布谣言称“支付宝几乎秒开是因为采用华为方舟编译器”?,这也是我们一直研究Profile技术的原因。困扰着我们的一直有两个问题,第一个问题是如何生成Profile文件,第二个问题是怎么使用生成的Profile文件。对于第一个问题的解决相对还是有思路的,因为app运行就会生成profile文件,因此我们手动运行几次app就能在文件系统中收集到这个文件,不过如何以一种较为自动化的手段收集仍然是个问题。第二个问题我们知道Profile文件最终生成的位置,因此我们可以把生成的文件放到相应的系统目录,不过大多数手机和应用都没有权限直接放置这个文件。因此Profile优化技术一直都没有落地,直到Baseline Proflie让我们看到了希望。
Baseline Profile
Baseline Profile是一套生成和使用Profile文件的工具,在2022年一月份开始进入视野,随后在Google I/O 2022随着Jetpack新变化得到广泛关注。其背景是Google Map加快了发版速度,Cloud Profle还没完全收集好就上新版,导致Cloud Proflie失效。还有一个背景是Jetpack Compose 不是系统代码,因此没有完全编译成机器码,而且Jetpack Compose库比较大,因此在Profile生成之前使用了Jetpack Compose的应用启动会产生性能问题。最后Google为了解决这些问题,创造了收集Profile的BaselineProfileRule Macrobenchmark以及使用Profile的ProfileInstaller。
使用Baseline Profile的机制可以在Android7及以上的手机上得到应用的启动加速,因为从上述知道Android7就已经开始有PGO(Profile-guided optimization)的编译方式。生成的Profile文件会打包到apk里面,并且会结合Google Play的Cloud Profile来引导AOT编译。虽然在国内基本上用不了Cloud Profile,不过Baseline Profile是可以独立于Google Play单独使用的。
image.png
在使用了Baseline Proflie之后,有道词典的启动速度从线上统计上看,冷启动时间有15%的提升。
这篇文章主要介绍了Android Runtime的演进以及对于应用启动的影响,下一篇文章我会详细介绍关于Profile&dex文件优化、Baseline Profile工具库原理,以及在实际操作上如何使用的问题,敬请大家期待一下!
2. kotlin—lazy及其原理
lazy是属性委托的一种,是有kotlin标准库实现。它是属性懒加载的一种实现方式,在对属性使用时才对属性进行初始化,并且支持对属性初始化的操作时进行加锁,使属性的初始化在多线程环境下线程安全。lazy默认是线程安全的。
lazy既然是属性委托的一种,那么其语法也遵循属性委托的语法:
对应的lazy 的语法为:
由于lazy为函数,其最后一个参数是函数,那么可以使用lamda的语法代替最后一个函数参数:
通过2中的语法,我们知道lazy是kotlin标准库中的重载函数,我们先从标准库lazy的函数的分析其原理:
lazy函数默认情况下是同步安全锁模式,其可以指定线程同步模式、线程公有模式、非线程安全模式,也在同步模式时指定使用的锁对象,lazy函数会创建懒加载类的实现类,通过懒加载类的实现类实现不同模式的懒加载。
我们依次分析SynchronizedLazyImpl、SafePublicationLazyImpl、UnsafeLazyImpl这三种模式是怎么实现懒加载的:
SynchronizedLazyImpl是同步模式的懒加载,它是lazy的默认实现,其在多线程环境下进行初始化是线程安全的,我们看看其源码实现:
同步模式的懒加载SynchronizedLazyImpl的实现原理其实是使用两个属性,一个是公有属性value—对外代表属性的值,一个是私有属性_value——是真正的值。value的get内部对_value进行初始化,如仔纳果_value已纳局初始化则直接返回,如果没有初始化过则加锁并调用初始化函数把返回值赋值给_value。
SafePublicationLazyImpl是多线程环境下的公共线程安全模式,我们从其源码分析其原理:
公共线程安全模式SafePublicationLazyImpl与同步模式SynchronizedLazyImpl的区别在于,SafePublicationLazyImpl使用自旋锁进行初始化操作,而SynchronizedLazyImpl是要同步锁的方式进行初始化操作,其他与SynchronizedLazyImpl的实现一样。
SafePublicationLazyImpl是非线程安全的懒加载实现模式,在单线程下进行初始化是没啥问题,但是多线程下是进行初始化是不安全的,我们从其源码分析其原理:
SafePublicationLazyImpl与前面的两种模式的实现方式不一样就是初始化时没有加任何锁,其它是一样的。
上面分析了使用lazy函数之后返回了不同的懒加载实现类及各懒加载实现类的原理,所以lazy的语句最终语句念茄没的是:
val propertyName by [SynchronizedLazyImpl | SafePublicationLazyImpl | UnsafeLazyImpl]
但具体是怎么个懒加载实现类的value的get方法呢?——下面我们举例,然后通过编译后的字节码分析器实现原理:
举例:
编译后的字节码文件:
通过对生成的字节码的分析,lazy的原理:
注意lazy实现了懒加载,达到在使用时才进行初始化的目的,但是也为此增加了一个懒加载类,如果一个类的初始化操作不耗时却使用lazy进行懒加载是不明智的,lazy的适合场景是类的初始化操作比较耗时占资源。
3. Kotlin——数组
Kotlin为数组增加了一个Array类。为元素是基本类型的数组增加了XxxArray类(其中Xxx可以是Byte、Short、Int等基本类型)
Kotlin自己提供了一套集合体系,Kotlin的集合体系抛弃了Java集合体系中的Queue集合,Kotlin集合体系中增加了可变集合和不可变集合。
Kotlin的数组使用Array<T>来代表,Kotlin的数组就是一个Array类的实例,因此Kotlin数组是引用类型
访问数组是通过在数组的索引后跟一郑中个[]来实现,方括号中的值是数组元素的索引值。kotlin中的[]运算值其实是get(index)、set(index,value)方法,使用[]访问数组元素,调用的其实就是get(index)方法,使用[]为数组元素赋值,调用的其实就是set(index,value)方法
上面的两种方式本质是一样的,在经过编译器编译优化后,会转换成根据数组的内存地址来访问数组元素,性能不会有任何损失
所有的数组都有 size 属性,通过这个属性可以访问到数组的长度。
for-in 循环可以自动遍判乱历数组的每个元素
对数组使用for-in循环会被编译成使用基于索引的循环,并不会创建迭代器。因此具有良好的性能
kotlin数组提供了一个indices属性,这个属性可返回数组的索引区间
这种通过索引区间遍历的实现具有更好的性能,kotlin将会在底层掘丛档将其编译成根据内存地址来访问元素,不需要额外创建区间对象
Kotlin还为数组提供了一个lastIndex属性,该属性用于返回数组最后一个元素的索引,size-1
如果需要同时访问数组的索引和元素,可以使用数组的 withIndex() ,该方法返回一个Iterable对象,该对象的所有元素都是IndexedValue.
4. Kotlin介绍系列(三)高级用法之DataClass
经常会需要创建一些类除了保存数据不干其他事情,比如我们腊凳野解析网络请求下轮喊来的数据。Kotlin就提供了一个非常方便的class—— data class
我们知道在Kotlin中,声明类的同时可以方便的直接声明构造方法等参数,鉴于data class只是存放数据,那么只一个构造方法足矣,所以连类的body也就不需要了。是不是很清爽?
编译器会根据我们在构造函数里声明的属性自动导出下列成员:
经常会遇到我们只需要替换一个对象的个别属性,而其他属性保留的情况。这就是data class中生成的函数的作用了。
本文已开始的例子类,它的生成的默认函数是下面粗和这样的:
fun (name: String = this.name, age: Int = this.age) = User(name, age)
这就运训我们这样写:
data class的生成的component方法给我们的结构化声明及使用提供了可能
Kotlin介绍系列(三)高级用法之object
Kotlin介绍系列(三)高级用法之Delegation
5. SpringBoot到Kotlin血泪史
如果原先有 @Bean(name="xxx") 直接用方法名即可(我原先的 name 和方法名不一样洞段就很尴尬)
hashedCredentialsMatcher 中的内容可以直接 new ,所以不需要注入
如果SpringBoot的版本从 1.5.x 变成了 2.0.x , shiroDialect 或者 shiroFilter 可能会报如下错误
把 thymeleaf-extras-shiro 的版本号改成 2.0.0 即可(原先是 1.2.1 )
在 SpringBoot 版本中,在 Realm 中注入 Service 时,为了启用缓存,需要在前面加上 @Lazy 注解,如下
在 Kotlin 版本中我不知道发了啥疯就把它去掉了(可能是看到前面类型是 lateinit var ,自以为是的觉得可以代替 @Lazy ),然后改成了改成如下形式
拷贝完整个丛滚项目之后,测试功能的时候,发现缓存没了……然后就开始疯狂DEBUG,从版本问题,到 jar 包冲突问题,经历了很漫长的一段时间后,我定位到了 ShiroConfiguration ,只要把 shiro aop 注解关闭就可以开启缓存了
WHAT??
疯狂谷纳郑誉歌一个小时无果(因为一直以为是 Kotlin 不兼容啥的,或者是 shiro 在 boot2.x 之后需要修改相应的配置)
然后又疯狂DEBUG,把 Kotlin 版和 springboot 版进行对比,最后。定位到了 @Lazy (还好只是改成了注释,没把它给直接删了)
果然。加了 @Lazy ,整个天都亮了
最后顺便提一句 Realm 认证超级管理员的问题,可以直接在 Realm 中加上超级管理员的特别认证,就不用去方法级别上区分这个权限可以超级管理和XX管理员都可用了
6. compileDebugKotlin FAILED和aidl
自从入职CS,项目编译一直有个神坑报错,每次都需要clean rebuild若干次, 非常耽误时间
简单的说, 如果在使用AIDL时需要一个自定义的数据类型, 我们一般会这么写:
当我们写一个子类SubClass继承该类.然敏毁后槐返在Kotlin文件中直接或者间接引用到SubClass时, 就会出现一个以下的报错
报错发生在 app:compileDebugKotlin , 也就是kotlinc. 但是我们明明已经定义了该类. 全局搜索发现有两个 CustomParcel.java , 推测是两个同名铅拿饥的文件引起.
除了我们自己写的Java文件, 另外一个肯定是aidl生成的. 引用一张图:
在编译开始时会把aidl转化为Java文件, 接下来才会经过javac, kotlinc把JVM语言文件转化为字节码 .class 文件.
查看aidl生成的文件, 发现是空的, 并且有一行注释: 说明这是一个 PlaceHolder , 也就是占位文件.
网上搜到有人遇到了 相同的问题 ,问题确实发生在kotlinC编译器以aidl生成的空java文件为编译目标, 而不是真正的java类文件. 并且也给出了解决办法,升级buildTools版本.
查看 buildTools提交记录
提交记录: No java output for parcelable declaration . 也就是移除了以下的为自定义的aidl Parcelable类生成Java文件的设定(29.0.2之前的实现)
升级29.0.3, 再次编译, 发现build/aidl目录下不再生成同名的 PlaceHolder 文件了, 只剩下唯一的我们自己的文件, kotlinC这次只能用唯一的文件来编译,报错解决.
至于为什么有时候clean rebuild能编译成功,需要探究下kotlinC的源码.
最坑的是, 29.0.2就是 gradle plugin4.1默认支持的版本 , 所以你不手动指定buildTools版本为29.0.3以上就会掉进坑里.
7. 如何学习Kotlin编程语言
为什么说 Kotlin 是优秀的
本文不会像一般介绍语言的文章那样,一开头就罗列出语言那些酷炫的特性,我们稍后再来探讨这些内容。
首先我将介绍一些其它的信息,因为2013 年一项研究显示,当开发者评估一种编程语言时生态系统要比语言特性更重要。这符合我个人的经验,下面就让我开始介绍吧:
Kotlin 被编译成 JVM 字节码或者 JavaScript 代码。Java 开发者将会是对它最感兴趣的人,不过对于使用垃圾收集运行时语言的开发者而言它也具有一定的吸引力,比如 Scala、Go、Python、Ruby 和 JavaScript 等语言。
Kotlin 来自业界,而不是学术界。它解决了开发者现今面临的实际问题。例如它的类型系统可以帮助你避免空指针异常。
切换到 Kotlin 无需成本!它是开源的但这不是重点,重点是它提供了一个高质量的一键从 Java 转换到 Kotlin 的工具,并且十分关注 Java 二进制文件的兼容性。你可以将现有 Java 项目的一次性转换成 Kotlin 项目,而该项目仍将可以正常编译,即使这是一个包含上百万行代码的复杂程序。
显然你可以从上文得知,Kotlin 程序能够使用所有现存的 Java 框架和库,甚至那些依赖注解处理的高级框架。它们之间的交互是无缝的,不需要包装或者适配层。Kotlin 可以整合 Maven,Gradle 以及其它构建系统。
它十分平易近人,语法精炼直观,仅仅是阅读语言参考文档几个小时就能学会使用。Kotlin 看起来十分像 Scala 但是更加简洁并且兼顾了可读性。
它不遵循特定的编程哲学,例如极度的函数式编程或者面向对象编程风格。
它不会增加运行时的开销。Kotlin 的标准库十分小巧紧凑:专注于扩展 Java 标准库,编译阶段的大量内联操作意味像 map/filter/rece 等管道结构函数将被编译成类似于命令式语言的代码。
Anko 与 Kovenant 等框架的出现意味着在 Android 开发者中 Kotlin 开始变得流行起来。如果你正在从事 Android 相关的工作,相信你很快就会获得好的工作。你可以阅读这份 Square 公司开发者 JakeWharton 的报告,了解用 Kotlin 进行 Android 开发的体验。
Kotlin 允许你继续使用你的工作效率提升工具。IntelliJ 的 IDE 对 Kotlin 的支持十分完善:你可以对代码进行重构、搜索、导航以及使用自动完成,而且 IDE 充分支持调试、单元测试、性能分析等等功能。
除了 Android,我认为 Kotlin 还非常适用于企业中 Java 的应用场景。如果你的工作是整天埋头于大公司的代码库中,那么当 Kotlin 1.0 版本正式发布时你应该尽快去了解一下:
由知名公司为它提供强大的商业支持。 JetBrains 这家公司 有一个高度称职的大团队致力于该项目,有稳定的商业模式甚至在自己的部分旗舰产品中使用 Kotlin,这表明短期内 Kotlin 不会被放弃。
使用 Kotlin 风险较低:可以由一两个感兴趣的团队成员在项目中小范围的试验 Kotlin,这并不会扰乱你的项目,因为 Kotlin 的类对外提供的 Java API 看起来就与普通的 Java 代码一样。
因为 Kotlin 十分注重语法的可读性,代码审查不会成为问题,对 Kotlin 不熟悉的团队成员仍然能够完成该工作。
Kotlin 基于 Java 6,所以假如你难以在项目中升级使用新版本的 JVM,你可以使用 Kotlin。
今年早些时候我向 Swiss Re 这家瑞士再保险公司的团队(他们使用 Java 和 .NET)展示了 Kotlin。首先我定义了一个简单的 Java 类包含一些字段以及 toString、equals、hashCode 等方法,大概有 50 行代码。然后我将它转换成 Kotlin 代码(大部分是自动完成的),结果仅剩 1 行代码,接着我还演示了其它节省时间的特性。他们看过后对 Kotlin 充满了热情并且认为 Kotlin 是它们项目中 C# 语言的一个潜在竞争对手。
我认为 Kotlin 正中企业 Java 开发者的红心,所以尽管 Kotlin 是免费的,JetBrains 还是能够通过它增加商业版本 IDE 的销售来赚大钱。这将激励他们根据用户的意愿持续改进它。
与此相比,对于那些由不相关产品资助的语言开发者来说,当用户需求与之前的设计理念冲突时,他们很少会因此作出调整。
特性
Kotlin 作为一门新的编程语言能够脱颖而出,是因为它关注生态系统:JetBrains 懂得生产力的高低更多的取决于生态系统而不是便捷的语法。
尽快如此,Kotlin 还是有许多有用的特性能让你编码的过程变得愉快:
我们已经提过 null 安全(可选),它能够让编译器系统的标记潜在的空指针引用。与一些语言不同的是它不涉及 option 类,因此是零开销的,并且还有其它语言特性确保它不会造成不便。
精炼的语法:无处不在的类型推断、简单的函数只需要一行、简单的结构以及 JavaBeans 也只需要一行就能声明、真正的属性——可以在背后自动生成 getFoo/setFoo 方法用于与 Java 进行交互、函数可以独立存在于类之外。
异常均为非检查型。(译者注:感兴趣的可以阅读一下Java 理论与实践: 关于异常的争论)
使用 data class 关键字创建数据类会自动生成通用方法,例如 equals、hashCode、toString 以及 和 componentN(同时声明多个变量时会调用该方法)。这将帮助你在不使用构建器的情况下便捷的获得不变类(immutable classes)。
但如果你需要构造复杂的结构体,借助类型安全的构建器这个特性可以简洁的实现。如果你使用 Google Protocol Buffers 来存储结构化数据, 通过 KBuilders 这个库也能很轻易做到。
支持函数式编程以及零开销的 lambda 表达式,能够在 Java 的集合中做 Map、Filter、Folder 等处理。Kotlin 的类型系统能够自动识别可变或者不可变的集合。
扩展函数特性能够让你在不改动源码的情况下为类添加方法。乍眼一看以为是为了避免写出像 FooUtils 这种风格工具类的语法糖,不过随着使用的加深,你会认识到它不仅能帮你更加容易的通过自动完成使用方法,还能协助你集成现有的 Java API 以及借助其它 Kotlin 特性构建功能强大的扩展。
支持运算符重载,但是不会像 Scala 或者 Perl 那样出现难以理解的代码。运算符被映射成相应名字的方法,通过重写这些方法改变运算符的行为(包括函数调用),但是不能定义新的运算符。这使得程序能够兼顾功能与可读性。
Kotlin 没有宏或者其它的方式来重定义语言,但是通过这些精心设计的特性能够使第三方库自由的对它进行扩展,官方对集合类进行的扩展也只是小试牛刀而已,请看以下例子。
想使用 fibers、actors 和 Go 风格的 channels?一个名为 Quasar 的库已经为你实现了。
使用 Markdown 替代 HTML 来编写 API 文档,这样编写 JavaDocs 可比以前舒适多了。(译者注:JetBrains 提供了相应的文档生成器 Dokka)
更好用的泛型。如果你没有完全掌握泛型参数中 super 以及 extends 的含义,别担心,这不是你的错。Java 的泛型的确令人费解,Kotlin 解决了这个问题。
委托是一个大家都知道的设计模式,Kotlin 原生支持它。
== 运算符的行为符合预期(译者注:简单来说 a == b 相当于 a.equals(b);新增了 === 运算符,用来判断运算符两边是否指向同一个对象)
想快速便捷的进行异步编程吗?当然!
字符串插值“可以使用这样的写法在字符创中直接引用变量 {this.example}”
函数中的参数可以指定默认值、使用可变长度以及通过参数名传参。
还有许多的调整与优化。假如 Java 中有某些让你觉得困扰的问题,我相信 Kotlin 一定已经把它处理好了。
现在就来试用一下!
跟很多现代编程语言一样,Kotlin 可以通过网页浏览器来进行体验。不过跟其他语言不一样的是,Kotlin 的实验网站提供了一个成熟的 IDE,包括响应很快的自动完成,实时的后台编译,甚至还有在线的静态分析!
在线试用一下吧
好了,让我们继续接下来的内容
目前存在哪些问题?
生活中没有什么是完美的,包括 Kotlin。以下是我尝试这门语言时遇到的一些问题。
最大的问题是不够成熟,因为 Kotlin 目前还处于 Beta 阶段,这意味着:
每更新一个版本,语法、ABI 以及标准库就变一次。好消息是这些变化通常比较微小,可以借助 IntelliJ IDE 来自动升级你的代码,所以这个过程并不会太麻烦。
Java-to-Kotlin 的转换工具(J2K)还没有完成。它偶尔会大规模的破坏和默默地擦除 Java 8 中的 Lambdas(修改:2015 年 10 月:M13 版本的转换工具已经可以正确地处理 Java 8 的特性了)。由它转换而成的代码并不总是最好的写法,但是 JetBrains 为这个工具付出了大量努力,它已经是我用过的语言转换工具中最好的了。所以我并不太担心这个问题,这个转换器正在迅速的改进中,变得越来越成熟。
你会遇到编译器错误。尽管我的程序并不大,但还是会发生无法编译的情况,甚至错误的编译结果。诊断这些问题并不难,但终归还是影响了开发的体验。
你会遇到 IDE 内部错误。当这个错误发生时,IntelliJ IDE 会弹出一个悬浮窗口,附带向 JetBrains 报告的选项。大部分错误无需理会,不过依然会使人厌烦。
偶尔会出现无法加载提示文档的错误(修改:M14 版本发布后,这个问题已被修复)
目前 JetBrains 正致力于完善发布 1.0 版本而不是添加新的功能,期待这些问题能够得到修复。
第二个我遇到的比较大的问题是,有时与 Java 的交互会受到局限。
一个典型的 Bug 是 Java 的类型系统无法防止你改变 Map 中 Key 的类型。按理来说,这样操作应该导致编译器报错,例如使用类型错误的 Key 删除元素。有些 JDK 中的集合使用了泛型,它们某些重要方法的泛型参数是 Obejct,所以编译器不会提示。当在 IntelliJ IDE 中编写 Java 代码时会有静态分析的警告,但是目前 Kotlin 环境还没有这个功能。因为 Kotlin 使用的是 Java 的集合框架没有自己实现,所以这导致了一些类型安全方面的问题,我已经遇到好几次了。
(修改:1.0 Beta 版本中这个问题已经解决了,Java 中集合框架的类型安全缺陷在 Kotlin 已经不复存在。哟呵!)
另一个例子是,当调用或使用 Java 代码时 Kotlin 的 Null 安全特性无法发挥作用(可以借助注解弥补)。作为 Kotlin 的初学者,刚开始你可能会写许多调用 Java 库的代码,但是因为以上的问题它们并没有你想象中那么好用。这种情况的改善只能等待 Kotlin 使用人数的增长。JetBrains 一直在尝试使 Null 安全特性能体现在 Java 交互中,这种想法是好的,但有时考虑并太周全。(修改: 从 M13 版本开始,在 Java 代码中将自动以 @NotNull @Nullable 等注解实现 Kotlin 的 Null 安全特性)
虽然有以上的问题存在,但同时也使得我们能更流畅的使用 Java API,我觉得这种权衡是值得的,只是在开发中要注意。
其它需要考虑的问题:
Kotlin 的社区还比较小。虽然目前没有多少 Kotlin 的库可以使用,但是凭借优秀的 Java 交互能力,Kotlin 可以使用现有成熟的 Java 库。
如果你喜欢看书来学习,那么你需要等到今年晚些时候才能看到 Kotlin 开发者写的书(译者注:Kotlin in Action)
纯粹的函数编程风格开发者可能会觉得类型系统中缺乏一些 Scala 或 Haskell 拥有的高级功能。如果你对类型系统一些功能比较看重,那么 Kotlin 可能不适合你。
Kotlin 还能编译成 Javascript 代码,但是比较少用,所以可能会遇到更多的问题,这是我从论坛中得到的印象。(修改: 目前 Kotlin 的开发重心在于完成 1.0 版本并使其稳定运行在 JVM 中,Javascript 方面的问题将会在 1.0 发布后着手解决)
没有标准的编程风格指南,目前 Kotlin 提供了多种语法可供选择。不同人写出来的 Kotlin 代码很可能完全不一样。这与 Go 严格的风格形成了鲜明的对比。(修改: Kotlin 1.0 版本开始,一些灵活的语法已经被移除了,例如现在重载运算符以及定义中缀函数时必须分别使用 operator 和 infix 关键字进行标记)
Kotlin 的编译速度稍稍慢于 Java,以及 IntelliJ IDE 的智能提示反应有点缓慢,不算严重而且比 Scala 快多了。(修改:Kotlin 1.0 开始编译速度有了明显提升)
Kotlin 有一个 Eclipse 插件,但是很明显没有 IntelliJ 的好用。
Kotlin 在某些方面比 Java 要严格。它不会自动将 Int 转换为 Long 类型,需要开发者显示的转换。这是因为 Kotlin 关注正确性和试图解决《Java Puzzlers》一书中提出的问题。JetBrains 声称他们已经搞定一半了。
Kotlin 基于 Java 6,因此会受到它的局限。Kotlin 与 C# 在很多领域都很相似甚至比 C# 做得更好,但是它缺少一些功能,例如 Java 平台尚未支持的某些数据类型。
为什么应该开始考虑使用 JVM
最近一段时间我遇到了很多使用动态脚本语言(JavaScript 或者 Go —— 译者注:Go 应该是静态编译型语言)的创业公司。
我在 Bitcoin Space 工作的时候,使用动态语言是非常痛苦的事情。在这些语言里没有安全性的类型,这已经导致了巨大的货币损失。Go 比较少出错,但是在基础层面上给人的体验依然很差,比如说缺少好的调试工具,快速 GC 机制,稳健的管理器以及可靠的分析工具。
过去 15 年或者更长时间里,Java 变得越来越健壮,越来越冗长,甚至有过度设计的迹象,这些变化很大程度上源于它的声誉。企业级 Java 类的名字 就是例证。在很长一段时间里我没有考虑 JVM,我确信这种环境并不适合我。
最终我因为 Android 被迫回到 Java,发现 Java 的开发环境已经改变了。虽然 XML 仍然不合时宜的频繁出现在各种场合,但是一些基础功能十分完善,令人印象深刻。 IntelliJ 是比 Eclipse 更快并且更智能的 IDE。Maven 一出现就得到了迅速的发展,拥有许多原本我想要其它构建 / 依赖系统增加的功能。较新的 Web 框架像 Ninja 和 Play 从类似 Ruby on Rails 的框架中学到了轻量简洁。有大量的库可供使用。硬件性能变得更高以及 JVM 变得更有效率,等等转变。
没有真正改变的是语言本身,Java 代码写起来依然是令人不快的冗长。
现在有了 Kotlin,完全无需承受离开 Java 现有的生态系统的疼苦。你可以编写更富有表现力的代码,但是却比脚本语言更简洁,同时拥有更好的性能和更少的错误。
如果你喜欢 JavaScript,可以尝试 Kotlin 的 JS 后端,或者在 Nashorn JS 引擎里运行你现有的代码。
最后,如果你喜欢 Go 语言是因为它可以编译独立运行的程序,那么试试 javapackager 工具。Kotlin 在本地为每个平台创建了捆绑包,这意味着在 linux 上不需要 JRE 的依赖就可以独立自主的获取 DEBs(linux 的安装包)或者压缩包。当然,它拆包之后不是单个文件而是单个目录,从部署的角度来看并不难操作。
简而言之:如果你之前因为看 Java 不顺眼而忽略了 JVM 的生态系统,那么你应该借着 Kotlin 这门新语言进入这个世界瞧瞧。