כיווץ, ערפול קוד (obfuscation) ואופטימיזציה של האפליקציה

כדי שהאפליקציה תהיה קטנה ומהירה ככל האפשר, כדאי לבצע אופטימיזציה ומיינימיזציה של גרסה ה-build של המהדורה באמצעות isMinifyEnabled = true.

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

כשמפתחים את הפרויקט באמצעות פלאגין Android Gradle מגרסה 3.4.0 ואילך, הפלאגין כבר לא משתמש ב-ProGuard כדי לבצע אופטימיזציה של קוד בזמן הידור. במקום זאת, הפלאגין עובד עם מַעְבֵּד R8 כדי לטפל במשימות הבאות בזמן הידור:

  • כיווץ קוד (או 'ניעור עץ'): זיהוי והסרה בטוחה של מחלקות, שדות, שיטות ומאפיינים שלא בשימוש מהאפליקציה ומיחסי התלות שלה בספרייה (כלי חשוב לעקיפת מגבלת ההפניות של 64 אלף). לדוגמה, אם אתם משתמשים רק בכמה ממשקי API של יחסי תלות בספרייה, כיווץ יכול לזהות קוד של ספרייה שהאפליקציה שלכם לא משתמשת בו ולהסיר רק את הקוד הזה מהאפליקציה. למידע נוסף, קראו את הקטע בנושא כיווץ הקוד.
  • צמצום משאבים: הסרת משאבים שלא בשימוש מהאפליקציה הארוזת, כולל משאבים שלא בשימוש ביחסי התלות של האפליקציה בספריות. הוא פועל בשילוב עם כיווץ קוד, כך שאחרי הסרת קוד שלא בשימוש, אפשר להסיר בבטחה גם משאבים שלא מפנים אליהם יותר. מידע נוסף זמין בקטע איך לכווץ את המשאבים.
  • אופטימיזציה: בדיקה של הקוד וכתיבה מחדש שלו כדי לשפר את הביצועים בסביבת זמן הריצה ולהקטין עוד יותר את הגודל של קובצי ה-DEX של האפליקציה. כך אפשר לשפר את ביצועי הקוד בזמן הריצה ב-30%, ולשפר באופן משמעותי את ההפעלה ואת תזמון הפריימים. לדוגמה, אם R8 מזהה שהענף else {} של משפט if/else מסוים אף פעם לא נבחר, R8 מסיר את הקוד של הענף else {}. מידע נוסף זמין בקטע אופטימיזציה של קוד.
  • ערפול (או צמצום מזהה): קיצור השם של הכיתות והחברים, וכתוצאה מכך צמצום גודל קובצי ה-DEX. מידע נוסף זמין בקטע בנושא ערפול הקוד.

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

הפעלה של כיווץ, ערפול קוד ואופטימיזציה

כשמשתמשים ב-Android Studio 3.4 או בפלאגין Android Gradle 3.4.0 ואילך, R8 הוא המהדר שממיר את קוד הבייט של Java בפרויקט לפורמט DEX שפועל בפלטפורמת Android. עם זאת, כשיוצרים פרויקט חדש באמצעות Android Studio, התכונות 'צמצום', 'ערפול' ו'אופטימיזציית קוד' לא מופעלות כברירת מחדל. הסיבה לכך היא שהאופטימיזציות האלה בזמן הקומפילציה מאריכות את זמן הפיתוח של הפרויקט, ויכולות ליצור באגים אם לא מתאימים אישית מספיק את הקוד שכדאי לשמור.

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

Kotlin

android {
    buildTypes {
        getByName("release") {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `isDebuggable=false`.
            isMinifyEnabled = true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            isShrinkResources = true

            proguardFiles(
                // Includes the default ProGuard rules files that are packaged with
                // the Android Gradle plugin. To learn more, go to the section about
                // R8 configuration files.
                getDefaultProguardFile("proguard-android-optimize.txt"),

                // Includes a local, custom Proguard rules file
                "proguard-rules.pro"
            )
        }
    }
    ...
}

Groovy

android {
    buildTypes {
        release {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type. Make sure to use a build
            // variant with `debuggable false`.
            minifyEnabled true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            shrinkResources true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

קובצי תצורה של R8

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

מקור מיקום תיאור
Android Studio <module-dir>/proguard-rules.pro כשיוצרים מודול חדש באמצעות Android Studio, סביבת הפיתוח המשולבת יוצרת קובץ proguard-rules.pro בספריית השורש של המודול.

כברירת מחדל, אין בקובץ הזה כללים. לכן, צריך לכלול כאן כללי ProGuard משלך, כמו כללים בהתאמה אישית לשמירה.

פלאגין של Android Gradle נוצר על ידי הפלאגין של Android Gradle בזמן הידור. הפלאגין של Android Gradle יוצר את הקובץ proguard-android-optimize.txt, שכולל כללים שמועילים לרוב הפרויקטים ב-Android ומאפשר להשתמש בהערות @Keep*.

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

הערה: הפלאגין של Android Gradle כולל קובצי כללים נוספים של ProGuard שהוגדרו מראש, אבל מומלץ להשתמש ב-proguard-android-optimize.txt.

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

בספרייה של AAR:
proguard.txt

בספריית JAR:
META-INF/proguard/<ProGuard-rules-file>

בנוסף למיקומים האלה, הפלאגין של Android Gradle בגרסה 3.6 ואילך תומך גם בכללי כיווץ מטורגטים.

אם ספריית AAR או JAR פורסמה עם קובץ כללים משלה, ואתם כוללים את הספרייה הזו כיחסי תלות בזמן הידור, R8 מחיל את הכללים האלה באופן אוטומטי בזמן הידור הפרויקט.

בנוסף לכללי ProGuard הרגילים, הפלאגין של Android Gradle בגרסה 3.6 ואילך תומך גם בכללי צמצום ממוקדים. אלה כללים שמטרגטים כלי כיווץ ספציפיים (R8 או ProGuard) וגם גרסאות ספציפיות של כיווץ.

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

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

Android Asset Package Tool 2‏ (AAPT2) אחרי שמפתחים את הפרויקט באמצעות minifyEnabled true: <module-dir>/build/intermediates/aapt_proguard_file/.../aapt_rules.txt התכונה AAPT2 יוצרת כללים על סמך הפניות למחלקות במניפסט, בפריסות ובמשאבים אחרים של האפליקציה. לדוגמה, AAPT2 כולל כלל שמירה לכל פעילות שרושמים במניפסט של האפליקציה כנקודת כניסה.
קובצי תצורה בהתאמה אישית כברירת מחדל, כשיוצרים מודול חדש באמצעות Android Studio, סביבת הפיתוח המשולבת יוצרת את הקובץ <module-dir>/proguard-rules.pro כדי שתוכלו להוסיף כללים משלכם. אפשר לכלול הגדרות נוספות, ו-R8 מחילה אותן בזמן הידור.

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

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

// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

כללי צמצום ספציפיים

הפלאגין של Android Gradle מגרסה 3.6 ואילך תומך בכללים של ספריות שמטרגטים מכשירי דחיסה ספציפיים (R8 או ProGuard), וגם גרסאות ספציפיות של מכשירי דחיסה. כך מפתחי הספריות יכולים להתאים אישית את הכללים שלהם כך שיפעלו בצורה אופטימלית בפרויקטים שמשתמשים בגרסאות חדשות של ה-shrinker, ועדיין להשתמש בכללים הקיימים בפרויקטים עם גרסאות ישנות יותר של ה-shrinker.

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

In an AAR library:
    proguard.txt (legacy location)
    classes.jar
    └── META-INF
        └── com.android.tools (targeted shrink rules location)
            ├── r8-from-<X>-upto-<Y>/<R8-rules-file>
            └── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>

In a JAR library:
    META-INF
    ├── proguard/<ProGuard-rules-file> (legacy location)
    └── com.android.tools (targeted shrink rules location)
        ├── r8-from-<X>-upto-<Y>/<R8-rules-file>
        └── proguard-from-<X>-upto-<Y>/<ProGuard-rules-file>

כלומר, כללי הצמצום המטורגטים מאוחסנים בספרייה META-INF/com.android.tools של קובץ JAR או בספרייה META-INF/com.android.tools בתוך classes.jar של קובץ AAR.

בספרייה הזו יכולות להיות כמה ספריות עם שמות בפורמט r8-from-<X>-upto-<Y> או proguard-from-<X>-upto-<Y>, כדי לציין לאילו גרסאות של אילו מכשירי דחיסה נכתבו הכללים בספריות. שימו לב שהחלקים -from-<X> ו--upto-<Y> הם אופציונליים, הגרסה <Y> היא בלעדית וטווחי הגרסאות חייבים להיות רציפים.

לדוגמה, r8-upto-8.0.0, r8-from-8.0.0-upto-8.2.0 ו-r8-from-8.2.0 הם קבוצה תקינה של כללי צמצום יעדים. הכללים בספרייה r8-from-8.0.0-upto-8.2.0 יהיו בשימוש ב-R8 מגרסה 8.0.0 ועד גרסה 8.2.0, אבל לא כולל.

על סמך המידע הזה, הפלאגין של Android Gradle מגרסה 3.6 ואילך יבחר את הכללים מהספריות התואמות של R8. אם בספרייה לא צוינו כללי דחיסה ממוקדים, הפלאגין של Android Gradle יבחר את הכללים מהמיקומים הקודמים (proguard.txt ל-AAR או META-INF/proguard/<ProGuard-rules-file> ל-JAR).

מפתחי ספריות יכולים לבחור לכלול בספריות שלהם כללי צמצום ממוקדים או כללי ProGuard מדור קודם, או את שני הסוגים אם הם רוצים לשמור על תאימות ל-Android Gradle plugin מגרסה 3.6 ואילך או לכלים אחרים.

לכלול הגדרות נוספות

כשיוצרים פרויקט או מודול חדשים באמצעות Android Studio, סביבת הפיתוח המשולבת יוצרת קובץ <module-dir>/proguard-rules.pro שבו אפשר לכלול כללים משלכם. אפשר גם לכלול כללים נוספים מקבצים אחרים על ידי הוספה שלהם למאפיין proguardFiles בסקריפט ה-build של המודול.

לדוגמה, אפשר להוסיף כללים ספציפיים לכל וריאנט של build על ידי הוספת נכס proguardFiles נוסף בבלוק productFlavor המתאים. קובץ Gradle הבא מוסיף את flavor2-rules.pro למאפיין המוצר flavor2. עכשיו, flavor2 משתמש בכל שלושת הכללים של ProGuard כי גם הכללים מהבלוק release חלים.

בנוסף, אפשר להוסיף את המאפיין testProguardFiles, שמציין רשימה של קובצי ProGuard שכלולים ב-APK לבדיקה בלבד:

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                "proguard-rules.pro"
            )
            testProguardFiles(
                // The proguard files listed here are included in the
                // test APK only.
                "test-proguard-rules.pro"
            )
        }
    }
    flavorDimensions.add("version")
    productFlavors {
        create("flavor1") {
            ...
        }
        create("flavor2") {
            proguardFile("flavor2-rules.pro")
        }
    }
}

Groovy

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                'proguard-rules.pro'
            testProguardFiles
                // The proguard files listed here are included in the
                // test APK only.
                'test-proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
            ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

צמצום הקוד

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

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

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

באיור 1 מוצגת אפליקציה עם תלות בספרייה בסביבת זמן ריצה. במהלך הבדיקה של קוד האפליקציה, R8 מזהה שאפשר להגיע ל-methods foo(), faz() ו-bar() מנקודת הכניסה של MainActivity.class. עם זאת, האפליקציה שלכם אף פעם לא משתמשת בכיתה OkayApi.class או בשיטה baz() בסביבת זמן הריצה, ו-R8 מסיר את הקוד הזה כשמקטינים את האפליקציה.

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

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

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

שימו לב שאם פרויקט ספרייה מכווץ, אפליקציה שתלויה בספרייה הזו כוללת מחלקות של ספריות מכווצות. יכול להיות שתצטרכו לשנות את כללי השמירה של הספרייה אם חסרות כיתות בחבילת ה-APK של הספרייה. אם אתם יוצרים ומפרסמים ספרייה בפורמט AAR, קובצי JAR מקומיים שהספרייה תלויה בהם לא מצומצמים בקובץ ה-AAR.

התאמה אישית של הקוד שרוצים לשמור

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

  • כשהאפליקציה קוראת ל-method מ-Java Native Interface‏ (JNI)
  • כשהאפליקציה מחפשת קוד בזמן הריצה (למשל באמצעות רפלקציה)

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

כדי לתקן שגיאות ולאלץ את R8 לשמור קוד מסוים, מוסיפים שורה -keep לקובץ הכללים של ProGuard. לדוגמה:

-keep public class MyClass

לחלופין, אפשר להוסיף את ההערה @Keep לקוד שרוצים לשמור. אם מוסיפים את @Keep בכיתה, כל המחלקה נשארת כפי שהיא. אם מוסיפים אותה בשיטה או בשדה, השיטה/השדה (והשם שלה) וגם שם המחלקה יישארו ללא שינוי. שימו לב שהערה זו זמינה רק כשמשתמשים בספריית ההערות של AndroidX וכשכוללים את קובץ הכללים של ProGuard שנארז עם הפלאגין Gradle, כפי שמתואר בסעיף על הפעלת כיווץ.

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

הסרת ספריות מקוריות

כברירת מחדל, ספריות קוד נייטיב מוסרות מגרסאות ה-build של האפליקציה שלכם. ההסרה הזו כוללת הסרה של טבלת הסמלים ומידע על ניפוי הבאגים שכלולים בכל ספריות ה-Native של האפליקציה שלכם. הסרת ספריות קוד נייטיב מובילה לחיסכון משמעותי בגודל, אבל אי אפשר לאבחן קריסות ב-Google Play Console כי המידע חסר (כמו שמות של מחלקות ופונקציות).

תמיכה בקריסה במקור

ב-Google Play Console מדווח על קריסות מקוריות בקטע תפקוד האפליקציה. תוכלו ליצור ולהעלות קובץ סמלים מקומי לניפוי באגים לאפליקציה שלכם בכמה שלבים פשוטים. הקובץ הזה מאפשר לכם להציג ב-Android Vitals מעקב סטאק של קריסה מקומי (שכולל שמות של כיתות ופונקציות) שמתורגם לסמלים, כדי לעזור לכם לנפות באגים באפליקציה בסביבת הייצור. השלבים האלה משתנים בהתאם לגרסה של פלאגין Android Gradle שבו נעשה שימוש בפרויקט ולפלט ה-build של הפרויקט.

פלאגין Android Gradle בגרסה 4.1 ואילך

אם בפרויקט שלכם נוצר Android App Bundle, תוכלו לכלול בו באופן אוטומטי את קובץ הסמלים המקומי של ניפוי הבאגים. כדי לכלול את הקובץ הזה ב-builds של הגרסה, מוסיפים את הקטע הבא לקובץ build.gradle.kts של האפליקציה:

android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }

בוחרים את רמת הסמל של ניפוי הבאגים מבין האפשרויות הבאות:

  • משתמשים ב-SYMBOL_TABLE כדי לקבל שמות של פונקציות ב-Play Console, בנתוני המעקב אחר סטאק (stack trace) שעבר סימבוליזציה. ברמה הזו יש תמיכה במצבות.
  • משתמשים ב-FULL כדי לקבל שמות של פונקציות, קבצים ומספרי שורות ב-symbolicated stack traces של Play Console.

אם בפרויקט שלכם נוצר קובץ APK, תוכלו להשתמש בהגדרת ה-build build.gradle.kts שצוינה למעלה כדי ליצור את קובץ הסמלים לניפוי באגים באופן נפרד. מעלים את הקובץ של הסמלים המקוריים של ניפוי הבאגים באופן ידני ל-Google Play Console. כחלק מתהליך ה-build, הפלאגין של Android Gradle יוצר את הפלט של הקובץ הזה במיקום הפרויקט הבא:

app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip

הפלאגין של Android Gradle בגרסה 4.0 ואילך (ומערכות build אחרות)

כחלק מתהליך ה-build, הפלאגין של Android Gradle שומר עותק של הספריות ללא היררכיה בספריית הפרויקט. מבנה הספריות הזה דומה למבנה הבא:

app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── arm64-v8a/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
├── x86/
│   ├── libgameengine.so
│   ├── libothercode.so
│   └── libvideocodec.so
└── x86_64/
    ├── libgameengine.so
    ├── libothercode.so
    └── libvideocodec.so
  1. מעבירים את התוכן של הספרייה הזו לקובץ zip:

    cd app/build/intermediates/cmake/universal/release/obj
    zip -r symbols.zip .
    
  2. מעלים את הקובץ symbols.zip באופן ידני ל-Google Play Console.

צמצום המשאבים

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

כדי להפעיל כיווץ משאבים, מגדירים את המאפיין shrinkResources לערך true בסקריפט ה-build (לצד minifyEnabled לצורך כיווץ קוד). לדוגמה:

Kotlin

android {
    ...
    buildTypes {
        getByName("release") {
            isShrinkResources = true
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Groovy

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android.txt'),
                'proguard-rules.pro'
        }
    }
}

אם עדיין לא יצרתם את האפליקציה באמצעות minifyEnabled כדי לצמצם את הקוד, כדאי לנסות לעשות זאת לפני שמפעילים את shrinkResources, כי יכול להיות שתצטרכו לערוך את הקובץ proguard-rules.pro כדי לשמור על מחלקות או שיטות שנוצרות או מופעלות באופן דינמי לפני שתתחילו להסיר משאבים.

התאמה אישית של המשאבים שרוצים לשמור

אם יש משאבים ספציפיים שאתם רוצים לשמור או להשליך, תוכלו ליצור קובץ XML בפרויקט עם תג <resources> ולציין את כל המשאבים שרוצים לשמור במאפיין tools:keep ואת כל המשאבים שרוצים להשליך במאפיין tools:discard. בשני המאפיינים אפשר להזין רשימה מופרדת בפסיקים של שמות משאבים. אפשר להשתמש בתו הכוכב כתו כללי לחיפוש.

לדוגמה:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

שומרים את הקובץ הזה במשאבי הפרויקט, למשל ב-res/raw/my.package.keep.xml. הקובץ הזה לא נכלל בחבילת ה-build של האפליקציה.

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

יכול להיות שייראה לכם מטופש לציין אילו משאבים להוציא משימוש במקום למחוק אותם, אבל זה יכול להיות שימושי כשמשתמשים בגרסאות build שונות. לדוגמה, אפשר להעביר את כל המשאבים לספריית הפרויקט המשותפת, ואז ליצור קובץ my.package.build.variant.keep.xml שונה לכל וריאנט של build, אם יודעים שנראה שמשאב מסוים נמצא בשימוש בקוד (ולכן לא הוסר על ידי ה-shrinker), אבל יודעים שלמעשה הוא לא ישמש לווריאנט של ה-build. יכול להיות גם שכלי ה-build זיהו משאב כנדרש באופן שגוי. הסיבה לכך היא שהמקודד מוסיף את מזהי המשאבים בקוד, ולכן יכול להיות שמנתח המשאבים לא יודע את ההבדל בין משאב שמצוין בפניה אמיתית לבין ערך שלם בקוד שיש לו את אותו ערך.

הפעלת בדיקות קפדניות של הפניות

בדרך כלל, הכלי לצמצום משאבים יכול לקבוע במדויק אם משאבים נמצאים בשימוש. עם זאת, אם הקוד מבצע קריאה ל- Resources.getIdentifier() (או אם אחת מהספריות מבצעת זאת – ספריית AppCompat מבצעת זאת), המשמעות היא שהקוד מחפש שמות של משאבים על סמך מחרוזות שנוצרות באופן דינמי. כשעושים את זה, כיווץ המשאבים פועל כברירת מחדל ומסמן את כל המשאבים עם פורמט שם תואם כמשאבים שאפשר להשתמש בהם והם לא זמינים להסרה.

לדוגמה, הקוד הבא גורם לסימון כל המשאבים עם הקידומת img_ כמשאבים בשימוש.

Kotlin

val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)

Java

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

הכלי לצמצום המשאבים גם בודק את כל המשתנים הקבועים של המחרוזות בקוד, וגם משאבים שונים מסוג res/raw/, כדי לחפש כתובות URL של משאבים בפורמט דומה ל-file:///android_res/drawable//ic_plus_anim_016.png. אם המערכת תמצא מחרוזות כאלה או מחרוזות אחרות שנראות ככאלה שאפשר להשתמש בהן כדי ליצור כתובות URL כאלה, היא לא תסיר אותן.

אלה דוגמאות למצב הצמצום הבטוח שמופעל כברירת מחדל. עם זאת, אפשר להשבית את הטיפול הזה "למקרה הצורך", ולהגדיר שכלי צמצום המשאבים ישמור רק משאבים שהוא בטוח שנעשה בהם שימוש. כדי לעשות זאת, מגדירים את shrinkMode לערך strict בקובץ keep.xml באופן הבא:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict" />

אם בכל זאת מפעילים מצב כיווץ מחמיר והקוד מפנה גם למשאבים באמצעות מחרוזות שנוצרות באופן דינמי, כמו שמוצג למעלה, צריך לשמור את המשאבים האלה באופן ידני באמצעות המאפיין tools:keep.

הסרת משאבים חלופיים שלא נמצאים בשימוש

כיווץ המשאבים של Gradle מסיר רק משאבים שקוד האפליקציה לא מפנה אליהם. כלומר, הוא לא מסיר משאבים חלופיים לתצורות מכשיר שונות. במידת הצורך אפשר להשתמש במאפיין resConfigs של הפלאגין Android Gradle כדי להסיר קובצי משאבים חלופיים שהאפליקציה לא צריכה.

לדוגמה, אם אתם משתמשים בספרייה שכוללת משאבי שפה (כמו AppCompat או Google Play Services), האפליקציה שלכם תכלול את כל מחרוזות השפה המתורגמות של ההודעות בספריות האלה, גם אם שאר האפליקציה תורגמה לאותן שפות וגם אם לא. אם רוצים לשמור רק את השפות שהאפליקציה תומכת בהן באופן רשמי, אפשר לציין את השפות האלה באמצעות המאפיין resConfig. כל המשאבים לשפות שלא צוינו יוסרו.

בקטע הבא מוסבר איך להגביל את משאבי השפה לאנגלית ולצרפתית בלבד:

Kotlin

android {
    defaultConfig {
        ...
        resourceConfigurations.addAll(listOf("en", "fr"))
    }
}

Groovy

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}

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

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

מיזוג משאבים כפולים

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

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

Gradle מחפש משאבים כפולים במיקומים הבאים:

  • המשאבים הראשיים המשויכים לקבוצת המקורות הראשית, בדרך כלל נמצאים ב-src/main/res/.
  • שכבות-על של הווריאנטים, מסוג ה-build וטעמים.
  • יחסי התלות של פרויקט הספרייה.

Gradle ממזג משאבים כפולים לפי סדר העדיפויות המצטבר הבא:

יחסי תלות ← ראשי ← פיתוח טעם ← סוג Build

לדוגמה, אם משאב כפול מופיע גם במשאבים העיקריים וגם בגרסת build, מערכת Gradle תבחר את המשאב בטעם ה-build.

אם משאבים זהים מופיעים באותו קבוצת מקורות, Gradle לא יכול למזג אותם ומפיק שגיאה של מיזוג משאבים. מצב כזה יכול לקרות אם מגדירים כמה קבוצות מקורות במאפיין sourceSet בקובץ build.gradle.kts. לדוגמה, אם גם src/main/res/ וגם src/main/res2/ מכילים משאבים זהים.

ערפול קוד (obfuscation)

מטרת הערפול (obfuscation) היא להקטין את גודל האפליקציה על ידי קיצור השמות של המחלקות, השיטות והשדות באפליקציה. דוגמה להסתרה באמצעות R8:

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
    android.content.Context mContext -> a
    int mListItemLayout -> O
    int mViewSpacingRight -> l
    android.widget.Button mButtonNeutral -> w
    int mMultiChoiceItemLayout -> M
    boolean mShowTitle -> P
    int mViewSpacingLeft -> j
    int mButtonPanelSideLayout -> K

השימוש בערפול קוד (obfuscation) לא מסיר קוד מהאפליקציה, אבל יכול להיות חיסכון משמעותי בגודל באפליקציות עם קובצי DEX שמוסיפים לאינדקס הרבה מחלקות, methods ושדות. עם זאת, מאחר שהערפול משנה את השמות של חלקים שונים בקוד, משימות מסוימות, כמו בדיקת נתוני מעקב ב-stack, מחייבות כלים נוספים. כדי להבין את פענוח קוד מעורפל של דוח קריסות אחרי ההסתרה, כדאי לקרוא את הקטע בנושא.

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

פענוח של דוח קריסות מעורפל

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

אופטימיזציה של קוד

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

  • אם הקוד אף פעם לא לוקח את ההסתעפות else {} עבור הצהרה נתונה מסוג if/else, R8 עשוי להסיר את הקוד של ההסתעפות else {}.
  • אם הקוד קורא לשיטה רק בכמה מקומות, יכול להיות ש-R8 יסיר את השיטה ויוסיף אותה בתוך שורת הקוד במקומות הקריאה הבודדים.
  • אם R8 קובע שלמחלקה יש רק תת-מחלקה ייחודית אחת, והמחלקה עצמה לא נוצרת (לדוגמה, מחלקת בסיס מופשטת שמשמשת רק למחלקת יישום קונקרטית אחת), R8 יכול לשלב את שתי המחלקות ולהסיר מחלקה מהאפליקציה.
  • למידע נוסף, ראו פוסטים בבלוג האופטימיזציה של R8 מאת ג'ייק וורטון.

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

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

ההשפעה על הביצועים בסביבת זמן הריצה

אם תפעילו את כל התכונות של R8 – דחיסה, ערפול ואופטימיזציה – תוכלו לשפר את ביצועי הקוד בסביבת זמן הריצה (כולל זמן ההפעלה וזמן הפריים בשרשור של ממשק המשתמש) בשיעור של עד 30%. השבתה של אחת מהאפשרויות האלה מגבילה באופן משמעותי את קבוצת האופטימיזציות שבהן R8 משתמש.

אם R8 מופעל, כדאי גם ליצור פרופילים של הפעלה כדי לשפר עוד יותר את ביצועי ההפעלה.

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

R8 כולל קבוצה של אופטימיזציות נוספות (שנקראות 'מצב מלא') שגורמות לו לפעול באופן שונה מ-ProGuard. האופטימיזציות האלה מופעלות כברירת מחדל מ-גרסה 8.0.0 של הפלאגין של Android Gradle.

כדי להשבית את האופטימיזציות הנוספות האלה, צריך לכלול את הקטע הבא בקובץ gradle.properties של הפרויקט:

android.enableR8.fullMode=false

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

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

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

מעקב חוזר אחר נתוני סטאק

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

כדי לשחזר את דוח הקריסות המקורי, R8 מספק את כלי שורת הפקודה retrace, שצורף לחבילת כלי שורת הפקודה.

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

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

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

R8 יוצר קובץ mapping.txt בכל פעם שהוא פועל, שמכיל את המידע הדרוש למיפוי של מעקב ה-stack בחזרה למעקב ה-stack המקורי. Android Studio שומר את הקובץ בספרייה <module-name>/build/outputs/mapping/<build-type>/.

כשמפרסמים את האפליקציה ב-Google Play, אפשר להעלות את הקובץ mapping.txt לכל גרסה של האפליקציה. כשמפרסמים באמצעות קובצי Android App Bundle, הקובץ הזה נכלל באופן אוטומטי כחלק מתוכן חבילת האפליקציות. לאחר מכן, Google Play תעקוב אחר דוחות קריסות נכנסים מבעיות שדווחו על ידי משתמשים, כדי שתוכלו לבדוק אותם ב-Play Console. מידע נוסף זמין במאמר במרכז העזרה בנושא ביטול ההצפנה של נתוני סטאק של קריסה.

פתרון בעיות ב-R8

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

יצירת דוח של קוד שהוסר (או נשמר)

כדי לפתור בעיות מסוימות שקשורות ל-R8, מומלץ לעיין בדוח עם כל הקוד שהוסר מהאפליקציה R8. לכל מודול שרוצים ליצור את הדוח הזה, מוסיפים את -printusage <output-dir>/usage.txt לקובץ הכללים המותאמים אישית. כשמפעילים את R8 ובונים את האפליקציה, R8 יוצר דוח עם הנתיב ושם הקובץ שציינתם. הדוח על קוד שהוסרה נראה כך:

androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
    public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
    public boolean hasWindowFeature(int)
    public void setHandleNativeActionModesEnabled(boolean)
    android.view.ViewGroup getSubDecor()
    public void setLocalNightMode(int)
    final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
    public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
    private static final boolean DEBUG
    private static final java.lang.String KEY_LOCAL_NIGHT_MODE
    static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...

אם במקום זאת רוצים לראות דוח של נקודות הכניסה ש-R8 קובעת מתוך כללי השמירה של הפרויקט , צריך לכלול את -printseeds <output-dir>/seeds.txt בקובץ הכללים בהתאמה אישית. כשמפעילים את R8 ובונים את האפליקציה, R8 יוצר דוח עם הנתיב ושם הקובץ שציינתם. הדוח של נקודות הכניסה השמורות נראה כך:

com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...

פתרון בעיות של כיווץ מקורות המידע

כשמקטינים את המשאבים, בחלון Build מוצג סיכום של המשאבים שהוסרו מהאפליקציה. (קודם צריך ללחוץ על Toggle view בצד ימין של החלון כדי להציג פלט טקסט מפורט מ-Gradle). לדוגמה:

:android:shrinkDebugResources
Removed unused resources: Resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning

Gradle יוצר גם קובץ אבחון בשם resources.txt בתיקייה <module-name>/build/outputs/mapping/release/ (אותה תיקייה שבה נמצאים קובצי הפלט של ProGuard). הקובץ כולל פרטים כמו אילו משאבים מפנים למשאבים אחרים, ואילו משאבים משתמשים בהם או הוסרו.

לדוגמה, כדי לבדוק למה הקובץ @drawable/ic_plus_anim_016 עדיין נמצא באפליקציה, פותחים את הקובץ resources.txt ומחפשים את שם הקובץ הזה. יכול להיות שתמצאו שהיא מופיעה בהפניה ממשאב אחר, באופן הבא:

16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out]     @drawable/ic_plus_anim_016

עכשיו צריך לבדוק למה אפשר לגשת ל-@drawable/add_schedule_fab_icon_anim. אם מחפשים למעלה, רואים שהמשאב מופיע בקטע 'המשאבים שאפשר לגשת אליהם ברמה הבסיסית הם:'. המשמעות היא שיש הפניית קוד ל-add_schedule_fab_icon_anim (כלומר, מזהה R.drawable שלו נמצא בקוד שאפשר לגשת אליו).

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

10:32:50.590 [QUIET] [system.out] Marking drawable:ic_plus_anim_016:2130837506
    used because it format-string matches string pool constant ic_plus_anim_%1$d.

אם אתם רואים אחת מהמחרוזות האלה ואתם בטוחים שהמחרוזת לא משמשת לטעינת המשאב הנתון באופן דינמי, תוכלו להשתמש במאפיין tools:discard כדי להודיע למערכת ה-build להסיר אותה, כפי שמתואר בקטע בנושא התאמה אישית של המשאבים שרוצים לשמור.