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

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

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

כשמפתחים את הפרויקט באמצעות פלאגין 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 של הפרויקט, ויכול להיות שהן יגרמו לבאגים אם לא תתאימו אישית את הקוד שרוצים לשמור.

לכן, מומלץ להפעיל את המשימות האלה בזמן הידור כשמפתחים את הגרסה הסופית של האפליקציה שבודקים לפני הפרסום. כדי להפעיל כיווץ, ערפול קוד (obfuscation) ואופטימיזציה, צריך לכלול את הפרטים הבאים בסקריפט ה-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 כולל כלל שמירה לכל Activity שמירשמם במניפסט של האפליקציה כנקודת כניסה.
קובצי תצורה בהתאמה אישית כברירת מחדל, כשיוצרים מודול חדש באמצעות Android Studio, סביבת הפיתוח המשולבת יוצרת את הקובץ <module-dir>/proguard-rules.pro כדי שתוכלו להוסיף כללים משלכם. אפשר לכלול הגדרות נוספות, ו-R8 מחילה אותן בזמן הידור.

כשמגדירים את המאפיין minifyEnabled לערך true, המערכת משלבת את הכללים מכל המקורות הזמינים שצוינו למעלה. חשוב לזכור את זה כשאתם פותרים בעיות באמצעות 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 למחלקה שומרת על המחלקה כולה כפי שהיא. הוספה של @Keep לשדה או לשיטה שומרת על השדה או השיטה (והשם שלהם) ועל שם המחלקה ללא שינוי. הערה: ההערה הזו זמינה רק כשמשתמשים ב-AndroidX Annotations Library וכשמצרפים את קובץ הכללים של ProGuard שמצורף לפלאגין Android Gradle, כפי שמתואר בקטע בנושא הפעלת צמצום.

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

הצגת ספריות מקוריות ב-Strip

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

תמיכה בקריסה ברמת שפת המכונה

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

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

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

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

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

  • משתמשים ב-SYMBOL_TABLE כדי לקבל שמות של פונקציות ב-Play Console, בנתוני המעקב אחר סטאק (stack trace) שעבר סימבוליזציה. הרמה הזו תומכת במצבות.
  • משתמשים ב-FULL כדי לקבל שמות של פונקציות, קבצים ומספרי שורות בדוחות הקריסות שצוינו ב-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, אם אתם יודעים שנראה שמשתמשים במשאב מסוים בקוד (ולכן הוא לא יוסר על ידי הכלי לצמצום הקוד), אבל אתם יודעים שבפועל לא ייעשה בו שימוש בגרסת ה-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"))
    }
}

מגניב

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 ומהטעמים של ה-build.
  • יחסי התלות של פרויקט הספרייה.

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

יחסי תלות → ראשי → סוג build → סוג build

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

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

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

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

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

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

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

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

  • אם הקוד שלכם אף פעם לא עובר להסתעפות else {} בהצהרה if/else מסוימת, יכול להיות ש-R8 תסיר את הקוד של ההסתעפות else {}.
  • אם הקוד קורא לשיטה רק בכמה מקומות, יכול להיות ש-R8 יסיר את השיטה ויוסיף אותה בתוך שורת הקוד במקומות הקריאה הבודדים.
  • אם R8 קובע שלמחלקה יש רק תת-מחלקה ייחודית אחת, והמחלקה עצמה לא נוצרת (לדוגמה, מחלקת בסיס מופשטת שמשמשת רק למחלקת יישום קונקרטית אחת), R8 יכול לשלב את שתי המחלקות ולהסיר מחלקה מהאפליקציה.
  • מידע נוסף זמין בפוסטים בבלוג של Jake Wharton בנושא אופטימיזציה של 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 הסופי של האפליקציה. כלומר אם רוצים לשמור את המחלקה ואת המאתחל הסטטי, צריך לכלול כלל Keep בקובץ הכללים כדי לעשות זאת.

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

מעקב אחר דוחות קריסות

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

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

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

-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

המאפיין LineNumberTable שומר מידע על המיקום של שיטות, כך שהמיקומים האלה מודפסים ב-traces של סטאק. המאפיין 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 להסיר אותו, כמו שמתואר בקטע איך להתאים אישית את המשאבים לשמירה.