مرحبا يا هبر! أوجه انتباهكم إلى ترجمة لمقال رائع من سلسلة من المقالات التي قام بها 
جيك وورتون سيئ السمعة حول كيفية دعم 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 .