Cómo se admite Java 8 en Android

Hola Habr! Les traigo a su atención una traducción de un maravilloso artículo de una serie de artículos del famoso Jake Worton sobre cómo Android 8 es compatible con Java.



El artículo original está aquí.

Trabajé desde casa durante varios años, y a menudo escuché a mis colegas quejarse de que Android admite diferentes versiones de Java.

Este es un tema bastante complicado. Primero debe decidir a qué nos referimos con "soporte Java en Android", porque en una versión del lenguaje puede haber muchas cosas: características (lambdas, por ejemplo), código de bytes, herramientas, API, JVM, etc.

Cuando las personas hablan sobre la compatibilidad con Java 8 en Android, generalmente se refieren a la compatibilidad con las características del lenguaje. Entonces, comencemos con ellos.

Lambdas


Una de las principales innovaciones de Java 8 fue lambdas.
El código se ha vuelto más conciso y simple, las lambdas nos han salvado de la necesidad de escribir clases anónimas voluminosas utilizando una interfaz con un único método en su interior.

class Java8 { interface Logger { void log(String s); } public static void main(String... args) { sayHi(s -> System.out.println(s)); } private static void sayHi(Logger logger) { logger.log("Hello!"); } } 

Después de compilar esto usando javac y la dx tool heredada, obtenemos el siguiente error:

 $ javac *.java $ ls Java8.java Java8.class Java8$Logger.class $ $ANDROID_HOME/build-tools/28.0.2/dx --dex --output . *.class Uncaught translation error: com.android.dx.cf.code.SimException: ERROR in Java8.main:([Ljava/lang/String;)V: invalid opcode ba - invokedynamic requires --min-sdk-version >= 26 (currently 13) 1 error; aborting 

Este error se produce debido al hecho de que las lambdas usan una nueva instrucción en el invokedynamic : invokedynamic , que se agregó en Java 7. Del texto de error puede ver que Android solo lo admite a partir de la API 26 (Android 8).

No suena muy bien, porque casi nadie lanzará una aplicación con 26 minApi. Para evitar esto, se utiliza el llamado proceso de desugaring , que hace posible el soporte lambda en todas las versiones de la API.

Historia de la desacarización


Ella es muy colorida en el mundo de Android. El objetivo de la desaccharización es siempre el mismo: permitir que las nuevas funciones de lenguaje funcionen en todos los dispositivos.

Inicialmente, por ejemplo, para admitir lambdas en Android, los desarrolladores conectaron el complemento Retrolambda . Utilizó el mismo mecanismo incorporado que la JVM, convirtiendo lambdas a clases, pero lo hizo en tiempo de ejecución y no en tiempo de compilación. Las clases generadas eran muy caras en términos de la cantidad de métodos, pero con el tiempo, después de mejoras y mejoras, este indicador disminuyó a algo más o menos razonable.

Luego, el equipo de Android anunció un nuevo compilador que admitía todas las características de Java 8 y era más productivo. Fue construido sobre el compilador Eclipse Java, pero en lugar de generar un código de bytes Java, generó un código de bytes Dalvik. Sin embargo, su rendimiento aún dejaba mucho que desear.

Cuando se abandonó el nuevo compilador (afortunadamente), el transformador de bytecode de Java en el bytecode de Java, que hizo el malabarismo, se integró en el complemento Android Gradle de Bazel , el sistema de compilación de Google. Y su rendimiento aún era bajo, por lo que la búsqueda de una mejor solución continuó en paralelo.

Y ahora nos dexer : D8 , que supuestamente reemplazaría la dx tool . La desaccharización ahora se realizó durante la conversión de archivos JAR compilados a .dex (dexing). D8 es mucho mejor en rendimiento en comparación con dx , y desde Android Gradle Plugin 3.1 se ha convertido en el dexer predeterminado.

D8


Ahora, usando D8, podemos compilar el código anterior.

 $ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class $ ls Java8.java Java8.class Java8$Logger.class classes.dex 

Para ver cómo el D8 convirtió lambda, puede usar la dexdump tool , que se incluye en el SDK de Android. Mostrará bastante de todo, pero nos centraremos solo en esto:

 $ $ANDROID_HOME/build-tools/28.0.2/dexdump -d classes.dex [0002d8] Java8.main:([Ljava/lang/String;)V 0000: sget-object v0, LJava8$1;.INSTANCE:LJava8$1; 0002: invoke-static {v0}, LJava8;.sayHi:(LJava8$Logger;)V 0005: return-void [0002a8] Java8.sayHi:(LJava8$Logger;)V 0000: const-string v0, "Hello" 0002: invoke-interface {v1, v0}, LJava8$Logger;.log:(Ljava/lang/String;)V 0005: return-void … 

Si aún no ha leído el código de bytes, no se preocupe: gran parte de lo que está escrito aquí puede entenderse intuitivamente.

En el primer bloque, nuestro método main con el índice 0000 obtiene una referencia del campo INSTANCE a la clase INSTANCE Java8$1 . Esta clase se generó durante la . El Java8$1 método principal tampoco contiene ninguna mención del cuerpo de nuestro lambda, por lo tanto, lo más probable es que esté asociado con la clase Java8$1 . El índice 0002 llama al método estático sayHi usando el enlace a INSTANCE . El sayHi requiere Java8$Logger , por lo que parece que Java8$1 implementa esta interfaz. Podemos verificar esto aquí:

 Class #2 - Class descriptor : 'LJava8$1;' Access flags : 0x1011 (PUBLIC FINAL SYNTHETIC) Superclass : 'Ljava/lang/Object;' Interfaces - #0 : 'LJava8$Logger;' 

El indicador SYNTHETIC significa que Java8$1 ha generado la clase Java8$1 y la lista de interfaces que incluye contiene el Java8$Logger .
Esta clase representa nuestra lambda. Si observa la implementación del método de log , no verá el cuerpo de la lambda.

 … [00026c] Java8$1.log:(Ljava/lang/String;)V 0000: invoke-static {v1}, LJava8;.lambda$main$0:(Ljava/lang/String;)V 0003: return-void … 

En cambio, el método static de la clase Java8 se Java8 - lambda$main$0 . Repito, este método se presenta solo en bytecode.

 … #1 : (in LJava8;) name : 'lambda$main$0' type : '(Ljava/lang/String;)V' access : 0x1008 (STATIC SYNTHETIC) [0002a0] Java8.lambda$main$0:(Ljava/lang/String;)V 0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; 0002: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V 0005: return-void 

El indicador SYNTHETIC nuevamente nos dice que este método fue generado, y su bytecode solo contiene el cuerpo lambda: una llamada a System.out.println . La razón por la que el cuerpo lambda está dentro de Java8.class es simple: puede necesitar acceso a miembros private de la clase, a los que la clase generada no tendrá acceso.

Todo lo que necesita para comprender cómo funciona la desaccharización se describe anteriormente. Sin embargo, al mirarlo en el código de bytes de Dalvik, puede ver que todo es mucho más complicado y aterrador allí.

Transformación de fuente


Para comprender mejor cómo se produce la desacrasificación , intentemos paso a paso convertir nuestra clase en algo que funcione en todas las versiones de la API.

Tomemos la misma clase con lambda como base:

 class Java8 { interface Logger { void log(String s); } public static void main(String... args) { sayHi(s -> System.out.println(s)); } private static void sayHi(Logger logger) { logger.log("Hello!"); } } 

Primero, el cuerpo lambda se mueve al método package private del package private .

  public static void main(String... args) { - sayHi(s -> System.out.println(s)); + sayHi(s -> lambda$main$0(s)); } + + static void lambda$main$0(String s) { + System.out.println(s); + } 

Luego se implementa una clase que implementa la interfaz Logger , dentro de la cual se ejecuta un bloque de código del cuerpo lambda.

  public static void main(String... args) { - sayHi(s -> lambda$main$0(s)); + sayHi(new Java8$1()); } @@ } + +class Java8$1 implements Java8.Logger { + @Override public void log(String s) { + Java8.lambda$main$0(s); + } +} 

A continuación, Java8$1 una instancia singleton de Java8$1 , que se almacena en la variable static INSTANCE .

  public static void main(String... args) { - sayHi(new Java8$1()); + sayHi(Java8$1.INSTANCE); } @@ class Java8$1 implements Java8.Logger { + static final Java8$1 INSTANCE = new Java8$1(); + @Override public void log(String s) { 

Aquí está la clase doblada final que se puede usar en todas las versiones de la API:

 class Java8 { interface Logger { void log(String s); } public static void main(String... args) { sayHi(Java8$1.INSTANCE); } static void lambda$main$0(String s) { System.out.println(s); } private static void sayHi(Logger logger) { logger.log("Hello!"); } } class Java8$1 implements Java8.Logger { static final Java8$1 INSTANCE = new Java8$1(); @Override public void log(String s) { Java8.lambda$main$0(s); } } 

Si observa la clase generada en el código de bytes de Dalvik, no encontrará nombres como Java8 $ 1; habrá algo como -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . La razón por la cual se genera ese nombre para la clase, y cuáles son sus ventajas, se basa en un artículo separado.

Soporte lambda nativo


Cuando utilizamos la dx tool para compilar una clase que contiene lambdas, un mensaje de error decía que esto solo funcionaría con 26 API.

 $ $ANDROID_HOME/build-tools/28.0.2/dx --dex --output . *.class Uncaught translation error: com.android.dx.cf.code.SimException: ERROR in Java8.main:([Ljava/lang/String;)V: invalid opcode ba - invokedynamic requires --min-sdk-version >= 26 (currently 13) 1 error; aborting 

Por lo tanto, parece lógico que si tratamos de compilar esto con el —min-api 26 , no se produzca la desacarización.

 $ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --min-api 26 \ --output . \ *.class 

Sin embargo, si .dex archivo .dex , aún se puede encontrar en él -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Por qué ¿Es esto un error D8?

Para responder a esta pregunta, y también por qué siempre se produce la desaccharización, debemos mirar dentro del Java8 de Java8 Java de la clase Java8 .

 $ javap -v Java8.class class Java8 { public static void main(java.lang.String...); Code: 0: invokedynamic #2, 0 // InvokeDynamic #0:log:()LJava8$Logger; 5: invokestatic #3 // Method sayHi:(LJava8$Logger;)V 8: return } … 

Dentro del método main , nuevamente vemos invocados dinámicamente en el índice 0 . El segundo argumento en la llamada es 0 : el índice del método de arranque asociado con él.

Aquí hay una lista de métodos de arranque :

 … BootstrapMethods: 0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:( Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String; Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType; Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;) Ljava/lang/invoke/CallSite; Method arguments: #28 (Ljava/lang/String;)V #29 invokestatic Java8.lambda$main$0:(Ljava/lang/String;)V #28 (Ljava/lang/String;)V 

Aquí el método bootstrap se llama metafactory en la clase java.lang.invoke.LambdaMetafactory . Vive en el JDK y crea clases anónimas sobre la marcha en tiempo de ejecución para lambdas, al igual que D8 las genera en el tiempo de cálculo.

Si nos fijamos en la Android java.lang.invoke
o a las AOSP java.lang.invoke , vemos que esta clase no está en tiempo de ejecución. Es por eso que el malabarismo siempre ocurre en el momento de la compilación, sin importar el minApi que tenga. La VM admite instrucciones de invokedynamic de invokedynamic similares a las invokedynamic , pero el invokedynamic integrado en el JDK no LambdaMetafactory disponible para su uso.

Referencias del método


Junto con lambdas, Java 8 agregó referencias de método: esta es una forma efectiva de crear una lambda cuyo cuerpo hace referencia a un método existente.

Nuestra interfaz Logger es solo un ejemplo. El cuerpo lambda se refería a System.out.println . Convirtamos la lambda en un método de referencia:

  public static void main(String... args) { - sayHi(s -> System.out.println(s)); + sayHi(System.out::println); } 

Cuando lo compilamos y echamos un vistazo al código de bytes, veremos una diferencia con la versión anterior:

 [000268] -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM.log:(Ljava/lang/String;)V 0000: iget-object v0, v1, L-$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM;.f$0:Ljava/io/PrintStream; 0002: invoke-virtual {v0, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V 0005: return-void 

En lugar de llamar al Java8.lambda$main$0 generado, que contiene una llamada a System.out.println , ahora se llama directamente a System.out.println .

Una clase con un lambda ya no es un singleton static , pero por el índice 0000 en el bytecode, vemos que obtenemos un enlace a PrintStream - System.out , que luego se usa para llamar a println en él.

Como resultado, nuestra clase se convirtió en esto:

  public static void main(String... args) { - sayHi(System.out::println); + sayHi(new -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM(System.out)); } @@ } + +class -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM implements Java8.Logger { + private final PrintStream ps; + + -$$Lambda$1Osqr2Z9OSwjseX_0FMQJcCG_uM(PrintStream ps) { + this.ps = ps; + } + + @Override public void log(String s) { + ps.println(s); + } +} 

Métodos Default y static en interfaces


Otro cambio importante y importante que trajo Java 8 fue la capacidad de declarar métodos default y static en las interfaces.

 interface Logger { void log(String s); default void log(String tag, String s) { log(tag + ": " + s); } static Logger systemOut() { return System.out::println; } } 

Todo esto también es compatible con D8. Usando las mismas herramientas que antes, es fácil ver una versión registrada de Logger con métodos default y static . Una de las diferencias con lambdas y method references es que los métodos predeterminados y estáticos se implementan en la VM de Android y, a partir de las 24 API, D8 no los desacoplará .

¿Quizás solo use Kotlin?


Mientras leían el artículo, la mayoría de ustedes probablemente pensó en Kotlin. Sí, admite todas las funciones de Java 8, pero kotlinc implementa de la misma manera que D8, con la excepción de algunos detalles.

Por lo tanto, el soporte de Android para nuevas versiones de Java sigue siendo muy importante, incluso si su proyecto está escrito al 100% en Kotlin.

Es posible que en el futuro Kotlin ya no admita código de bytes Java 6 y Java 7. IntelliJ IDEA , Gradle 5.0 cambió a Java 8. El número de plataformas que se ejecutan en JVM más antiguas está disminuyendo.

Desugaring APIs


Todo este tiempo hablé sobre las características de Java 8, pero no dije nada sobre las nuevas API: transmisiones, CompletableFuture , fecha / hora, etc.

Volviendo al ejemplo de Logger, podemos usar la nueva API de fecha / hora para saber cuándo se enviaron los mensajes.

 import java.time.*; class Java8 { interface Logger { void log(LocalDateTime time, String s); } public static void main(String... args) { sayHi((time, s) -> System.out.println(time + " " + s)); } private static void sayHi(Logger logger) { logger.log(LocalDateTime.now(), "Hello!"); } } 

Compílelo nuevamente con javac y conviértalo al bytecode de Dalvik con D8, que lo desacopla para brindar soporte en todas las versiones de la API.

 $ javac *.java $ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class 

Incluso puede ejecutar esto en su dispositivo para asegurarse de que funciona.

 $ adb push classes.dex /sdcard classes.dex: 1 file pushed. 0.5 MB/s (1620 bytes in 0.003s) $ adb shell dalvikvm -cp /sdcard/classes.dex Java8 2018-11-19T21:38:23.761 Hello 

Si API 26 y superior está en este dispositivo, aparecerá el mensaje Hola. Si no, veremos lo siguiente:

 java.lang.NoClassDefFoundError: Failed resolution of: Ljava/time/LocalDateTime; at Java8.sayHi(Java8.java:13) at Java8.main(Java8.java:9) 

D8 trató con lambdas, un método de referencia, pero no hizo nada para trabajar con LocalDateTime , y esto es muy triste.

Los desarrolladores tienen que usar sus propias implementaciones o envoltorios en la API de fecha / hora, o usar bibliotecas como ThreeTenBP para trabajar con el tiempo, pero ¿por qué no puedes hacer D8 con tus propias manos?

Epílogo


La falta de soporte para todas las nuevas API de Java 8 sigue siendo un gran problema en el ecosistema de Android. Después de todo, es poco probable que cada uno de nosotros nos permita especificar la API de 26 minutos en nuestro proyecto. ¡Las bibliotecas que admiten Android y JVM no pueden permitirse el lujo de utilizar la API que se nos presentó hace 5 años!

Y a pesar de que el soporte de Java 8 ahora es parte de D8, cada desarrollador debe especificar explícitamente la compatibilidad de origen y destino en Java 8. Si escribe sus propias bibliotecas, puede reforzar esta tendencia al diseñar bibliotecas que usen código de bytes Java 8 (incluso si no está utilizando nuevas funciones de idioma).

Se está trabajando mucho en D8, por lo que parece que todo estará bien en el futuro con soporte para funciones de lenguaje. Incluso si escribe solo en Kotlin, es muy importante obligar al equipo de desarrollo de Android a admitir todas las nuevas versiones de Java, mejorar el código de bytes y las nuevas API.

Esta publicación es una versión escrita de mi charla Excavando en D8 y R8 .

Source: https://habr.com/ru/post/478692/


All Articles