חדשות על מוצרים

הגדרה ופתרון בעיות של כללי שמירה ב-R8

משך הקריאה: 7 דקות
Ajesh Pai ו- Ben Weiss

בפיתוח מודרני של אפליקציות ל-Android, משתמשים מצפים לקבל אפליקציה קטנה, מהירה ומאובטחת. הכלי העיקרי של מערכת ה-build של Android להשגת המטרה הזו הוא R8 , כלי האופטימיזציה, הקומפיילר שמטפל בהסרת קוד ומשאבים לא פעילים לצורך כיווץ, שינוי שם של קוד או הקטנה ואופטימיזציה של האפליקציה.

הפעלת R8 היא שלב חשוב בהכנת אפליקציה לפרסום, אבל היא מחייבת את המפתחים לספק הנחיות בצורה של 'כללי שמירה'.

אחרי שתקראו את המאמר הזה, כדאי לצפות בסרטון 'הדגשת ביצועים: שבוע' ב-YouTube כדי ללמוד איך להפעיל את הכלי לשיפור הביצועים R8, איך לנפות באגים ואיך לפתור בעיות.

 

 

למה צריך כללים לשמירת נתונים ב-Keep

הצורך בכתיבת כללי Keep נובע מסתירה מהותית: R8 הוא כלי לניתוח סטטי, אבל אפליקציות ל-Android מסתמכות לעיתים קרובות על דפוסי ביצוע דינמיים כמו רפלקציה או קריאות אל קוד Native וממנו באמצעות JNI ‏ (Java Native Interface).

‫R8 יוצר גרף של קוד בשימוש על ידי ניתוח של קריאות ישירות. כשניגשים לקוד באופן דינמי, הניתוח הסטטי של R8 לא יכול לחזות את זה, והוא יזהה את הקוד כלא בשימוש ויסיר אותו, מה שיוביל לקריסות בזמן הריצה.

כלל שמירה הוא הוראה מפורשת לקומפיילר R8, שבה מצוין: "הכיתה, השיטה או השדה הספציפיים האלה הם נקודת כניסה שתהיה נגישה באופן דינמי בזמן הריצה. אתם חייבים לשמור אותו, גם אם אתם לא מוצאים הפניה ישירה אליו".

מידע נוסף על כללי השמירה זמין במדריך הרשמי.

איפה כותבים כללים ל-Keep

כללי שמירה מותאמים אישית לאפליקציה נכתבים בקובץ טקסט. לפי המוסכמה, שם הקובץ הוא proguard-rules.pro והוא נמצא בבסיס של האפליקציה או של מודול הספרייה. לאחר מכן מציינים את הקובץ הזה בסוג ה-build release של קובץ build.gradle.kts של המודול.

release {

    isShrinkResources = true

    isMinifyEnabled = true

    proguardFiles(

        getDefaultProguardFile("proguard-android-optimize.txt"),

        "proguard-rules.pro",

    )

}

שימוש בקובץ ברירת המחדל הנכון

השיטה getDefaultProguardFile מייבאת קבוצת כללים שמוגדרת כברירת מחדל ומסופקת על ידי Android SDK. אם משתמשים בקובץ לא מתאים, יכול להיות שהאפליקציה לא תעבור אופטימיזציה. חשוב להשתמש ב-proguard-android-optimize.txt. הקובץ הזה מספק את כללי השמירה שמוגדרים כברירת מחדל לרכיבי Android רגילים ו מאפשר את האופטימיזציות של הקוד ב-R8. הגרסה המיושנת proguard-android.txt מספקת רק את כללי השמירה הספציפיים (keep rule), אבל לא מאפשרת את האופטימיזציות של R8.

progaurd.png

זו בעיה חמורה בביצועים, ולכן אנחנו מתחילים להזהיר מפתחים מפני שימוש בקובץ הלא נכון, החל מ-Android Studio Narwhal 3 Feature Drop. החל מגרסה 9.0 של פלאגין Android Gradle, אנחנו כבר לא תומכים בקובץ proguard-android.txt המיושן. לכן חשוב לשדרג לגרסה המותאמת.

איך כותבים כללים ב-Keep

כלל שמירה מורכב משלושה חלקים עיקריים:

  1. אפשרות כמו -keep או -keepclassmembers
  2. מגבילים אופציונליים כמו allowshrinking
  3. מפרט כיתה שמגדיר את הקוד להתאמה

לעיון בתחביר המלא ובדוגמאות, אפשר לעיין בהנחיות בנושא הוספת כללי שמירה.

שמירה על כללי אנטי-תבניות

חשוב להכיר את השיטות המומלצות, אבל גם את האנטי-דפוסים. האנטי-דפוסים האלה נובעים בדרך כלל מאי-הבנות או מקיצורי דרך לפתרון בעיות, והם עלולים לפגוע באופן משמעותי בביצועים של גרסת ייצור.

אפשרויות כלליות

הדגלים האלה הם מתגים גלובליים שאסור להשתמש בהם בגרסת build להפצה. הם מיועדים לניפוי באגים זמני בלבד, כדי לבודד בעיה.

השימוש ב--dontotptimize משבית למעשה את האופטימיזציות של R8 לשיפור הביצועים, וכתוצאה מכך האפליקציה פועלת לאט יותר.

כשמשתמשים ב--dontobfuscate, כל הפעולות של שינוי השם מושבתות, וכשמשתמשים ב--dontshrink, ההסרה של קוד לא פעיל מושבתת. שני הכללים הגלובליים האלה מגדילים את גודל האפליקציה.

כדי לשפר את חוויית המשתמש באפליקציה, מומלץ להימנע משימוש בסימונים הגלובליים האלה בסביבת ייצור, ככל האפשר.

כללי שמירה רחבים מדי

הדרך הכי קלה לבטל את היתרונות של R8 היא לכתוב כללי שמירה רחבים מדי. כללים כמו זה שבהמשך מנחים את כלי האופטימיזציה R8 לא לכווץ, לא לבצע ערפול קוד (obfuscation) ולא לבצע אופטימיזציה לאף מחלקה בחבילה הזו או לאף אחת מחבילות המשנה שלה. הפעולה הזו מסירה לחלוטין את היתרונות של R8 לכל החבילה. במקום זאת, כדאי לנסות לכתוב כללי שמירה ספציפיים ומצומצמים יותר.
 

-keep class com.example.package.** { *;} // WIDE KEEP RULES CAUSE PROBLEMS

אופרטור ההיפוך (!)

נראה שאופרטור ההיפוך (!) הוא דרך יעילה להחרגת חבילה מכלל. אבל זה לא כל כך פשוט. לדוגמה:

-keep class !com.example.my_package.** { *; } // USE WITH CAUTION

יכול להיות שתחשבו שהכלל הזה אומר "לא לשמור כיתות ב-com.example.package". אבל למעשה הוא אומר "לשמור כל כיתה, שיטה ומאפיין בכל האפליקציה שלא נמצאים ב- com.example.package". אם זה הפתיע אתכם, כדאי לבדוק אם יש שלילות בהגדרות של R8.

כללים מיותרים לרכיבי Android

טעות נפוצה נוספת היא הוספה ידנית של כללי שמירה עבור ActivitiesServices או BroadcastReceivers של האפליקציה. זה לא נחוץ. קובץ proguard-android-optimize.txt שמוגדר כברירת מחדל כבר כולל את הכללים הרלוונטיים לרכיבי Android הרגילים האלה, כדי שהם יפעלו ללא צורך בהגדרה נוספת.

בנוסף, ספריות רבות מביאות איתן כללי שמירה משלהן. לכן לא צריך לכתוב כללים משלכם בשביל זה. אם יש בעיה בכללי השמירה בספרייה שבה אתם משתמשים, מומלץ לפנות ליוצר הספרייה כדי לברר מה הבעיה.

שיטות מומלצות לשימוש בכללים

אחרי שהסברנו מה לא לעשות, נדבר על שיטות מומלצות.

כתיבת כללים ספציפיים ב-Keep

כללי שמירה טובים צריכים להיות מצומצמים וספציפיים ככל האפשר. הם צריכים לשמור רק את מה שנדרש, כדי לאפשר ל-R8 לבצע אופטימיזציה לכל השאר.
 

כללאיכות

 

-keep class com.example.** { ; }

 

נמוך: שומר את כל החבילה ואת חבילות המשנה שלה

 

-keep class com.example.MyClass { ; }

 

נמוך: שומר על כיתה שלמה, שסביר להניח שהיא עדיין רחבה מדי
-keepclassmembers class com.example.MyClass {

    private java.lang.String secretMessage;

    public void onNativeEvent(java.lang.String);

}
גבוהה: נשמרים רק המאפיינים והשיטות הרלוונטיים ממחלקה ספציפית

שימוש באבות משותפים

במקום לכתוב כללי שמירה נפרדים לכמה מודלים שונים של נתונים, כותבים כלל אחד שמכוון לממשק או למחלקת בסיס משותפת. הכלל שמופיע בהמשך אומר ל-R8 לשמור את כל החברים במחלקות שמיישמות את הממשק הזה, והוא ניתן להרחבה בקלות.

# Keep all fields of any class that implements SerializableModel

-keepclassmembers class * implements com.example.models.SerializableModel {

    <fields>;

}

שימוש בהערות כדי לטרגט מספר כיתות

יוצרים הערה בהתאמה אישית (לדוגמה, @Serialize) ומשתמשים בה כדי 'לתייג' מחלקות שצריך לשמור את השדות שלהן. זהו עוד דפוס נקי, הצהרתי וניתן להרחבה. אפשר גם ליצור כללי Keep להערות שכבר קיימות מתוך מסגרות שבהן אתם משתמשים.

# Keep all fields of any class annotated with @Serialize

-keepclassmembers class * {

    @com.example.annotations.Serialize <fields>;

}

בחירת האפשרות המתאימה ב-Keep

האפשרות Keep (שמירה) היא החלק הכי חשוב בכלל. בחירה לא נכונה עלולה להשבית את האופטימיזציה ללא צורך.

אפשרות Keepמה עושים עם זה
-keepמונע את ההסרה או השינוי של השם של המחלקה ושל החברים שמוזכרים בהצהרה .
-keepclassmembersמונע את ההסרה או את שינוי השם של המשתמשים שצוינו, אבל מאפשר להסיר את הכיתה עצמה, רק בכיתות שלא הוסרו בדרך אחרת.
-keepclasseswithmembersשילוב: הכיתה וגם החברים שלה יישמרו, רק אם כל החברים שצוינו נמצאים.

מידע נוסף על האפשרות 'שמירה' זמין במאמר בנושא אפשרויות השמירה.

אפשרות לאופטימיזציה באמצעות משנים

משנים כמו allowshrinking ו-allowobfuscation מרחיבים כלל -keep רחב, ומחזירים את יכולת האופטימיזציה ל-R8. לדוגמה, אם ספרייה מדור קודם מחייבת אתכם להשתמש ב--keep במחלקה שלמה, יכול להיות שתוכלו לשפר את האופטימיזציה על ידי הפעלת כיווץ והצפנה:

# Keep this class, but allow R8 to remove it if it's unused and allow R8 to rename it.

-keep,allowshrinking,allowobfuscation class com.example.LegacyClass

הוספת הגדרות גלובליות להוספת אופטימיזציה

בנוסף לכללי Keep, אפשר להוסיף דגלים גלובליים לקובץ התצורה של R8 כדי לעודד אופטימיזציה נוספת.

-repackageclasses היא אפשרות עוצמתית שמורה ל-R8 להעביר את כל המחלקות המוסתרות לחבילה אחת. הפעולה הזו חוסכת נפח משמעותי בקובץ ה-DEX, כי היא מסירה מחרוזות מיותרות של שמות חבילות.

-allowaccessmodification מאפשרת ל-R8 להרחיב את הגישה (למשל, private ל- public) כדי לאפשר הטמעה אגרסיבית יותר. האפשרות הזו מופעלת עכשיו כברירת מחדל כשמשתמשים ב-proguard-android-optimize.txt.

אזהרה: מפתחי ספריות אף פעם לא צריכים להוסיף את דגלי האופטימיזציה הגלובליים האלה לכללי הצרכן שלהם, כי הם יחולו בכפייה על כל האפליקציה.

כדי להבהיר את העניין עוד יותר, בגרסה 9.0 של פלאגין של Android Gradle נתחיל להתעלם לחלוטין מדגלי אופטימיזציה גלובליים של ספריות. 

שיטות מומלצות לשימוש בספריות

כל אפליקציית Android מסתמכת על ספריות בדרך כזו או אחרת. בואו נדבר על שיטות מומלצות לספריות.

למפתחי ספריות

אם הספרייה משתמשת ב-reflection או ב-JNI, אתם אחראים לספק למשתמשים שלה את כללי השמירה הנדרשים. הכללים האלה ממוקמים בקובץ consumer-rules.pro, שמאוגד אוטומטית בתוך קובץ ה-AAR של הספרייה.

android {

    defaultConfig {

        consumerProguardFiles("consumer-rules.pro")

    }

    ...

}

למשתמשים בספרייה

סינון כללי Keep בעייתיים

אם אתם חייבים להשתמש בספרייה שכוללת כללי שמירה בעייתיים, אתם יכולים לסנן אותם בקובץ build.gradle.kts החל מ-AGP 9.0. כך תנחו את R8 להתעלם מהכללים שמגיעים מתלות ספציפית.

release {

    optimization.keepRules {

        // Ignore all consumer rules from this specific library

        it.ignoreFrom("com.somelibrary:somelibrary")

    }

}

הכלל הכי טוב ב-Keep הוא לא להגדיר כלל בכלל

האסטרטגיה האולטימטיבית להגדרת R8 היא להימנע לחלוטין מהצורך לכתוב כללי שמירה. במקרים רבים, אפשר להשיג זאת על ידי בחירה בספריות מודרניות שמעדיפות יצירת קוד על פני רפלקציה. בעזרת יצירת קוד, הכלי לאופטימיזציה יכול לקבוע בקלות רבה יותר איזה קוד נמצא בשימוש בזמן הריצה ואיזה קוד אפשר להסיר. בנוסף, אם לא משתמשים בהשתקפות דינמית, אין נקודות כניסה 'מוסתרות', ולכן לא צריך כללי שמירה. כשבוחרים ספרייה חדשה, תמיד עדיף לבחור פתרון שמשתמש ביצירת קוד ולא ברפלקציה.

מידע נוסף על בחירת ספריות זמין במאמר בחירה נכונה של ספריות.

ניפוי באגים ופתרון בעיות בהגדרת R8

אם R8 מסיר קוד שהוא היה צריך לשמור, או אם קובץ ה-APK גדול מהצפוי, אפשר להשתמש בכלים האלה כדי לאבחן את הבעיה.

איתור כללים כפולים וכללים גלובליים של Keep

מכיוון ש-R8 ממזג כללים מעשרות מקורות, יכול להיות שיהיה קשה לדעת מהו קבוצת הכללים הסופית. הוספת הדגל הזה לקובץ proguard-rules.pro יוצרת דוח מלא:

# Outputs the final, merged set of rules to the specified file

-printconfiguration build/outputs/logs/configuration.txt

אפשר לחפש בקובץ הזה כללים מיותרים או לעקוב אחרי כלל בעייתי (כמו -dontoptimize) עד לספרייה הספציפית שכללה אותו.

תשאלו את R8: למה אתה שומר את זה?

אם כיתה שציפיתם שהיא תוסר עדיין מופיעה באפליקציה, R8 יכול להסביר לכם למה. פשוט מוסיפים את הכלל הזה:

# Asks R8 to explain why it's keeping a specific class

class com.example.MyUnusedClass

-whyareyoukeeping 

במהלך הבנייה, R8 ידפיס את שרשרת ההפניות המדויקת שגרמה לו לשמור את המחלקה הזו, וכך תוכלו לעקוב אחרי ההפניה ולשנות את הכללים.

מדריך מלא זמין בקטע פתרון בעיות ב-R8.

השלבים הבאים

‫R8 הוא כלי יעיל לשיפור ביצועי האפליקציה ב-Android. היעילות שלו תלויה בהבנה נכונה של אופן הפעולה שלו כמנוע ניתוח סטטי.

כדי לשמור בדיוק את מה שצריך, אפשר לכתוב כללים ספציפיים ברמת החבר, להשתמש בנתוני צאצאים ובהערות, ולבחור בקפידה את אפשרויות השמירה הנכונות. השיטה המתקדמת ביותר היא לבטל את הצורך בכללים לחלוטין, על ידי בחירה בספריות מודרניות שמבוססות על יצירת קוד, במקום בספריות קודמות שמבוססות על רפלקציה.

במהלך שבוע ההדגשה של הביצועים, כדאי לצפות בסרטון של היום ב-YouTube ולהמשיך עם האתגר R8 שלנו. אם יש לכם שאלות לגבי הפעלה או פתרון בעיות ב-R8, תוכלו להשתמש בתג #optimizationEnabled. אנחנו כאן כדי לעזור.

הגיע הזמן לראות את היתרונות בעצמכם.

אנחנו ממליצים להפעיל את המצב המלא של R8 באפליקציה עוד היום.

  1. כדי להתחיל, כדאי לעיין במדריכים למפתחים: הפעלת אופטימיזציה של אפליקציות.
  2. בודקים אם אתם עדיין משתמשים ב-proguard-android.txt ומחליפים אותו ב-proguard-android-optimize.txt.
  3. לאחר מכן, מודדים את ההשפעה. אל תסתפקו בתחושה של הבדל, בדקו אותו. כדי למדוד את השיפור בביצועים, אפשר להתאים את הקוד מ אפליקציית הדוגמה Macrobenchmark ב-GitHub כדי למדוד את זמני ההפעלה לפני ואחרי.

אנחנו בטוחים שתראו שיפור משמעותי בביצועים של האפליקציה.

אתם יכולים גם להשתמש בתג #AskAndroid ברשתות החברתיות כדי לשאול שאלות. במהלך השבוע המומחים שלנו בודקים את השאלות שלכם ועונים עליהן.

מחר נדבר על אופטימיזציה מונחית פרופיל באמצעות פרופילים של Baseline ו-Startup, נסביר איך השתפרו ביצועי העיבוד של Compose בגרסאות האחרונות ונשתף שיקולים לגבי ביצועים של עבודה ברקע.

נכתב על ידי:

להמשך הקריאה