前言
Kotlin语法特性有哪些会造成额外的性能开销呢?我们在开发过程中如何去避开这些问题,写出高效Kotlin代码,这就是这篇文章的目的。
开发环境
Kotlin版本:1.3.20
平台:在JVM / Android上的实现
分析工具:Kotlin字节码分析工具
一、inline关键字
class FunctionTest {
private fun upload(filePath: String, onSuccess: () -> Unit) {
print("start")
onSuccess()
}
@Test
fun testUpload() {
val filePath = "/test.mp4"
upload(filePath) {
print("success")
}
}
}
转化成同等的java代码
public final class FunctionTest {
private final void upload(String filePath, Function0 onSuccess) {
String var3 = "start";
System.out.print(var3);
onSuccess.invoke();
}
@Test
public final void testUpload() {
String filePath = "/test.mp4";
this.upload(filePath, (Function0)null.INSTANCE);
}
}
可以看到我们使用高阶函数时,实际上会把lambda块封装成Function0对象,最后invoke()才调用结束,多出了运行时的损耗。通过对高阶函数添加inline,在编译时会把高函数复制到调用的地方,减少了运行时的开销。
class FunctionTest {
private inline fun upload(filePath: String, onSuccess: () -> Unit) {
print("start")
onSuccess()
}
@Test
fun testUpload() {
val filePath = "/test.mp4"
upload(filePath) {
print("success")
}
}
}
同等的java代码,使用inline效果如下。
public final class FunctionTest {
//编译时upload()的函数体会被替换到调用的地方
private final void upload(String filePath, Function0 onSuccess) {
System.out.print("start");
onSuccess.invoke();
}
@Test
public final void testUpload() {
String filePath = "/test.mp4";
//来自upload()函数体
System.out.print("start");
System.out.print("success");
}
}
注意:
- 如果高阶函数lambda参数是变量时,即使声明inline的函数也会失效,如下
class FunctionTest {
private inline fun upload(filePath: String, onSuccess: () -> Unit) {
print("start")
onSuccess()
}
@Test
fun testUpload() {
val filePath = "/test.mp4"
val a = { print("success") }
upload(filePath, a)
}
}
同等java代码
public final class FunctionTest {
private final void upload(String filePath, Function0 onSuccess) {
System.out.print("start");
onSuccess.invoke();
}
@Test
public final void testUpload() {
String filePath = "/test.mp4";
Function0 a = (Function0)null.INSTANCE;
//替换了部分,还是需要调用invoke()
System.out.print("start");
a.invoke();
}
}
- inline函数会中的lambda块内的return被复制过去,从而导致整个函数的返回。应该使用return@高阶函数名,指明是在lambda块内返回。
class FunctionTest {
private inline fun upload(filePath: String, onSuccess: () -> Unit) {
print("start")
onSuccess()
}
@Test
fun testUpload() {
val filePath = "/test.mp4"
upload(filePath) {
print("success")
// return会导致整个外部函数的返回,应该使用return@高阶函数名,指明是在lambda块内返回。
return@upload
}
print("end")
}
}
二、Foreach性能开销
fun forEach() {
(1..10).forEach {
print(it)
}
}
同等java代码
public final void forEach() {
byte var1 = 1;
Iterable $receiver$iv = (Iterable)(new IntRange(var1, 10));
Iterator var2 = $receiver$iv.iterator();
while(var2.hasNext()) {
int element$iv = ((IntIterator)var2).nextInt();
int var5 = false;
System.out.print(element$iv);
}
}
从上面代码可以看到,创建 IntRange 对象外,还有IntIterator 的开销。所以对于一个范围使用一个简单的 for 循环,而不是forEach,来减少迭代器的开销。
三、集合链式操作使用Sqeuence来提高集合处理速度
对一个User集合先做map操作最后做filter操作,代码如下
class SqeuenceTest {
fun test() {
val list = listOf(1, 2, 3, 4, 5, 6)
list.map{ it * 2 }.filter { it % 3 == 0 }.average()
}
}
程序处理过程:users调用map会产生中间List<String>集合,接着调用filter,产生中间List<String>集合,最后调用average来产生我们需要的结果List<String>集合,如图:
image.png
通过将集合转化成Sequence后,能够解决产生中间集合问题。
优化后代码如下
class SqeuenceTest {
fun test() {
val list = listOf(1, 2, 3, 4, 5, 6)
list.asSequence().map{ it * 2 }.filter { it % 3 == 0 }.average()
}
}
Sequence过程:Sequence先对单个元素进行一系列的整体操作(简单理解为合并map和filter操作),然后再对下一个元素做进行一系列的整体操作,直到处理完集合中所有元素为止,最后获取Sequence<T>结果集合。
image.png
结论:对于集合链式操作时,我们需要把集合先转化成Sequence,再进行操作,从而减少中间集合和循环次数,最终提高集合处理速度。
四、const关键字
伴生对象通过在类中使用companion object来创建,用来替代静态成员,类似于Java中的静态内部类。所以在伴生对象中声明常量是很常见的做法,但如果写法不对,可能就会产生额外开销。比如下面这段声明TAG常量的代码:
class CompanionTest {
companion object {
private const val TAG = "CompanionTest"
private val tag = "CompanionTest"
}
@Test
fun print() {
print(TAG)
print(tag)
}
}
将这段Kotlin代码转化成等同的Java代码后
public final class CompanionTest {
private static final String TAG = "CompanionTest";
private static final String tag = "CompanionTest";
public static final CompanionTest.Companion Companion = new CompanionTest.Companion((DefaultConstructorMarker)null);
@Test
public final void print() {
String var1 = "CompanionTest";
System.out.print(var1);
var1 = tag;
System.out.print(var1);
}
public static final class Companion {
private Companion() {
}
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
从上面代码可以看到伴生对象常量使用const,在编译时会替换成常量值,不需要进行赋值。
五、lazy()
lazy()委托属性可以用于只读属性的惰性加载,但我们经常忽略lazy的可选的model参数,lazy(mode)的源码如下:
public actual fun <T> lazy(initializer: () -> T): Lazy<T> =
SynchronizedLazyImpl(initializer)
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
我们先了解三种mode的作用:
- LazyThreadSafetyMode.SYNCHRONIZED:初始化属性时会有双重锁检查,保证该值只在一个线程中计算,并且所有线程会得到相同的值。
- LazyThreadSafetyMode.PUBLICATION:多个线程会同时执行,初始化属性的函数会被多次调用,但是只有第一个返回的值被当做委托属性的值。
- LazyThreadSafetyMode.NONE:没有双重锁检查,不应该用在多线程下。
从源码中我们可以看到lazy()默认的是LazyThreadSafetyMode.SYNCHRONIZED,在不需要线程安全的场景下,会造成不必要的线程安全的开销,比如Android中预知只在主线程,我们可以指定LazyThreadSafetyMode.NONE,来避免不必要的损耗。
六、基本类型数组
class BasicTypeArray {
val intArray = intArrayOf(1)
val array = arrayOf(1)
val nullArray = arrayOf<Int?>(null)
}
将这段Kotlin代码转化成等同的Java代码后
public final class BasicTypeArray {
@NotNull
private final int[] intArray = new int[]{1};
@NotNull
private final Integer[] array = new Integer[]{1};
@NotNull
private final Integer[] nullArray = new Integer[]{(Integer)null};
}
我们可以看到后面两种方式对基本类型做了装箱处理,产生了额外的开销。
所以我们使用非空的数组时,应该使用XXXArray,避免自动装箱。
参考文章:
https://tech.meituan.com/2018/07/05/kotlin-code-inspect.html
https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
https://juejin.im/post/5b28f4946fb9a00e3a5a9b8c
网友评论