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

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

משך הקריאה: 7 דקות
צפייה בפרופיל של Ajesh Pai צפייה בפרופיל של Ben Weiss
Ajesh Pai & Ben Weiss

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

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

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

 

 

למה צריך כללים לשמירה

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

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

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

פרטים נוספים על כללי השמירה זמינים במדריך הרשמי.

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

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

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. מפרט כיתה שמגדיר את הקוד להתאמה

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

Keep Rule anti-patterns

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

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

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

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

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

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

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

הדרך הכי קלה לבטל את היתרונות של R8 היא לכתוב כללי שמירה רחבים מדי. כללים כמו זה שבהמשך מנחים את הכלי לאופטימיזציה R8 לא לכווץ, לא להסתיר ולא לבצע אופטימיזציה של אף מחלקה בחבילה הזו או אף מחלקה בחבילות המשנה שלה. הפעולה הזו מסירה לחלוטין את היתרונות של 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

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

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

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

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

כתיבת כללים מצומצמים ב-Keep

כללי שמירה טובים ב-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 rule), אפשר להוסיף לדגל הגלובלי בקובץ ההגדרות של 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")

    }

}

הכלל הכי טוב לשמירה הוא לא לשמור בכלל

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

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

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

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

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

מכיוון ש-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. היעילות שלו תלויה בהבנה נכונה של אופן הפעולה שלו כמנוע ניתוח סטטי.

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

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

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

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

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

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

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

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

נכתב על ידי:
להמשך קריאה