Kotlin: cleaning Java bytecode before release
The photo by Pan Xiaozhen (source)

Kotlin: cleaning Java bytecode before release

Kirill Rozov
Published in
3 min readSep 30, 2019

Kotlin removes much boilerplate code that we need to write by hands or generate with an IDE or third party libraries like Dagger 2. Good examples of that in Kotlin is null safety. Thanks to the feature, you don’t have to write if(value != null), but we know that a high power can’t come without a price. The price in Kotlin/JVM is additional Java bytecode.

Let’s check decompiled Java code produced by the Kotlin compiler and find out how to bring down the price.

Function parameters

Kotlin code:

// Sample.kt
fun sample(arg1: String) {
}

Decompiled Java bytecode:

public final class SampleKt {      public static final void sample(@NotNull String arg1) {
Intrinsics.checkParameterIsNotNull(arg1, “arg1”);
}
}

In all of these cases, we can find calls of methods from kotlin.jvm.internal.Intrinsics class. But do we need call of those methods in real production code? Let’s discover another sample.

Platform types

Kotlin has specific kind of types — platform type. You can’t define the type in Kotlin code, only receive it from platform code (e.g., Java code).

// Utils.java
public class Utils {

public static String getValue() {
throw new RuntimeException();
}
}
// Sample.kt
fun sample() {
val value: String = Utils.getValue()
}

Decompiled Java bytecode:

public final class SampleKt {   public static final void sample() {
Intrinsics.checkExpressionValueIsNotNull(
Utils.getValue(), "Utils.getValue()"
);
}
}

It will be good if we have the check in debug code when an app under development and remove the code from release build.

Remove code

ProGuard/R8

You can add the next lines to your ProGuard configuration to remove utility bytecode:

// proguard.pro
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void checkExpressionValueIsNotNull(...);
public static void checkNotNullExpressionValue(...);
public static void checkReturnedValueIsNotNull(...);
public static void checkFieldIsNotNull(...);
public static void checkParameterIsNotNull(...);
}

-assumenosideefeects specifies methods that don’t have any side effects, other than possibly returning a value e.g., logs or check of some state to ensure correct work.

Kotlin compiler arguments

You can remove part of assertions with Kotlin compiler arguments:

  • -Xno-call-assertions — Don’t generate not-null assertions for arguments of platform types
  • -Xno-receiver-assertions — Don’t generate not-null assertion for extension receiver arguments of platform types
  • -Xno-param-assertions — Don’t generate not-null assertions on parameters of methods accessible from Java
// build.gradle
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile)
.all {
kotlinOptions {
freeCompilerArgs += [
'-Xno-call-assertions',
'-Xno-receiver-assertions',
'-Xno-param-assertions'
]
}
}

You can find more about information about Kotlin/JVM compiler arguments here.

Summary

Kotlin is a good language, and all of its features have price. Say that Java has no too much additional bytecode and Kotlin is overhead is stupid. Part of Java features (e.g., Generics and switch for String) work as sugar and only generate other bytecode. Kotlin generates more additional bytecode because you write less in source code.

You can bring down the price with ProGuard/R8 and Kotlin compiler args. I recommend using both ways for release builds. Why only for release build? Because high-quality Android application must be fast, has 60 fps, and has small size. But only for end-users. Don’t remove anything from debug builds to have as much as possible information about incorrect behavior of an app to simplify debug of an app.

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Responses (3)

What are your thoughts?