Bagaimana Java 8 didukung di Android

Halo, Habr! Saya membawa kepada Anda terjemahan dari artikel yang luar biasa dari serangkaian artikel oleh Jake Worton yang terkenal buruk tentang bagaimana Android 8 didukung oleh Java.



Artikel asli ada di sini

Saya bekerja dari rumah selama beberapa tahun, dan saya sering mendengar rekan saya mengeluh tentang Android yang mendukung berbagai versi Java.

Ini adalah topik yang agak rumit. Pertama, Anda perlu memutuskan apa yang kami maksud dengan β€œdukungan Java di Android”, karena dalam satu versi bahasa bisa ada banyak hal: fitur (lambda, misalnya), bytecode, alat, API, JVM dan sebagainya.

Ketika orang berbicara tentang dukungan Java 8 di Android, mereka biasanya berarti dukungan untuk fitur bahasa. Jadi, mari kita mulai dengan mereka.

Lambdas


Salah satu inovasi utama Java 8 adalah lambdas.
Kode telah menjadi lebih ringkas dan sederhana, lambdas telah menyelamatkan kita dari menulis kelas anonim rumit menggunakan antarmuka dengan metode tunggal di dalamnya.

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

Setelah mengkompilasi ini menggunakan alat javac dan legacy dx tool , kami mendapatkan kesalahan berikut:

 $ 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 

Kesalahan ini terjadi karena fakta bahwa lambdas menggunakan instruksi baru dalam bytecode - invokedynamic , yang ditambahkan di Java 7. Dari teks kesalahan Anda dapat melihat bahwa Android hanya mendukungnya dimulai dengan 26 API (Android 8).

Kedengarannya tidak terlalu bagus, karena hampir tidak ada orang yang akan merilis aplikasi dengan 26 minApi. Untuk menyiasatinya, apa yang disebut proses desugaring digunakan , yang memungkinkan dukungan lambda pada semua versi API.

Sejarah Desaccharization


Dia cukup berwarna di dunia Android. Tujuan desaccharization selalu sama - untuk memungkinkan fitur bahasa baru berfungsi di semua perangkat.

Awalnya, misalnya, untuk mendukung lambdas di Android, pengembang menghubungkan plugin Retrolambda . Dia menggunakan mekanisme built-in yang sama dengan JVM, mengubah lambdas ke kelas, tapi dia melakukannya di runtime, dan tidak pada waktu kompilasi. Kelas yang dihasilkan sangat mahal dalam hal jumlah metode, tetapi seiring waktu, setelah perbaikan dan peningkatan, indikator ini menurun menjadi sesuatu yang lebih atau kurang masuk akal.

Kemudian, tim Android mengumumkan kompiler baru yang mendukung semua fitur Java 8 dan lebih produktif. Itu dibangun di atas kompiler Java Eclipse, tetapi bukannya menghasilkan bytecode Java, itu menghasilkan bytecode Dalvik. Namun, kinerjanya masih banyak yang harus diinginkan.

Ketika kompiler baru (untungnya) ditinggalkan, Java bytecode transformer di bytecode Java, yang melakukan juggling, diintegrasikan ke dalam Plugin Android Gradle dari Bazel , sistem build Google. Dan kinerjanya masih rendah, sehingga pencarian solusi yang lebih baik dilanjutkan secara paralel.

Dan sekarang kami diberi dexer - D8 , yang seharusnya menggantikan dx tool . Desaccharization sekarang dilakukan selama konversi file JAR yang dikompilasi menjadi .dex (dexing). D8 jauh lebih baik dalam kinerja dibandingkan dengan dx , dan karena Android Gradle Plugin 3.1 telah menjadi dexer default.

D8


Sekarang, menggunakan D8, kita dapat mengkompilasi kode di atas.

 $ 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 

Untuk melihat bagaimana D8 mengkonversi lambda, Anda dapat menggunakan dexdump tool , yang termasuk dalam Android SDK. Ini akan menampilkan cukup banyak segalanya, tetapi kami hanya akan fokus pada ini:

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

Jika Anda belum membaca bytecode, jangan khawatir: banyak dari apa yang ditulis di sini dapat dipahami secara intuitif.

Di blok pertama, metode main kami dengan indeks 0000 mendapat referensi dari bidang INSTANCE ke kelas INSTANCE Java8$1 . Kelas ini dihasilkan selama . Metode bytecode utama juga tidak mengandung penyebutan tubuh lambda kita, oleh karena itu, kemungkinan besar, ini terkait dengan kelas Java8$1 . Indeks 0002 kemudian memanggil metode statis sayHi menggunakan tautan ke INSTANCE . sayHi membutuhkan Java8$Logger , sehingga tampaknya Java8$1 mengimplementasikan antarmuka ini. Kami dapat memverifikasi ini di sini:

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

Bendera SYNTHETIC berarti kelas Java8$1 telah dibuat dan daftar antarmuka yang disertakan berisi Java8$Logger .
Kelas ini mewakili lambda kita. Jika Anda melihat implementasi metode log , Anda tidak akan melihat tubuh lambda.

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

Sebaliknya, metode static dari kelas Java8 - lambda$main$0 . Saya ulangi, metode ini hanya disajikan dalam 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 

Bendera SYNTHETIC lagi memberitahu kita bahwa metode ini dihasilkan, dan bytecode-nya hanya berisi tubuh lambda: panggilan ke System.out.println . Alasan mengapa badan lambda ada di dalam Java8.class sederhana - mungkin perlu mengakses anggota kelas pribadi, yang tidak dapat diakses oleh kelas yang dihasilkan oleh kelas.

Semua yang Anda butuhkan untuk memahami cara kerja desakcharisasi dijelaskan di atas. Namun, melihatnya dalam bytecode Dalvik, Anda dapat melihat bahwa semuanya jauh lebih rumit dan menakutkan di sana.

Transformasi Sumber


Untuk lebih memahami bagaimana desakarida terjadi, mari kita coba langkah demi langkah untuk mengubah kelas kita menjadi sesuatu yang akan bekerja pada semua versi API.

Mari kita ambil kelas yang sama dengan lambda sebagai dasar:

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

Pertama, tubuh lambda dipindahkan ke metode 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); + } 

Kemudian sebuah kelas diimplementasikan yang mengimplementasikan antarmuka Logger , di mana blok kode dari tubuh lambda dieksekusi.

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

Selanjutnya, instance singleton dari Java8$1 , yang disimpan dalam variabel variabel 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) { 

Berikut ini adalah kelas yang dijuluki akhir yang dapat digunakan pada semua versi 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); } } 

Jika Anda melihat kelas yang dihasilkan dalam bytecode Dalvik, Anda tidak akan menemukan nama seperti Java8 $ 1 - akan ada sesuatu seperti -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Alasan mengapa penamaan tersebut dihasilkan untuk kelas, dan apa kelebihannya, menarik ke artikel terpisah.

Dukungan lambda asli


Ketika kami menggunakan dx tool untuk mengkompilasi kelas yang berisi lambdas, pesan kesalahan mengatakan bahwa ini hanya akan bekerja dengan 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 

Oleh karena itu, tampaknya logis bahwa jika kita mencoba untuk mengkompilasi ini dengan β€”min-api 26 , maka desaccharization tidak akan terjadi.

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

Namun, jika kita membuang file .dex , maka masih dapat ditemukan di dalamnya -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Kenapa begitu Apakah ini bug D8?

Untuk menjawab pertanyaan ini, dan mengapa desakarisasi selalu terjadi , kita perlu melihat ke dalam kode kode Java dari kelas 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 } … 

Di dalam metode main , kita kembali melihat invokedynamic pada indeks 0 . Argumen kedua dalam panggilan adalah 0 - indeks metode bootstrap yang terkait dengannya.

Berikut adalah daftar metode bootstrap :

 … 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 

Di sini metode bootstrap disebut metafactory di kelas java.lang.invoke.LambdaMetafactory . Dia tinggal di JDK dan menciptakan kelas on-the-fly anonim dalam runtime untuk lambdas, seperti D8 yang menghasilkannya dalam waktu komputasi.

Jika Anda melihat Android java.lang.invoke
atau ke AOSP java.lang.invoke , kita melihat bahwa kelas ini tidak di runtime. Itu sebabnya de-juggling selalu terjadi pada waktu kompilasi, tidak peduli berapa minApi yang Anda miliki. VM mendukung instruksi bytecode yang mirip dengan invokedynamic , tetapi invokedynamic built-in ke JDK tidak tersedia untuk digunakan.

Referensi metode


Seiring dengan lambdas, Java 8 menambahkan referensi metode - ini adalah cara yang efektif untuk membuat lambda yang tubuhnya mereferensikan metode yang ada.

Antarmuka Logger kami hanyalah contoh seperti itu. Tubuh lambda disebut System.out.println . Mari kita ubah lambda menjadi metode referensi:

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

Ketika kita mengompilasinya dan melihat bytecode, kita akan melihat satu perbedaan dengan versi sebelumnya:

 [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 

Alih-alih memanggil Java8.lambda$main$0 dihasilkan Java8.lambda$main$0 , yang berisi panggilan ke System.out.println , sekarang System.out.println dipanggil langsung.

Kelas dengan lambda bukan lagi singleton static , tetapi dengan indeks 0000 dalam bytecode, kita melihat bahwa kita mendapatkan tautan ke PrintStream - System.out , yang kemudian digunakan untuk memanggil println di atasnya.

Akibatnya, kelas kami berubah menjadi ini:

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

Metode Default dan static di antarmuka


Perubahan penting dan besar lainnya yang dibawa Java 8 adalah kemampuan untuk mendeklarasikan metode default dan static di antarmuka.

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

Semua ini juga didukung oleh D8. Menggunakan alat yang sama seperti sebelumnya, mudah untuk melihat versi Logger yang masuk dengan metode default dan static . Salah satu perbedaan dengan lambdas dan method references adalah bahwa metode default dan statis diterapkan di VM Android dan, dimulai dengan 24 API, D8 tidak akan memisahkan mereka.

Mungkin hanya menggunakan Kotlin?


Saat membaca artikel, sebagian besar dari Anda mungkin berpikir tentang Kotlin. Ya, itu mendukung semua fitur Java 8, tetapi mereka diimplementasikan oleh kotlinc dengan cara yang sama seperti D8, dengan pengecualian beberapa detail.

Oleh karena itu, dukungan Android untuk versi baru Java masih sangat penting, bahkan jika proyek Anda 100% ditulis di Kotlin.

Ada kemungkinan bahwa di masa depan Kotlin tidak lagi mendukung bytecode Java 6 dan Java 7. IntelliJ IDEA , Gradle 5.0 beralih ke Java 8. Jumlah platform yang berjalan pada JVM lama semakin berkurang.

Desugaring APIs


Selama ini saya berbicara tentang fitur Java 8, tetapi tidak mengatakan apa-apa tentang API baru - stream, CompletableFuture , tanggal / waktu dan sebagainya.

Kembali ke contoh Logger, kita dapat menggunakan API tanggal / waktu baru untuk mengetahui kapan pesan dikirim.

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

Kompilasi lagi dengan javac dan konversikan ke bytecode Dalvik dengan D8, yang memisahkannya untuk dukungan pada semua versi API.

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

Anda bahkan dapat menjalankan ini di perangkat Anda untuk memastikan itu berfungsi.

 $ 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 

Jika API 26 dan di atasnya ada di perangkat ini, pesan Hello akan muncul. Jika tidak, kita akan melihat yang berikut:

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

D8 berurusan dengan lambdas, metode referensi, tetapi tidak melakukan apa pun untuk bekerja dengan LocalDateTime , dan ini sangat menyedihkan.

Pengembang harus menggunakan implementasi atau pembungkus mereka sendiri pada api tanggal / waktu, atau menggunakan perpustakaan seperti ThreeTenBP untuk bekerja dengan waktu, tetapi mengapa Anda tidak dapat melakukan D8 dengan tangan Anda sendiri?

Epilog


Kurangnya dukungan untuk semua Java 8 API baru tetap menjadi masalah besar di ekosistem Android. Memang, tidak mungkin masing-masing dari kita dapat mengizinkan kita untuk menentukan 26 menit API dalam proyek kita. Perpustakaan yang mendukung Android dan JVM tidak mampu menggunakan API yang diperkenalkan kepada kami 5 tahun yang lalu!

Dan meskipun dukungan Java 8 sekarang merupakan bagian dari D8, setiap pengembang masih harus secara eksplisit menentukan kompatibilitas sumber dan target di Java 8. Jika Anda menulis pustaka Anda sendiri, Anda dapat memperkuat tren ini dengan meletakkan pustaka yang menggunakan bytecode Java 8. (bahkan jika Anda tidak menggunakan fitur bahasa baru).

Banyak pekerjaan sedang dilakukan pada D8, jadi sepertinya semuanya akan baik-baik saja di masa depan dengan dukungan untuk fitur bahasa. Bahkan jika Anda hanya menulis di Kotlin, sangat penting untuk memaksa tim pengembangan Android untuk mendukung semua versi Java yang baru, meningkatkan bytecode dan API baru.

Posting ini adalah versi tertulis dari ceramah saya Menggali D8 dan R8 .

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


All Articles