כדי שהאפליקציה תהיה קטנה ומהירה ככל האפשר, צריך לבצע אופטימיזציה והקטנה
גרסת ה-build של הגרסה באמצעות isMinifyEnabled = true
.
כך מתאפשר כיווץ, שמסיר קוד ומשאבים שלא נמצאים בשימוש. ערפול קוד (obfuscation), שמקצר את השמות של הכיתות והחברים באפליקציה. וגם אופטימיזציה, שמבצעת אסטרטגיות אגרסיביות יותר כדי לצמצם עוד יותר את בגודל של האפליקציה ולשפר את הביצועים שלה. בדף הזה מתואר איך R8 מבצע את המשימות האלה מסוג זמן הידור (compile-time) בפרויקט שלכם, ואיך תוכלו להתאים אותן אותם.
כשיוצרים פרויקט באמצעות פלאגין Android Gradle 3.4.0 ומעלה, הפלאגין כבר לא משתמש ב-ProGuard כדי לבצע אופטימיזציה של קוד בזמן הידור. במקום זאת, הפלאגין עובד עם מהדר (compiler) R8 כדי לטפל בדברים הבאים משימות שקשורות לזמן הידור (compile):
- כיווץ קוד (או רעידת עצים): זיהוי והסרה בטוחה של קוד שאינו בשימוש כיתות, שדות, שיטות ומאפיינים מהאפליקציה ומהספרייה שלה של יחסי התלות (וזה כלי חשוב שעוזר לעקוף מגבלת הפניות של 64,000). לדוגמה, אם משתמשים רק מספר קטן של ממשקי API של תלות בספרייה, כיווץ יכול לזהות קוד ספרייה שהאפליקציה לא משתמשת בה, ומסירים רק את הקוד הזה מהאפליקציה. שפת תרגום מידע נוסף זמין בקטע בנושא כיווץ הקוד.
- כיווץ משאבים: מסיר משאבים שלא נמצאים בשימוש מהאפליקציה הארוזה, כולל משאבים שלא נמצאים בשימוש ביחסי התלות של ספריות האפליקציה שלכם. התכונה פועלת ב בשילוב עם כיווץ קוד, כך שכאשר מסירים קוד שלא נמצא בשימוש, בנוסף, אם יש משאבים שכבר לא מפנים אליהם, אפשר להסיר בבטחה. למידה מידע נוסף, עברו לקטע שבו לכווץ את המשאבים.
- אופטימיזציה: בודק ומשכתב את הקוד כדי לשפר את זמן הריצה
את הביצועים ולהפחית עוד יותר את גודל קובצי ה-DEX של האפליקציה. הזה
שיפור של עד 30% בביצועים של קוד בזמן הריצה, ושיפור משמעותי
ותזמון פריים. לדוגמה, אם R8 מזהה ש
else {}
הסתעפות להצהרה נתונה של 'אם/אחר' אף פעם לא מתבצעת, R8 מסיר את הקוד עבור ההסתעפותelse {}
. מידע נוסף זמין בקטע בנושא אופטימיזציה של קוד. - ערפול קוד (obfuscation) (או הקטנת מזהים): מקצר את שמות המחלקות. וחברים, ולכן קובצי DEX קטנים יותר. מידע נוסף זמין בכתובת בקטע שעוסק בערפול קוד (obfuscation) של הקוד.
כשיוצרים את גרסת ההפצה של האפליקציה, אפשר להגדיר את R8 כך שיפעלו את משימות ההידור שמתוארות למעלה בשבילכם. אפשר גם להשבית תוספים מסוימים לבצע משימות או להתאים אישית את ההתנהגות של R8 באמצעות קבצים של כללי ProGuard. למעשה, R8 פועל עם כל הקבצים הקיימים של כללי ProGuard, כך לא צריך לבצע שינויים בפלאגין של Android Gradle כדי להשתמש ב-R8 של הכללים הקיימים.
הפעלת כיווץ, ערפול קוד (obfuscation) ואופטימיזציה
כשמשתמשים ב-Android Studio 3.4 או בפלאגין של Android Gradle מגרסה 3.4.0 ואילך, R8 המהדר (compiler) שמוגדר כברירת מחדל שממיר את קוד הבייט של Java של הפרויקט ל-DEX בפורמט שפועל בפלטפורמת Android. עם זאת, כשיוצרים פרויקט חדש באמצעות Android Studio, לא ניתן לבצע כיווץ, ערפול קוד (obfuscation) ואופטימיזציה של קוד מופעלת כברירת מחדל. הסיבה היא שהאופטימיזציות האלה בזמן הידור מגדילות את בזמן שאתם יוצרים את הפרויקט שלכם, ועלולים לגרום לבאגים אם לא להתאים אישית את הקוד לשמור.
לכן מומלץ להפעיל את המשימות האלה מסוג 'קומפילציה' בזמן יצירת הגרסה הסופית. של האפליקציה שאתם בודקים לפני הפרסום. כדי להפעיל כיווץ, ערפול קוד (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" ) } } ... }
מגניב
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 כדי לשנות את התנהגות ברירת המחדל שלו להבין את מבנה האפליקציה, למשל הכיתות שמשמשות כנקודות כניסה בקוד של האפליקציה. יש אפשרות לשנות חלק מקובצי הכללים האלה, אבל עשויים להיווצר באופן אוטומטי באמצעות כלים לזמן הידור (compile), כמו AAPT2, או עוברות בירושה מפריטי התלות של הספריות של האפליקציה. הטבלה הבאה מתארת את מקורות של קובצי כללי ProGuard שמשמשים את R8.
מקור | מיקום | תיאור |
סטודיו ל-Android | <module-dir>/proguard-rules.pro
|
כשיוצרים מודול חדש באמצעות Android Studio, סביבת הפיתוח המשולבת (IDE) יוצרת
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) | אחרי בניית הפרויקט באמצעות minifyEnabled true :
<module-dir>/build/intermediates/aapt_proguard_file/.../aapt_rules.txt
|
התכונה AAPT2 יוצרת כללים שמבוססים על הפניות לכיתות באפליקציה מניפסט, פריסות ומשאבים אחרים של אפליקציות. לדוגמה, AAPT2 כולל לשמור את הכלל לכל פעילות שרושמים במניפסט של האפליקציה לנקודת הכניסה. |
קובצי תצורה בהתאמה אישית | כברירת מחדל, כשיוצרים מודול חדש באמצעות Android Studio, סביבת הפיתוח המשולבת (IDE)
יוצר <module-dir>/proguard-rules.pro כדי להוסיף תמונות משלך
כללים.
|
אפשר לכלול הגדרות נוספות, ו-R8 מחילה אותם בזמן הידור (compile). |
כשמגדירים את המאפיין minifyEnabled
לערך true
, המערכת של R8 משלבת כללים מכל
מהמקורות הזמינים שצוינו למעלה. חשוב לזכור כאשר
לפתור בעיות ב-R8, בגלל יחסי תלות אחרים של זמן הידור,
למשל יחסי תלות של ספריות, עלולים לגרום לשינויים בהתנהגות R8
לא יודעים על כך.
כדי להפיק דוח מלא של כל הכללים שיוחלו על ידי R8 כשיוצרים את
את הפרויקט, כוללים את הפרטים הבאים בקובץ proguard-rules.pro
של המודול:
// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt
כללי כיווץ מטורגט
הפלאגין Android Gradle בגרסה 3.6 ואילך תומך בספריות כללים שמתמקדים מכווצים ספציפיים (R8 או ProGuard), וגם גרסאות ספציפיות של כיווץים. הזה שמאפשר למפתחי ספריות להתאים אישית את הכללים שלהם כך שיפעלו בצורה אופטימלית בפרויקטים שמשתמשות בגרסאות כיווץ חדשות, ומאפשרות לכללים הקיימים להמשיך שמשמשים בפרויקטים עם גרסאות ישנות יותר של 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
או הספרייה 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 מלפני יותר מ-3.6 או לכלים אחרים.
לכלול הגדרות נוספות
כשיוצרים פרויקט או מודול חדש באמצעות Android Studio, סביבת הפיתוח המשולבת (IDE) יוצרת
<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") } } }
מגניב
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 מסיר קוד זה בעת כיווץ האפליקציה.
איור 1. בזמן הידור (compile), R8 בונה שמבוסס על כללי השמירה המשולבים של הפרויקט, כדי לזהות קוד שלא ניתן להגיע אליו.
R8 קובע את נקודות הכניסה באמצעות כללי -keep
בפרויקט
קובצי תצורה של R8. כלומר, הכללים צריכים לציין
מחלקות R8 לא אמורות למחוק כאשר מכווצים את האפליקציה, ו-R8 לוקח בחשבון
את הכיתות האלה ככל האפשר בנקודות כניסה לאפליקציה. הפלאגין של Android Gradle
ו-AAPT2 יוצרים באופן אוטומטי כללי שמירה שנדרשים על ידי רוב האפליקציות
פרויקטים בשבילך, כמו הפעילויות, התצוגות והשירותים של האפליקציה שלך. אבל, לפעמים
אם אתם צריכים להתאים אישית את התנהגות ברירת המחדל הזו באמצעות כללי שמירה נוספים, תוכלו לקרוא
בסעיף שמסביר איך להתאים אישית את הקוד לשמור.
אם רוצים במקום זאת לצמצם את גודל המשאבים באפליקציה, דלגו לקטע בנושא כיווץ המשאבים.
הערה: אם פרויקט ספרייה מכווץ, אפליקציה שתלויה בספרייה הזו כולל מחלקות ספריות מכווצות. ייתכן שתצטרכו לשנות את כללי השמירה בספרייה אם חסרות כיתות ב-APK של הספרייה. אם אתם יוצרים ומפרסמים ספרייה בפורמט AAR, קובצי JAR מקומיים שהספרייה שלך תלויה בהם לא התכווצו בקובץ ה-AAR.
התאמה אישית של הקוד שיישמר
ברוב המצבים, קובץ ברירת המחדל של כללי ProGuard (proguard-android-optimize.txt
)
מספיק ל-R8 כדי להסיר רק את הקוד שלא נמצא בשימוש. אבל, לפעמים
יש מצבים מסוימים שקשה ל-R8 לנתח בצורה נכונה, והוא עלול להוביל להסרה
הקוד שהאפליקציה צריכה בפועל. כמה דוגמאות למקרים שבהם יכול להיות שההסרה שגויה
כוללים את הקוד:
- כשהאפליקציה שולחת קריאה ל-method מתוך ממשק המקור של Java (JNI)
- כשהאפליקציה מחפשת קוד בזמן ריצה (למשל, באמצעות השתקפות)
בדיקת האפליקציה אמורה לגלות שגיאות שנגרמו כתוצאה מהסרה לא הולמת אבל אפשר גם לבדוק איזה קוד הוסר על ידי יצירת דוח על קוד שהוסר.
כדי לתקן שגיאות ולאלץ את R8 לשמור קוד מסוים, צריך להוסיף
-keep
בקובץ הכללים של ProGuard. לדוגמה:
-keep public class MyClass
לחלופין, אפשר להוסיף את
הערה @Keep
לקוד שאתם
שרוצים לשמור. אם מוסיפים את @Keep
בכיתה, כל הכיתה נשארת כפי שהיא.
אם מוסיפים אותו לשיטה או לשדה, גם השיטה/השדה (והשם שלהם) יישמרו
כששם הכיתה נשמר. לתשומת ליבכם: ההערה הזו זמינה רק כשמשתמשים
ה
ספריית ההערות ב-AndroidX
וכשכוללים את קובץ כללי ProGuard שנארז עם נתוני Android,
הפלאגין של Gradle, כמו שמתואר בקטע שמסביר איך
להפעיל כיווץ.
יש הרבה שיקולים שצריך להביא בחשבון כשמשתמשים באפשרות -keep
. עבור
לקבלת מידע נוסף על התאמה אישית של קובץ הכללים, ניתן לקרוא את
המדריך של ProGuard.
פתרון בעיות
מפורטות בעיות נפוצות נוספות שאתם עשויים להיתקל בהן כאשר
שנעלם.
הצגת ספריות מקוריות ב-Strip
כברירת מחדל, ספריות של קוד נייטיב מוסרות בגרסאות build של האפליקציה. ההסרה הזו כוללת הסרה של טבלת הסמלים ומידע על תוצאות ניפוי הבאגים נכללות בספריות המקוריות שבהן האפליקציה שלך משתמשת. הסרת קוד מקורי ספריות מובילות לחיסכון משמעותי בגודל; עם זאת, לא ניתן לאבחן קורסת ב-Google Play Console בגלל המידע החסר (כמו שמות של מחלקות ופונקציות).
תמיכה בקריסה במקור
ב-Google Play Console מדווחות קריסות מקוריות בקטע תפקוד האפליקציה. עם כמה אפשר ליצור ולהעלות קובץ סמלים מקורי של ניפוי באגים לאפליקציה. הקובץ הזה מאפשר דוחות קריסות נייטיב, שמוצגים בהם סמלי קריסות נייטיב (כולל סיווג ו שמות הפונקציות) בתפקוד האפליקציה כדי לנפות באגים באפליקציה בסביבת הייצור. השלבים האלה משתנים בהתאם לגרסת הפלאגין של 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. הרמה הזו תומכת במצבות. - כדי לקבל שמות של פונקציות, קבצים ומספרי שורות ב-Play Console צריך להשתמש ב-
FULL
דוחות קריסות סימבוליים.
אם יוצרים 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
דחוס את התוכן של הספרייה הזו:
cd app/build/intermediates/cmake/universal/release/obj
zip -r symbols.zip .
באופן ידני להעלות את הקובץ
symbols.zip
אל Google Play Console.
כיווץ המשאבים
כיווץ המשאבים פועל רק בשילוב עם כיווץ קוד. אחרי shrinker מסיר את כל הקוד שלא נמצא בשימוש, shrinker של המשאב יכול לזהות משאבים שהאפליקציה עדיין משתמשת בהם. זה נכון במיוחד כשמוסיפים קוד ספריות שכוללות משאבים — צריך להסיר קוד ספרייה שלא נמצא בשימוש אין הפניה למשאבי הספרייה, וכתוצאה מכך הם ניתנים להסרה על ידי המשאב קטן יותר.
כדי להפעיל כיווץ משאבים, צריך להגדיר את המאפיין shrinkResources
אל true
בסקריפט ה-build (לצד
minifyEnabled
לכיווץ קוד). לדוגמה:
Kotlin
android { ... buildTypes { getByName("release") { isShrinkResources = true isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" ) } } }
מגניב
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());
shrinker של המשאב גם בודק את כל הקבועים של המחרוזת ב-
וגם מגוון משאבים של 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 גם ממזגת משאבים בעלי שם זהה, כמו
פריטים גרפיים עם אותו שם שעשויים להיות בתיקיות משאבים שונות. הזה
ההתנהגות לא נשלטת על ידי המאפיין 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) היא להקטין את גודל האפליקציה על ידי קיצור השמות של הכיתות, השיטות והשדות באפליקציה. הדוגמה הבאה היא ערפול קוד (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 שמוסיפים לאינדקס הרבה מחלקות, שיטות ושדות. עם זאת, מכיוון שערפול קוד (obfuscation) משנה את השם של חלקים שונים בקוד, משימות מסוימות, כמו בדיקת דוחות של קריסות, נדרשים כלים נוספים. כדי להבין את דוח קריסות אחרי ערפול קוד (obfuscation), יש לקרוא את הקטע שמסביר לפענח דוח קריסות מעורפל.
בנוסף, אם הקוד מסתמך על מתן שמות צפויים לשיטות האפליקציה שלך והשיעורים - כאשר משתמשים בחשיבה, לדוגמה, צריך להתייחס אליהם כנקודות כניסה ולציין את כללי השמירה שלהן, כפי שמתואר שמסביר איך להתאים אישית את הקוד לשמור. אלו שנשארים הכללים מורים ל-R8 לא רק לשמור את הקוד הזה ב-DEX הסופי של האפליקציה, אלא גם לשמור את השם המקורי שלו.
פענוח של דוח קריסות מעורפל
אחרי ש-R8 מעורפל את הקוד, קשה להבין את דוח הקריסות (אם זה לא אפשרי), מכיוון ששמות של מחלקות ושיטות השתנה. כדי לקבל את דוח הקריסות המקורי, לשחזר את דוח הקריסות.
אופטימיזציה של קוד
כדי לבצע אופטימיזציה נוספת של האפליקציה, R8 בודק את הקוד לעומק כדי להסיר קודים נוספים שלא נמצאים בשימוש, או, במידת האפשר, לשכתב את הקוד כדי הוא פחות מפורט. דוגמאות לאופטימיזציות כאלה:
- אם הקוד אף פעם לא לוקח את ההסתעפות
else {}
עבור הצהרה נתונה מסוג if/else, R8 עשוי להסיר את הקוד של ההסתעפותelse {}
. - אם הקוד מפעיל שיטה בכמה מקומות בלבד, R8 עשוי להסיר את השיטה ולהטמיע אותו באתרים המוקדמים של התקשרות.
- אם R8 קובע שלכיתה יש רק מחלקה ייחודית אחת, והמחלקה אין יצירה של עצמו (לדוגמה, סיווג בסיס מופשט שנעשה בו שימוש רק מחלקה אחת של הטמעה קונקרטית), אז R8 יכול לשלב את שני המחלקות להסיר כיתה מהאפליקציה.
- למידע נוסף, אפשר לקרוא את פוסטים של אופטימיזציה בבלוג R8 מאת ג'ייק וורטון.
R8 לא מאפשר להשבית או להפעיל אופטימיזציות נפרדות, או לשנות את
פעולת אופטימיזציה. למעשה, R8 מתעלם מכל כללי ProGuard שמנסים
כדי לשנות אופטימיזציות ברירת מחדל, כמו -optimizations
ו
-optimizationpasses
. ההגבלה הזו חשובה כי R8 ממשיכה
לשפר ושמירה על התנהגות סטנדרטית של אופטימיזציות
הצוות של Studio יכול לפתור בקלות בעיות שנתקלת בהן.
שימו לב שהפעלת האופטימיזציה תשנה את דוחות הקריסות של תרגום מכונה. לדוגמה, הטבעה מסירה את הפריימים במקבץ. צפייה בקטע שעוסק ב- מעקב כדי ללמוד איך להשיג את דוחות הקריסות המקוריים.
ההשפעה על הביצועים של סביבת זמן הריצה
אם כל הפעולות של כיווץ, ערפול קוד ואופטימיזציה (obfuscation) מופעלות, R8 ישתפר ביצועי הקוד בזמן הריצה (כולל זמן ההפעלה וזמן הפריים ב-thread של ממשק המשתמש) בשיעור של עד 30%. השבתת האפשרות הזו מגבילה באופן משמעותי את קבוצת האופטימיזציות R8 שימושים.
אם R8 מופעל, צריך גם ליצור פרופילים לסטארט-אפ לביצועי הפעלה טובים עוד יותר.
להפעיל אופטימיזציות אגרסיביות יותר
גרסה R8 כוללת קבוצה של אופטימיזציות נוספות (שנקראות 'מצב מלא') שגורמת לו להתנהג באופן שונה מ-ProGuard. האופטימיזציות האלה מופעלות על ידי ברירת מחדל מאז גרסה 8.0.0 של הפלאגין ל-Android Gradle
כדי להשבית את האופטימיזציות האלה, צריך לכלול את האופטימיזציות הבאות ב
קובץ gradle.properties
של הפרויקט:
android.enableR8.fullMode=false
מכיוון שהאופטימיזציות הנוספות גורמות ל-R8 להתנהג באופן שונה מ-ProGuard, הם עשויים לדרוש מכם לכלול כללים נוספים של ProGuard כדי למנוע זמן ריצה אם אתם משתמשים בכללים שמיועדים ל-ProGuard. לדוגמה, נניח שאתם מפנה למחלקה דרך ממשק ה-API של Java Reflection. כשלא משתמשים "full mode", R8 מניח שאתם מתכוונים לבחון ולשנות אובייקטים של המחלקה הזו בזמן הריצה - גם אם הקוד בפועל לא קיים - והוא מופעל באופן אוטומטי שומר את המחלקה ואת המאתחל הסטטי שלו.
עם זאת, כשמשתמשים ב'מצב מלא', R8 לא מניח את ההנחה הזו, ובמקרה של R8 מצהיר/ה שהקוד שלך אף פעם לא משתמש במחלקה בזמן הריצה, הוא מסיר את מה-DEX הסופי של האפליקציה שלכם. כלומר, אם אתם רוצים להשאיר את הכיתה מאתחל סטטי, כדי לעשות זאת עליך לכלול כלל שמירה בקובץ הכללים ש.
אם תיתקלו בבעיות במהלך השימוש ב'מצב מלא' של R8, עיינו ב דף השאלות הנפוצות לגבי R8 לפתרון אפשרי. אם לא הצלחת לפתור את הבעיה, לדווח על באג.
מעקב אחר דוחות קריסות
הקוד שמעובד על ידי R8 משתנה בדרכים שונות שיכולות ליצור דוחות קריסות קשה יותר להבין את זה, כי דוחות הקריסות לא מתאימים בדיוק לקוד המקור. הזה יכול להיות רלוונטי לשינויים במספרי השורות כשמידע על ניפוי הבאגים לא נשמרו. המצב הזה יכול לנבוע מאופטימיזציות, כמו הבלטה וקווים כלליים. התורם הגדול ביותר הוא ערפול קוד (obfuscation), שבו גם הכיתות והשיטות לשנות שמות.
כדי לשחזר את דוח הקריסות המקורי, ב-R8 מקבלים את כלי שורת הפקודה retrace, בחבילה עם חבילת כלי שורת הפקודה.
כדי לתמוך במעקב אחרי דוחות הקריסות של האפליקציה, צריך לוודא
שומר מספיק מידע לצורך שחזור על ידי הוספת הקוד הבא
כללים לקובץ proguard-rules.pro
של המודול:
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
המאפיין LineNumberTable
שומר מידע לגבי המיקום
באחת מהשיטות האלה כדי שהמיקומים האלה יודפסו בדוחות קריסות. המאפיין SourceFile
מבטיחה שבכל זמני הריצה הפוטנציאליים יודפסו הנתונים תלויי המיקום.
ההוראה -renamesourcefileattribute
מגדירה את השם של קובץ המקור במקבץ
עובר ל-SourceFile
בלבד. השם המקורי של קובץ המקור בפועל אינו
נדרש בעת ביצוע מעקב, מפני שקובץ המיפוי מכיל את קובץ המקור המקורי.
R8 יוצר קובץ mapping.txt
בכל פעם שהוא מופעל,
מכיל את המידע הנחוץ למיפוי דוחות הקריסות בחזרה למקור
דוחות קריסות. Android Studio שומר את הקובץ
<module-name>/build/outputs/mapping/<build-type>/
כשמפרסמים את האפליקציה ב-Google Play, אפשר להעלות את הקובץ mapping.txt
לכל גרסה של האפליקציה. כשמפרסמים באמצעות קובצי Android App Bundle,
נכלל באופן אוטומטי כחלק מהתוכן של ה-App Bundle. ואז Google
מערכת Play תעקוב אחר דוחות קריסות נכנסים מבעיות שדווחו על ידי משתמשים, כדי
אפשר לעיין בהן ב-Play Console. מידע נוסף זמין במרכז העזרה
מאמר שמסביר איך
פענוח קוד מעורפל של דוחות קריסה.
פתרון בעיות ב-R8
בקטע הזה מתוארות כמה אסטרטגיות לפתרון בעיות בהפעלה כיווץ, ערפול קוד (obfuscation) ואופטימיזציה באמצעות 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
בחלון הזה מוצג סיכום של
משאבים שהוסרו מהאפליקציה. (קודם צריך ללחוץ על הצגה או הסתרה
בצד שמאל של החלון כדי להציג פלט טקסט מפורט מ-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
(כלומר, המזהה שניתן להזזה שלו היה
שנמצא בקוד שניתן לגשת אליו).
אם לא משתמשים בבדיקה מחמירה, אפשר לסמן את מזהי המשאבים כנגישים אם יש קבועים של מחרוזות שנראים כאילו אפשר להשתמש בהם כדי לבנות שמות של משאבים שנטענים באופן דינמי. במקרה הזה, אם תבצעו חיפוש את פלט ה-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 להסיר אותו,
כפי שמתואר בקטע על התאמה אישית של המשאבים לשמירה.