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 siniSaya 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
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 .