Hay dos presentaciones sobre Kotlin que disfrute mucho, ambas describen lo mejor de este nuevo lenguaje.
Pero hay una cosa no descripta en esas presentaciones, un detalle sobre cómo Kotlin genera el bytecode:
Las 'inlined functions' de Kotlin no remueven el codigo muerto y lo incluyen en el bytecode.
No es un gran problema ya que Proguard o una herramienta similar remueve el codigo muerto al optimizar las clases, pero en los proyectos Android a veces es necesario deshabilitar las optimizaciones de Proguard debido a la complejidad del proyecto, las librerias usadas, etc.
Un ejemplo mostrando el código y el bytecode de Java y Kotlin
Veamos un ejemplo. Queremos mostrar mensajes de debug, pero no queremos incluir en el bytecode de la release build este código de debug.:
Por ejemplo, mostramos un mensaje de debug dentro de un método llamado "doSomething"
Java |
void doSomething() {
if (BuildConfig.DEBUG) {
System.out.println("Este es un mensaje de debug");
}
}
|
Kotlin |
fun doSomething() {
debug { "Este es un mensaje de debug" }
}
|
+1 para Kotlin, menos código, limpio y simple.
En Java podríamos crear un método static y poner la condición dentro del método, así tendríamos un código limpio como el de Kotlin, pero el compilador de Java no removería todos los llamados a la función y todos los strings de debug se incluirían en la build de release. Incluso optimizando con Proguard los llamados al método se remueven, pero no los parámetros, en realidad depende de la cantidad de pasos en la optimización dentro de proguard.properties.
Veamos ahora el bytecode generado por Java y Kotlin.
Para analizar el bytecode uso el plugin de IntelliJ/Eclipse creado por los autores de la librería ASM, el
OW2 Consortium. Para Kotlin podría usar el plugin que viene con IntelliJ pero no tiene una opción para ignorar los números de linea, los labels y la info de stack.
Bytecode generado cuando la constante
DEBUG es
true
Java |
void doSomething() {
getstatic 'BuildConfig.DEBUG','Z'
ifeq l0
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
ldc "Este es un mensaje de debug"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
l0
return
}
|
Kotlin (el bytecode no es óptimo como el de java, ver el subrayado) |
public final static void doSomething() {
nop
getstatic 'BuildConfig.DEBUG','Z'
ifeq l0
ldc "Este es un mensaje de debug"
astore 0
nop
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
aload 0
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
l0
return
}
|
+1 para Java, menos código que ejecutar.
La diferencia no es grande pero Kotlin incluye algunos nops inútiles el la forma en que carga el string "Este es un mensaje de debug" es redundante, carga el string, lo guarda en el stack (astore 0), y lo carga de nuevo (aload 0).
Bytecode generado cuando la constante
DEBUG es
false
Java |
void doSomething() {
return
}
|
Kotlin (dead code is underlined) |
public final static void doSomething() {
nop
iconst_0
ifeq l0
ldc "Este es un mensaje de debug"
astore 0
nop
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
aload 0
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
l0
return
}
|
+1 para Java, el compilador ignora el código dentro de la condición, pero el
inline de Kotlin no hace ningún chequeo, simplemente copia y pega el código dentro del método, sin tener en cuenta el valor de la constante.
Cómo evitarlo usando gradle flavors
Lo que reomiendo para Kotlin es, en vez de chequear el valor de una constante, usar
gradle flavors, el método para la build de debug muestra el mensaje sin chequear nada, el método de release no hace nada.
Debug flavor | Release flavor |
inline fun debug(func: () -> String) {
println(func())
}
| inline fun debug(func: () -> String) {
}
|
Así el bytecode generado en la build de release es casi nulo .
Kotlin Bytecode: Debug flavor |
public final static void doSomething() {
nop
ldc "Este es un mensaje de debug"
astore 0
nop
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
aload 0
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
return
}
|
Kotlin Bytecode: Release flavor |
public final static void doSomething() {
nop
return
}
|
Kotlin agrega un nop dentro del bytecode de release, pero es mejor que el código muerto incluido antes.
Estamos resolviendo un gran problema con esto? Para la mayoria de los proyectos no, pero estos pequeños detalles pueden ser relevantes en proyectos que necesitan alta performance.