كيف يتم دعم Java 8 على Android

مرحبا يا هبر! أوجه انتباهكم إلى ترجمة لمقال رائع من سلسلة من المقالات التي قام بها جيك وورتون سيئ السمعة حول كيفية دعم 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 #2, 0 // InvokeDynamic #0:log:()LJava8$Logger; 5: invokestatic #3 // Method sayHi:(LJava8$Logger;)V 8: return } … 

داخل الطريقة 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 .

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


All Articles