כדי שהאפליקציה תהיה קטנה ומהירה ככל האפשר, כדאי לבצע אופטימיזציה ומיינימיזציה של גרסה ה-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 של הפרויקט, ויכול להיות שהן יגרמו לבאגים אם לא תתאימו אישית את הקוד שרוצים לשמור.
לכן, מומלץ להפעיל את המשימות האלה בזמן הידור כשמפתחים את הגרסה הסופית של האפליקציה שבודקים לפני הפרסום. כדי להפעיל את התכונות של צמצום, ערפול ואופטימיזציה, צריך לכלול את השורות הבאות בסקריפט ה-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 שהוגדרו מראש, אבל מומלץ להשתמש ב- |
יחסי תלות בספרייה |
בספרייה של AAR:
בספריית JAR: בנוסף למיקומים האלה, גם בפלאגין Android Gradle 3.6 ואילך יש תמיכה בכללים ייעודיים לצמצום קוד. |
אם ספריית AAR או JAR מתפרסמת עם קובץ כללים משלה, ואתם כוללים את הספרייה הזו כיחסי תלות בזמן הידור, R8 מחיל את הכללים האלה באופן אוטומטי בזמן הידור הפרויקט. בנוסף לכללי ProGuard הרגילים, הפלאגין של Android Gradle בגרסה 3.6 ואילך תומך גם בכללי צמצום ממוקדים. אלה כללים שמטרגטים מכשירי דחיסה ספציפיים (R8 או ProGuard), וגם גרסאות ספציפיות של מכשירי דחיסה. כדאי להשתמש בקובצי כללים שארוזים עם ספריות אם יש כללים מסוימים שנדרשים כדי שהספרייה תפעל כמו שצריך – כלומר, מפתח הספרייה ביצע בשבילכם את שלבי פתרון הבעיות. עם זאת, חשוב לזכור שהכללים מוסיפים זה לזה, ולכן אי אפשר להסיר כללים מסוימים שכוללת יחסי התלות בספרייה, והם עשויים להשפיע על הידור של חלקים אחרים באפליקציה. לדוגמה, אם ספרייה כוללת כלל להשבתת אופטימיזציות של קוד, הכלל הזה משבית את האופטימיזציות בכל הפרויקט. |
Android Asset Package Tool 2 (AAPT2) | אחרי ה-build של הפרויקט באמצעות 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 מחילה בזמן ה-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 קובע שאפשר להגיע לשיטות foo()
, faz()
ו-bar()
מנקודת הכניסה MainActivity.class
. עם זאת, האפליקציה שלכם אף פעם לא משתמשת בכיתה OkayApi.class
או בשיטה baz()
במהלך זמן הריצה, ו-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.
בקטע פתרון בעיות מפורטות בעיות נפוצות אחרות שעשויות להתרחש אם הקוד שלכם יוסר.
הסרת ספריות מקוריות
כברירת מחדל, ספריות של קוד מקורי מוסרות בגרסאות build של האפליקציה. ההסרה הזו כוללת הסרה של טבלת הסמלים ומידע על ניפוי באגים שמכילות כל הספריות המקומיות שבהן האפליקציה משתמשת. הסרה של ספריות של קוד מקורי מובילה לחיסכון משמעותי בגודל, אבל אי אפשר לאבחן קריסות במסוף Google Play בגלל המידע החסר (כמו שמות של כיתות ופונקציות).
תמיכה בקריסה ברמת שפת המכונה
ב-Google Play Console מדווחים על קריסות מקוריות בקטע נתוני תפקוד האפליקציה ב-Android. תוכלו ליצור ולהעלות קובץ סמלים מקומי לניפוי באגים לאפליקציה שלכם בכמה שלבים פשוטים. הקובץ הזה מאפשר לכם להציג ב-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
מעבירים את התוכן של הספרייה הזו לקובץ zip:
cd app/build/intermediates/cmake/universal/release/obj
zip -r symbols.zip .
מעלים את הקובץ
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")) } }
Groovy
android { defaultConfig { ... resConfigs "en", "fr" } }
כשמשחררים אפליקציה בפורמט Android App Bundle, כברירת מחדל, רק השפות שמוגדרות במכשיר של המשתמש מורידות בזמן התקנת האפליקציה. באופן דומה, רק משאבים שתואמים לצפיפות המסך של המכשיר וספריות מקומיות שתואמות ל-ABI של המכשיר נכללים בהורדה. למידע נוסף, קראו את המאמר הגדרת Android App Bundle.
באפליקציות מדור קודם שפורסמו באמצעות חבילות 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/
מכילים משאבים זהים.
ערפול הקוד
מטרת הערפול היא לצמצם את גודל האפליקציה על ידי קיצור השמות של המחלקות, השיטות והשדות של האפליקציה. דוגמה להסתרה באמצעות 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
ערפול קוד לא מסיר קוד מהאפליקציה, אבל אפשר לחסוך משמעותית בגודל של אפליקציות עם קובצי DEX שמוסיפים לאינדקס הרבה מחלקות, שיטות ושדות. עם זאת, מאחר שהערפול משנה את השמות של חלקים שונים בקוד, יש משימות מסוימות, כמו בדיקת נתוני מעקב ב-stack, שדורשות כלים נוספים. כדי להבין את פענוח קוד מעורפל של דוח קריסות אחרי ההסתרה, כדאי לקרוא את הקטע בנושא.
בנוסף, אם הקוד שלכם מסתמך על שמות צפויים לשיטות ולכיתות של האפליקציה – למשל, כשמשתמשים בהשתקפות (reflection) – עליכם להתייחס לחתימות האלה כנקודות כניסה ולציין להן כללי שמירה, כפי שמתואר בקטע בנושא התאמה אישית של הקוד שרוצים לשמור. כללי השמירה האלה מאפשרים ל-R8 לא רק לשמור את הקוד הזה ב-DEX הסופי של האפליקציה, אלא גם לשמור על השם המקורי שלו.
פענוח קוד מעורפל של דוח קריסות
אחרי ש-R8 מבצע ערפול לקוד, קשה (אם לא בלתי אפשרי) להבין את נתיב הסטאק כי יכול להיות ששמות הכיתות והשיטות השתנו. כדי לקבל את נתיב הסטאק המקורי, צריך לעקוב שוב אחרי נתיב הסטאק.
אופטימיזציה של קוד
כדי לבצע אופטימיזציה נוספת של האפליקציה, R8 בודק את הקוד ברמה עמוקה יותר כדי להסיר עוד קוד שלא בשימוש, או, במקרים שבהם הדבר אפשרי, לשכתב את הקוד כך שיהיה פחות מפורט. ריכזנו כאן כמה דוגמאות לאופטימיזציות כאלה:
- אם הקוד שלכם אף פעם לא עובר להסתעפות
else {}
בהצהרה if/else מסוימת, יכול להיות ש-R8 תסיר את הקוד של ההסתעפותelse {}
. - אם הקוד קורא לשיטה רק במספר מקומות מצומצם, יכול להיות ש-R8 יסיר את השיטה ויוסיף אותה בתוך שורת הקוד במקומות הקריאה הבודדים.
- אם R8 קובע שלכיתה יש רק תת-כיתה ייחודית אחת, והכיתה עצמה לא נוצרת (לדוגמה, מחלקת בסיס מופשטת שמשמשת רק למחלקת יישום קונקרטית אחת), R8 יכול לשלב את שתי הכיתות ולהסיר את אחת מהן מהאפליקציה.
- מידע נוסף זמין בפוסטים בבלוג של Jake Wharton בנושא אופטימיזציה של R8.
ב-R8 אי אפשר להשבית או להפעיל אופטימיזציות נפרדות, או לשנות את ההתנהגות של אופטימיזציה. למעשה, R8 מתעלם מכל כללי ProGuard שמנסים לשנות אופטימיזציות ברירת המחדל, כמו -optimizations
ו--optimizationpasses
. ההגבלה הזו חשובה כי ככל ש-R8 יתפתח, שמירה על התנהגות רגילה של אופטימיזציות תעזור לצוות Android Studio לפתור בקלות בעיות שעשויות לצוץ.
חשוב לזכור שהפעלת האופטימיזציה תשנה את נתוני המעקב אחר סטאק של האפליקציה. לדוגמה, הטמעה בקוד תסיר מסגרות של סטאק. בקטע מעקב חוזר מוסבר איך לקבל את מעקב ה-stack המקורי.
ההשפעה על הביצועים בסביבת זמן הריצה
אם תפעילו את כל התכונות של 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
שומר מידע על המיקום של שיטות, כך שהמיקומים האלה מודפסים ב-traces של סטאק. המאפיין SourceFile
מבטיח שכל סביבות זמן הריצה הפוטנציאליות ידפיסו את פרטי המיקום בפועל. ההנחיה -renamesourcefileattribute
מגדירה את שם קובץ המקור בשרטוטי ה-stack כ-SourceFile
בלבד. שם הקובץ המקורי של המקור לא נדרש במהלך המעקב מחדש, כי קובץ המיפוי מכיל את קובץ המקור המקורי.
R8 יוצר קובץ mapping.txt
בכל פעם שהוא פועל, שמכיל את המידע הדרוש למיפוי של מעקב ה-stack בחזרה למעקב ה-stack המקורי. Android Studio שומר את הקובץ בספרייה <module-name>/build/outputs/mapping/<build-type>/
.
כשמפרסמים את האפליקציה ב-Google Play, אפשר להעלות את הקובץ mapping.txt
לכל גרסה של האפליקציה. כשמפרסמים באמצעות קובצי Android App Bundle, הקובץ הזה נכלל באופן אוטומטי כחלק מתוכן חבילת האפליקציות. לאחר מכן, Google Play תתעד מחדש את נתוני המעקב אחר סטאק (stack trace) הנכנסים מהבעיות שדווחו על ידי משתמשים, כדי שתוכלו לבדוק אותם ב-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 להסיר אותו, כפי שמתואר בקטע התאמה אישית של המשאבים שרוצים לשמור.