Wie Java 8 auf Android unterstützt wird

Hallo habr Ich mache Sie auf eine Übersetzung eines wunderbaren Artikels aus einer Reihe von Artikeln des berüchtigten Jake Worton aufmerksam, in denen es darum geht, wie Android 8 von Java unterstützt wird.



Der Originalartikel ist hier

Ich habe mehrere Jahre von zu Hause aus gearbeitet und oft gehört, wie sich meine Kollegen darüber beschwert haben, dass Android verschiedene Java-Versionen unterstützt.

Dies ist ein ziemlich kompliziertes Thema. Zuerst müssen Sie entscheiden, was wir unter „Java-Unterstützung in Android“ verstehen, da es in einer Version der Sprache viele Dinge geben kann: Funktionen (z. B. Lambdas), Bytecode, Tools, APIs, JVM usw.

Wenn von Java 8-Unterstützung in Android die Rede ist, bedeutet dies in der Regel Unterstützung für Sprachfunktionen. Fangen wir also mit ihnen an.

Lambdas


Eine der Hauptinnovationen von Java 8 waren Lambdas.
Der Code ist übersichtlicher und einfacher geworden, und Lambdas haben uns das Schreiben umständlicher anonymer Klassen über eine Schnittstelle mit einer einzigen darin enthaltenen Methode erspart.

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!"); } } 

Nach dem Kompilieren mit Javac und dem Legacy- dx tool wird der folgende Fehler angezeigt:

 $ 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 

Dieser Fehler ist darauf zurückzuführen, dass Lambdas eine neue Anweisung im Bytecode verwenden - invokedynamic , der in Java 7 hinzugefügt wurde. Aus dem Fehlertext geht hervor, dass Android diese Anweisung nur ab der 26-API (Android 8) unterstützt.

Es klingt nicht sehr gut, da kaum jemand eine Anwendung mit 26 minApi veröffentlicht. Um dies zu umgehen , wird der sogenannte Desugaring- Prozess verwendet , der Lambda-Unterstützung für alle Versionen der API ermöglicht.

Geschichte der Entzuckerung


Sie ist ziemlich bunt in der Android-Welt. Das Ziel der Entzuckerung ist immer dasselbe - neue Sprachfunktionen können auf allen Geräten verwendet werden.

Um beispielsweise Lambdas in Android zu unterstützen, haben Entwickler zunächst das Retrolambda- Plugin eingebunden . Er verwendete denselben eingebauten Mechanismus wie die JVM, konvertierte Lambdas in Klassen, tat dies jedoch zur Laufzeit und nicht zur Kompilierungszeit. Die generierten Klassen waren in Bezug auf die Anzahl der Methoden sehr teuer, aber im Laufe der Zeit ging dieser Indikator nach Verbesserungen und Verbesserungen auf etwas mehr oder weniger Vernünftiges zurück.

Dann kündigte das Android-Team einen neuen Compiler an , der alle Java 8-Funktionen unterstützt und produktiver ist. Es wurde auf dem Eclipse Java-Compiler erstellt, aber anstatt einen Java-Bytecode zu generieren, wurde ein Dalvik-Bytecode generiert. Die Leistung ließ jedoch noch zu wünschen übrig.

Als der neue Compiler (zum Glück) aufgegeben wurde, wurde der Java-Bytecode-Umsetzer im Java-Bytecode, der die Jonglage durchführte, in das Android Gradle Plugin von Bazel , Googles Build-System, integriert. Da die Leistung immer noch gering war, wurde die Suche nach einer besseren Lösung parallel fortgesetzt.

Und jetzt wurde uns dexer - D8 , der das dexer dx tool ersetzen sollte. Die Desaccharisierung wurde nun während der Konvertierung kompilierter JAR-Dateien nach .dex (Dexing) durchgeführt. D8 hat eine viel bessere Leistung als DX und ist seit Android Gradle Plugin 3.1 zum Standard-Dexer geworden.

D8


Jetzt können wir mit D8 den obigen Code kompilieren.

 $ 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 

Um zu sehen, wie der D8 Lambda konvertiert, können Sie das dexdump tool , das im Android SDK enthalten ist. Es wird eine Menge von allem angezeigt, aber wir werden uns nur darauf konzentrieren:

 $ $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 … 

Wenn Sie den Bytecode noch nicht gelesen haben, machen Sie sich keine Sorgen: Vieles, was hier geschrieben ist, kann intuitiv verstanden werden.

Im ersten Block erhält unsere Hauptmethode mit dem Index 0000 eine Referenz aus dem Feld INSTANCE auf die INSTANCE Klasse Java8$1 . Diese Klasse wurde während der erzeugt. Der Bytecode der Hauptmethode enthält auch keine Erwähnung des Körpers unseres Lambda, daher ist er höchstwahrscheinlich mit der Java8$1 Klasse Java8$1 . Der Index 0002 ruft dann die statische Methode sayHi über die Verknüpfung mit INSTANCE . Die sayHi erfordert Java8$Logger , daher scheint Java8$1 diese Schnittstelle zu implementieren. Das können wir hier überprüfen:

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

Das SYNTHETIC Flag bedeutet, dass die Java8$1 Klasse generiert wurde und die darin enthaltene Schnittstellenliste den Java8$Logger .
Diese Klasse repräsentiert unser Lambda. Wenn Sie sich die Implementierung der log Methode ansehen, werden Sie den Körper des Lambda nicht sehen.

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

Stattdessen heißt die static Methode der Java8 Klasse Java8 lambda$main$0 . Ich wiederhole, diese Methode wird nur in Bytecode dargestellt.

 … #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 

Das SYNTHETIC Flag gibt erneut an, dass diese Methode generiert wurde und ihr Bytecode nur den Lambda-Body enthält: einen Aufruf von System.out.println . Der Grund, warum der Lambda-Body in Java8.class enthalten ist, ist einfach: Möglicherweise muss er auf private Member der Klasse zugreifen, auf die die generierte Klasse keinen Zugriff hat.

Alles, was Sie brauchen, um zu verstehen, wie die Desaccharisierung funktioniert, ist oben beschrieben. Anhand des Dalvik-Bytecodes können Sie jedoch erkennen, dass dort alles viel komplizierter und erschreckender ist.

Quelltransformation


Um besser zu verstehen, wie die Desaccharisierung erfolgt, versuchen wir Schritt für Schritt, unsere Klasse in etwas zu konvertieren, das auf allen Versionen der API funktioniert.

Nehmen wir die gleiche Klasse mit Lambda als Basis:

 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!"); } } 

Zunächst wird der Lambda-Körper in die Methode package private verschoben.

  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); + } 

Anschließend wird eine Klasse implementiert, die die Logger Schnittstelle implementiert, in der ein Codeblock aus dem Lambda-Body ausgeführt wird.

  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); + } +} 

Als nächstes wird eine Singleton-Instanz von Java8$1 , die in der static Variablen 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) { 

Hier ist die letzte überspielte Klasse, die für alle Versionen der API verwendet werden kann:

 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); } } 

Wenn Sie sich die generierte Klasse im Dalvik-Bytecode ansehen, werden Sie keine Namen wie Java8 $ 1 finden - es wird so etwas wie -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Der Grund, warum eine solche Benennung für die Klasse generiert wird, und welche Vorteile dies hat, wird in einem separaten Artikel erläutert.

Eingeborene Lambda-Unterstützung


Als wir mit dem dx tool eine Klasse kompilierten, die Lambdas enthielt, wurde in einer Fehlermeldung darauf hingewiesen, dass dies nur mit 26 APIs funktioniert.

 $ $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 

Daher erscheint es logisch, dass beim Kompilieren mit dem —min-api 26 keine Desaccharisierung erfolgt.

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

Wenn wir jedoch die .dex Datei .dex , kann sie immer noch darin gefunden werden -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Warum so? Ist das ein D8-Bug?

Um diese Frage zu beantworten und auch, warum immer eine Desaccharisierung stattfindet , müssen wir den Java-Bytecode der Java8 Klasse untersuchen.

 $ 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 } … 

In der main wir wieder invokedynamic bei Index 0 . Das zweite Argument im Aufruf ist 0 - der Index der ihm zugeordneten Bootstrap- Methode.

Hier ist eine Liste der Bootstrap- Methoden:

 … 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 

Hier wird die Bootstrap- Methode in der Klasse java.lang.invoke.LambdaMetafactory als metafactory . Er lebt im JDK und erstellt zur Laufzeit anonyme On-the-Fly-Klassen für Lambdas, so wie D8 sie in Rechenzeit generiert.

Wenn Sie in der Android java.lang.invoke suchen
In den AOSP java.lang.invoke wir, dass sich diese Klasse nicht in der Laufzeit befindet. Das ist der Grund, warum das De-Jonglieren immer zur Kompilierungszeit stattfindet, egal welche minApi Sie haben. Die VM unterstützt Bytecode-Anweisungen ähnlich wie invokedynamic , die im JDK integrierte invokedynamic steht jedoch nicht zur Verfügung.

Methodenreferenzen


Neben Lambdas wurden in Java 8 Methodenreferenzen hinzugefügt. Dies ist eine effektive Möglichkeit, ein Lambda zu erstellen, dessen Körper auf eine vorhandene Methode verweist.

Unsere Logger Oberfläche ist nur ein Beispiel. Der Lambda-Körper bezieht sich auf System.out.println . Lassen Sie uns das Lambda in eine Referenzmethode umwandeln:

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

Wenn wir es kompilieren und uns den Bytecode ansehen, werden wir einen Unterschied zur vorherigen Version feststellen:

 [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 

Anstatt das generierte Java8.lambda$main$0 , das einen Aufruf von System.out.println , wird jetzt System.out.println direkt aufgerufen.

Eine Klasse mit einem Lambda ist kein static Singleton mehr, aber durch den Index 0000 im Bytecode sehen wir, dass wir einen Link zu PrintStream - System.out , der dann verwendet wird, um println darauf println .

Infolgedessen hat sich unsere Klasse wie folgt entwickelt:

  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); + } +} 

Default und static Methoden in Schnittstellen


Eine weitere wichtige und wichtige Änderung, die Java 8 mit sich brachte, war die Möglichkeit, default und static Methoden in Schnittstellen zu deklarieren.

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

All dies wird auch von D8 unterstützt. Mit den gleichen Tools wie zuvor ist es einfach, eine angemeldete Version von Logger mit default und static Methoden anzuzeigen. Einer der Unterschiede zu Lambdas und method references besteht darin, dass die Standardmethoden und die statischen Methoden in der Android-VM implementiert sind und D8 sie ab der 24-API nicht entkoppelt .

Vielleicht einfach Kotlin benutzen?


Wahrscheinlich haben die meisten von Ihnen beim Lesen des Artikels an Kotlin gedacht. Ja, es werden alle Java 8-Funktionen unterstützt, aber sie werden von kotlinc mit Ausnahme einiger Details auf dieselbe Weise wie D8 implementiert.

Daher ist die Android-Unterstützung für neue Java-Versionen immer noch sehr wichtig, auch wenn Ihr Projekt zu 100% in Kotlin geschrieben ist.

Möglicherweise wird Kotlin in Zukunft den Bytecode von Java 6 und Java 7. IntelliJ IDEA , Gradle 5.0 auf Java 8 umgestellt. Die Anzahl der Plattformen, die auf älteren JVMs ausgeführt werden, nimmt ab.

APIs desugarieren


Die ganze Zeit habe ich über Java 8-Funktionen gesprochen, aber nichts über die neuen APIs gesagt - Streams, CompletableFuture , Datum / Uhrzeit und so weiter.

Zurück zum Logger-Beispiel können wir die neue Datums- / Uhrzeit-API verwenden, um herauszufinden, wann Nachrichten gesendet wurden.

 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!"); } } 

Kompilieren Sie es erneut mit javac und konvertieren Sie es mit D8 in den Dalvik-Bytecode, wodurch es für die Unterstützung aller API-Versionen entkoppelt wird .

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

Sie können dies sogar auf Ihrem Gerät ausführen, um sicherzustellen, dass es funktioniert.

 $ 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 

Wenn API 26 und höher auf diesem Gerät installiert ist, wird die Meldung Hallo angezeigt. Wenn nicht, sehen wir folgendes:

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

D8 befasste sich mit Lambdas, einer Referenzmethode, funktionierte jedoch nicht mit LocalDateTime , und das ist sehr traurig.

Entwickler müssen ihre eigenen Implementierungen oder Wrapper für Datum / Uhrzeit-API verwenden oder Bibliotheken wie ThreeTenBP , um mit der Zeit zu arbeiten. Warum können Sie D8 nicht mit Ihren eigenen Händen ThreeTenBP ?

Nachwort


Der Mangel an Unterstützung für alle neuen Java 8-APIs bleibt ein großes Problem im Android-Ökosystem. In der Tat ist es unwahrscheinlich, dass jeder von uns die 26-minütige API in seinem Projekt festlegen kann. Bibliotheken, die sowohl Android als auch JVM unterstützen, können es sich nicht leisten, die vor 5 Jahren eingeführte API zu verwenden!

Auch wenn die Java 8-Unterstützung jetzt Teil von D8 ist, sollte jeder Entwickler die Quell- und Zielkompatibilität in Java 8 explizit angeben. Wenn Sie Ihre eigenen Bibliotheken schreiben, können Sie diesen Trend verstärken, indem Sie Bibliotheken mit Java 8-Bytecode auslegen (Auch wenn Sie keine neuen Sprachfunktionen verwenden).

An D8 wird viel gearbeitet, so dass es den Anschein hat, als ob in Zukunft alles in Ordnung sein wird, wenn Sprachfunktionen unterstützt werden. Selbst wenn Sie nur auf Kotlin schreiben, ist es sehr wichtig, das Android-Entwicklungsteam zu zwingen, alle neuen Versionen von Java zu unterstützen, Bytecode und neue APIs zu verbessern.

Dieser Beitrag ist eine schriftliche Version meines Vortrags Digging in D8 und R8 .

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


All Articles