مرحبا يا هبر! أوجه انتباهكم إلى ترجمة لمقال رائع من سلسلة من المقالات التي قام بها
جيك وورتون سيئ السمعة حول كيفية دعم Android 8 لجافا.
المقال الأصلي هناعملت من المنزل لعدة سنوات ، وكثيراً ما سمعت زملائي يشكون من دعم Android للإصدارات المختلفة من Java.
هذا موضوع معقد إلى حد ما. يجب أولاً تحديد ما نعنيه بـ "دعم Java في Android" ، لأنه في إصدار واحد من اللغة يمكن أن يكون هناك الكثير من الأشياء: الميزات (على سبيل المثال ، lambdas) ، الرمز الفرعي ، الأدوات ، واجهات برمجة التطبيقات ، JVM وما إلى ذلك.
عندما يتحدث الأشخاص عن دعم Java 8 في Android ، فإنهم عادة ما يعنيون دعم ميزات اللغة. لذلك ، دعونا نبدأ معهم.
امدا
واحدة من الابتكارات الرئيسية لجافا 8 كان lambdas.
لقد أصبح الرمز أكثر إيجازًا وبساطة ، لقد أنقذنا lambdas من كتابة فصول مجهولة مجهولة باستخدام واجهة مع طريقة واحدة في الداخل.
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!"); } }
بعد تجميع هذا باستخدام
dx tool
javac و legacy
dx tool
، حصلنا على الخطأ التالي:
$ 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
يحدث هذا الخطأ بسبب حقيقة أن lambdas تستخدم تعليمة جديدة في bytecode -
invokedynamic
، والتي تمت إضافتها في Java 7. من نص الخطأ ، يمكنك أن ترى أن Android يدعمها فقط بدءًا من 26 API (Android 8).
لا يبدو هذا جيدًا جدًا ، لأنه بالكاد سيصدر أي شخص تطبيقًا مدته 26 دقيقة. للتغلب على هذا ، يتم استخدام ما يسمى عملية
desugaring ، مما يجعل دعم lambda ممكن على جميع إصدارات API.
تاريخ انحلال
إنها ملونة جميلة في عالم Android. هدف إلغاء التشفير دائمًا هو نفسه - السماح لميزات اللغة الجديدة بالعمل على جميع الأجهزة.
في البداية ، على سبيل المثال ، لدعم lambdas في Android ، قام مطورو البرامج بتوصيل المكون الإضافي
Retrolambda . لقد استخدم نفس الآلية المضمنة مثل JVM ، حيث قام بتحويل lambdas إلى فصول ، لكنه فعل ذلك في وقت التشغيل ، وليس في وقت الترجمة. كانت الفئات التي تم إنشاؤها مكلفة للغاية من حيث عدد الطرق ، ولكن بمرور الوقت ، وبعد التحسينات والتحسينات ، انخفض هذا المؤشر إلى شيء أكثر أو أقل منطقية.
بعد ذلك ،
أعلن فريق Android
عن مترجم جديد يدعم جميع ميزات Java 8 وكان أكثر إنتاجية. تم إنشاؤه على أعلى برنامج التحويل البرمجي Java Eclipse Java ، ولكن بدلاً من إنشاء رمز جافا جافا ، قام بإنشاء رمز Dalvik bytecode. ومع ذلك ، أدائها لا يزال يترك الكثير مما هو مرغوب فيه.
عندما تم التخلي عن المحول البرمجي الجديد (لحسن الحظ) ،
تم دمج محول Java bytecode في Java bytecode ، الذي قام
بالعبث في نظام Android Gradle Plugin من
Bazel ، نظام بناء Google. وكان أداءها لا يزال منخفضًا ، لذا استمر البحث عن حل أفضل بشكل متوازٍ.
والآن تم
dexer
-
D8 ، والذي كان من المفترض أن يحل محل
dx tool
. تم تنفيذ Desaccharization الآن أثناء تحويل ملفات JAR المترجمة إلى
.dex
(dexing). D8 أفضل بكثير في الأداء مقارنة بـ
dx
، ومنذ أن أصبح Android Gradle Plugin 3.1 أصبح dexer الافتراضي.
D8
الآن ، باستخدام D8 ، يمكننا تجميع الشفرة أعلاه.
$ 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
لمعرفة كيفية تحويل D8 lambda ، يمكنك استخدام
dexdump tool
، المضمنة في Android SDK. سيعرض الكثير من كل شيء ، لكننا سنركز فقط على هذا:
$ $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 …
إذا لم تكن قد قرأت الرمز الفرعي بعد ، فلا تقلق: يمكن فهم الكثير مما يكتب هنا بشكل حدسي.
في المجموعة الأولى ، تحصل طريقتنا
main
مع الفهرس
0000
على مرجع من حقل
INSTANCE
إلى فئة
INSTANCE
Java8$1
. تم إنشاء هذا الفصل أثناء عملية
. إن الطريقة الرئيسية للبيتكود لا تحتوي أيضًا على أي ذكر لجسم لامدا لدينا ، لذلك ، على الأرجح ، يرتبط
Java8$1
. الفهرس
0002
ثم استدعاء الأسلوب
sayHi
ثابت باستخدام الرابط إلى
INSTANCE
. تتطلب
sayHi
Java8$Logger
، لذلك يبدو أن
Java8$1
تنفذ هذه الواجهة. يمكننا التحقق من هذا هنا:
Class #2 - Class descriptor : 'LJava8$1;' Access flags : 0x1011 (PUBLIC FINAL SYNTHETIC) Superclass : 'Ljava/lang/Object;' Interfaces - #0 : 'LJava8$Logger;'
تعني علامة
SYNTHETIC
أنه تم إنشاء فئة
Java8$1
وأن قائمة الواجهات التي
Java8$Logger
تحتوي على
Java8$Logger
.
هذه الفئة تمثل لامدا لدينا. إذا نظرت إلى تطبيق طريقة
log
، فلن ترى جسم اللمدا.
… [00026c] Java8$1.log:(Ljava/lang/String;)V 0000: invoke-static {v1}, LJava8;.lambda$main$0:(Ljava/lang/String;)V 0003: return-void …
بدلاً من ذلك ، يتم
Java8
الأسلوب
static
لفئة
Java8
-
lambda$main$0
. أكرر ، يتم تقديم هذه الطريقة فقط في 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
تخبرنا العلامة
SYNTHETIC
مرة أخرى أنه تم إنشاء هذه الطريقة ، وأن رمزها الثانوي يحتوي فقط على نص lambda: استدعاء
System.out.println
. السبب في أن جسم lambda داخل
Java8.class بسيط - فقد يحتاج إلى الوصول إلى أعضاء
private
، والتي لن يتمكن الفصل الذي تم إنشاؤه من الوصول إليه.
كل ما تحتاجه لفهم كيف يعمل
إلغاء التشريح هو موضح أعلاه. ومع ذلك ، عند النظر إليها في رمز Dalvik bytecode ، يمكنك أن ترى أن كل شيء أكثر تعقيدًا ومخيفًا هناك.
تحويل المصدر
لفهم كيفية حدوث
إلغاء التشكل بشكل أفضل ، دعونا نجرب خطوة بخطوة لتحويل
فصلنا إلى شيء يعمل على جميع إصدارات واجهة برمجة التطبيقات.
لنأخذ نفس الفئة مع lambda كأساس:
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!"); } }
أولاً ، يتم نقل جسم لامدا إلى طريقة
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); + }
ثم يتم تنفيذ فصل يقوم بتنفيذ واجهة
Logger
، حيث يتم تنفيذ مجموعة من التعليمات البرمجية من نص 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); + } +}
بعد ذلك ، يتم إنشاء مثيل مفرد من
Java8$1
، والذي يتم تخزينه في متغير
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) {
هذه هي الفئة النهائية
المدبلجة والتي يمكن استخدامها في جميع إصدارات واجهة برمجة التطبيقات:
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); } }
إذا نظرت إلى الفصل الذي تم إنشاؤه في رمز Dalvik bytecode ، فلن تجد أسماء مثل Java8 $ 1 - سيكون هناك شيء مثل
-$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY
. السبب وراء إنشاء مثل هذه التسمية للفئة ، وما هي مزاياها ، مقالة منفصلة.
دعم لامدا الأصلي
عندما استخدمنا
dx tool
لتجميع فئة تحتوي على lambdas ، قالت رسالة خطأ إن هذا لن يعمل إلا مع 26 واجهات برمجة التطبيقات.
$ $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
لذلك ، يبدو من المنطقي أننا إذا حاولنا ترجمة ذلك
—min-api 26
، فلن يحدث إلغاء التشريد.
$ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --min-api 26 \ --output . \ *.class
ومع ذلك ، إذا
.dex
ملف
.dex
، فلا يزال من الممكن العثور عليه
-$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY
. لماذا هذا هل هذا خطأ D8؟
للإجابة على هذا السؤال ، ولماذا
يحدث دائمًا عدم الإزالة ، نحتاج إلى البحث داخل كود جافا من فئة
Java8
.
$ javap -v Java8.class class Java8 { public static void main(java.lang.String...); Code: 0: invokedynamic
داخل الطريقة
main
، نرى مرة أخرى
invokeynamic في الفهرس
0
. الوسيطة الثانية في المكالمة هي
0
- فهرس طريقة
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
هنا تسمى طريقة
bootstrap metafactory
في فئة
java.lang.invoke.LambdaMetafactory
. إنه
يعيش في JDK ويقوم بإنشاء فصول مجهولة في وقت التشغيل لـ lambdas ، تمامًا مثل D8 التي تولدها في وقت الحساب.
إذا نظرت إلى
Android java.lang.invoke
أو إلى
AOSP java.lang.invoke
، نرى أن هذه الفئة ليست في وقت التشغيل. لهذا السبب يحدث إلغاء التفكيك دائمًا في وقت الترجمة ، بغض النظر عن minApi لديك. يدعم VM إرشادات bytecode المشابهة لـ
invokedynamic
، لكن
invokedynamic
المدمج في JDK
LambdaMetafactory
متاح للاستخدام.
مراجع الطريقة
بالإضافة إلى lambdas ، أضافت Java 8 مراجع الطريقة - هذه طريقة فعالة لإنشاء لامدا يشير جسمها إلى طريقة موجودة.
لدينا واجهة
Logger
هي مجرد مثال على ذلك. يشار إلى لامدا الجسم
System.out.println
. دعنا نحول لامدا إلى طريقة مرجعية:
public static void main(String... args) { - sayHi(s -> System.out.println(s)); + sayHi(System.out::println); }
عندما نجمعها ونلقي نظرة على الكود الثاني ، سنرى اختلافًا واحدًا مع الإصدار السابق:
[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
بدلاً من استدعاء
Java8.lambda$main$0
الذي تم إنشاؤه ، والذي يحتوي على استدعاء
System.out.println
، يسمى الآن
System.out.println
مباشرة.
لم تعد الفئة ذات lambda مفردة
static
، لكن من خلال الفهرس
0000
في bytecode ، نرى أننا نحصل على رابط
PrintStream
-
System.out
، والذي يستخدم بعد ذلك للاتصال بـ
println
عليه.
نتيجة لذلك ، تحول فئتنا إلى هذا:
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
static
في الواجهات
كان التغيير المهم والكبير الآخر الذي أدخله Java 8 هو القدرة على إعلان الأساليب
default
static
في الواجهات.
interface Logger { void log(String s); default void log(String tag, String s) { log(tag + ": " + s); } static Logger systemOut() { return System.out::println; } }
كل هذا مدعوم من قبل D8. باستخدام الأدوات نفسها كما كان من قبل ، من السهل أن ترى نسخة مسجّلة من المسجل مع الأساليب
default
static
. أحد الاختلافات مع lambdas
method references
هو أن الأساليب الافتراضية والثابتة يتم تنفيذها في Android VM ، وأن تبدأ D8 من 24 واجهة برمجة تطبيقات لن
تفصلها .
ربما مجرد استخدام Kotlin؟
أثناء قراءة المقال ، ربما فكر معظمكم في Kotlin. نعم ، يدعم جميع ميزات Java 8 ، لكن يتم تنفيذها بواسطة
kotlinc
بنفس طريقة D8 ، باستثناء بعض التفاصيل.
لذلك ، لا يزال دعم Android للإصدارات الجديدة من Java مهمًا للغاية ، حتى إذا كان مشروعك مكتوبًا بنسبة 100٪ في Kotlin.
من المحتمل أن Kotlin في المستقبل لن تدعم Java 6 و Java 7. bytecode. IntelliJ
IDEA ، Gradle 5.0 تحولت إلى Java 8. يتناقص عدد المنصات التي تعمل على JVMs الأقدم.
واجعل واجهات برمجة التطبيقات
تحدثت طوال هذا الوقت عن ميزات Java 8 ، لكنني لم أقل شيئًا عن واجهات برمجة التطبيقات الجديدة - التدفقات ،
CompletableFuture
، التاريخ / الوقت وما إلى ذلك.
بالعودة إلى مثال Logger ، يمكننا استخدام واجهة برمجة تطبيقات التاريخ / الوقت الجديدة لمعرفة وقت إرسال الرسائل.
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!"); } }
قم بتجميعه مرة أخرى باستخدام
javac
وقم بتحويله إلى رمز Dalvik bytecode باستخدام D8 ، والذي
يفصله للحصول على الدعم في جميع إصدارات API.
$ javac *.java $ java -jar d8.jar \ --lib $ANDROID_HOME/platforms/android-28/android.jar \ --release \ --output . \ *.class
يمكنك حتى تشغيل هذا على جهازك للتأكد من أنه يعمل.
$ 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
إذا كان API 26 وما فوق موجودًا على هذا الجهاز ، فستظهر رسالة Hello. إذا لم يكن كذلك ، فسنرى ما يلي:
java.lang.NoClassDefFoundError: Failed resolution of: Ljava/time/LocalDateTime; at Java8.sayHi(Java8.java:13) at Java8.main(Java8.java:9)
تعامل D8 مع lambdas ، وهي طريقة مرجعية ، لكنه لم يفعل شيئًا للعمل مع
LocalDateTime
، وهذا أمر محزن جدًا.
يتعين على المطورين استخدام تطبيقاتهم أو
ThreeTenBP
الخاصة في تاريخ / وقت api ، أو استخدام مكتبات مثل
ThreeTenBP
للعمل مع الوقت ، ولكن لماذا لا تستطيع أن تفعل D8 بيديك؟
خاتمة
يظل نقص الدعم لجميع واجهات برمجة تطبيقات Java 8 الجديدة مشكلة كبيرة في النظام البيئي لنظام Android. في الواقع ، من غير المحتمل أن يسمح لنا كل منا بتحديد واجهة برمجة التطبيقات التي تبلغ مدتها 26 دقيقة في مشروعنا. لا تستطيع المكتبات التي تدعم كلا من Android و JVM استخدام واجهة برمجة التطبيقات المقدمة إلينا قبل 5 سنوات!
على الرغم من أن دعم Java 8 أصبح الآن جزءًا من D8 ، فلا يزال يتعين على كل مطور تحديد توافق المصدر والهدف بشكل صريح في Java 8. إذا كنت تكتب مكتباتك الخاصة ، فيمكنك تعزيز هذا الاتجاه عن طريق وضع مكتبات تستخدم كود Java 8 bytecode (حتى إذا كنت لا تستخدم ميزات لغة جديدة).
يتم تنفيذ الكثير من العمل على D8 ، لذلك يبدو أن كل شيء سيكون على ما يرام في المستقبل مع دعم ميزات اللغة. حتى إذا كنت تكتب فقط على Kotlin ، فمن المهم للغاية إجبار فريق تطوير Android على دعم جميع الإصدارات الجديدة من Java ، وتحسين الكود ، وواجهات برمجة التطبيقات الجديدة.
هذا المنشور هو نسخة مكتوبة من حديثي
Digging to D8 and R8 .