מערכת ה-build של פלטפורמת Android (Soong) מריצה את קומפיילר R8 על יעדים שמקמפלים קוד בייט של DEX, כולל אפליקציות ל-Android (android_app), בדיקות (android_test) וספריות Java שניתנות להתקנה עם compile_dex: true (כמו services.jar), כדי לצמצם, לבצע אופטימיזציה ולהסיר קוד ומשאבים שלא בשימוש. בספריות סטטיות (android_library וסטטיות java_library), Soong לא מפעיל את R8 ישירות, אלא משתמש בבלוק המאפיינים optimize כדי לצרף ולהפיץ כללי שמירה של צרכנים (export_proguard_flags_files: true) ליעדי DEX במורד הזרם שמקשרים אותם באופן סטטי.
מהנדסי פלטפורמות שיוצרים תמונות מערכת או חבילות ספקים יכולים להפיק תועלת ישירה ממחיקת כללי שמירה רחבים מדי:
- טביעת רגל קטנה יותר של מחיצת המערכת: R8 מסירה מחלקות, שיטות ומשאבים לא פעילים לפני אריזת קובצי APK ו-JAR ב-
/system,/system_ext,/productו-/vendor. - ארטיפקטים קומפילטיביים קטנים יותר: פחות שיטות DEX פירושן קובצי
.odexו-.vdexקטנים יותר שנוצרים על ידיdex2oatבמהלך זמן הבנייה או הקומפילציה במכשיר. - הפחתת העומס על הזיכרון בזמן הריצה: מערכת Android ממפה את
.odex,.vdexוטבלאות של משאבי APK לזיכרון התהליך באמצעות קריאותmmapשל דפדוף לפי דרישה. קובצי הפעלה קטנים יותר מפחיתים את מספר התקלות בדפדוף במהלך הפעלת האפליקציה, ומקטינים את הזיכרון של קוד ומשאבים שמוקצים לתהליכים. מידע נוסף על ההשפעה של קוד אפליקציה שעבר קומפילציה על הזיכרון של המכשיר זמין במאמר קוד האפליקציה הוא זיכרון. - אופטימיזציה יעילה יותר של התוכנית כולה: אילוצי שמירה מצומצמים מאפשרים לשיטות מוטבעות של R8, לבטל את הווירטואליזציה של ממשקים וקריאות וירטואליות, להסיר שדות לא בשימוש ולהפיץ קבועים בין מחלקות.
הגדרת אופטימיזציה של R8 ב-Soong
בקובצי Android.bp, מגדירים את האופטימיזציה של R8 באמצעות המאפיין optimize block ב-android_app ובמטרות java_library שניתנות להתקנה (או במודולים android_library סטטיים java_library כדי לייצא כללי שמירה של צרכנים):
android_app {
name: "MySystemApp",
srcs: ["src/**/*.java"],
optimize: {
obfuscate: true,
shrink_resources: true,
},
}
בטבלה הבאה מפורטים המאפיינים הנפוצים ביותר של optimize ב-Soong (מוגדרים ב-build/soong/java/dex.go):
| מאפיין (property) | תיאור |
|---|---|
enabled |
הגדרה שקובעת אם R8 יפעל ביעד. ברירת המחדל היא true
לכל יעדי android_app.
|
shrink |
ההגדרה קובעת אם תתבצע אופטימיזציה של הקוד כדי להסיר מחלקות, שדות ושיטות שלא ניתן להגיע אליהם. ערך ברירת המחדל הוא true עבור יעדי android_app (false עבור מודולים עצמאיים של java_library ומודולים לבדיקה, שעוברים -dontshrink אלא אם הוגדר ערך אחר).
|
optimize |
שולטת באופטימיזציות של קוד בייט, כמו הטמעת מתודות, מיזוג מחלקות, הפצת קבועים והסרת ענפים מתים. ברירת המחדל היא true ליעדי android_app (נשלט על ידי דגל בניית הגרסה RELEASE_R8_OPTIMIZE_BY_DEFAULT).
|
obfuscate |
המדיניות קובעת את ההגדרות של מזעור המזהים ושינוי השם שלהם. ערך ברירת המחדל הוא false לצורך תאימות היסטורית, אבל מומלץ להגדיר את הערך ל-true בכל מקום שאפשר כדי להקטין את גודל ה-DEX ולבצע אופטימיזציות מתקדמות יותר (ראו הפעלה של טשטוש בכל מקום שאפשר).
|
shrink_resources |
הכלי מסיר משאבים שלא בשימוש (רשומות res/) מחבילת ה-APK אחרי כיווץ קוד. ברירת המחדל היא false.
|
optimized_shrink_resources |
מריץ את צינור האופטימיזציה המשולב של קוד ומשאבים R8, כך ש-R8 עוקב אחרי קוד והפניות למשאבים יחד במעבר יחיד. כאשר ההגדרה
shrink_resources: true מוגדרת, ברירת המחדל היא
RELEASE_USE_OPTIMIZED_RESOURCE_SHRINKING_BY_DEFAULT
(true בגרסאות רגילות של הפלטפורמה).
|
proguard_flags_files |
רשימת קבצים ספציפיים למודול .flags או .pro
שמכילים כללי שמירה מותאמים אישית. אל תוסיפו קבצים מותאמים אישית אם הערות רגילות או הגדרות ברירת מחדל מספיקות.
|
export_proguard_flags_files |
הספרייה הזו מעבירה את proguard_flags_files למודולים במורד הזרם שתלויים בה באופן סטטי.
|
trace_references_from |
מפרט יעדים נלווים של ספריית Java שהבייטקוד שלהם מפנה אל היעד הזה. R8 עוקב אחרי היעדים האלה ושומר אותם באופן אוטומטי. |
הפעלה של טשטוש בכל מקום שאפשר
ב-Soong, obfuscate מוגדר כברירת מחדל ל-false לצורך תאימות היסטורית, אבל מומלץ להגדיר במפורש את obfuscate: true ב-android_app וב-DEX עצמאיים בכל מקום שאפשר:
- קובץ DEX קטן יותר ו
.vdexטביעת רגל קטנה יותר: שינוי השם של חבילות, מחלקות, שדות ומתודות למזהים קצרים (a,b) מקטין את מאגר המחרוזות של DEX (string_idsו-string_data_item) ואת מתארי הסוג. מכיוון שמערכת Android ממפה את הקבצים.vdexו-.odexלזיכרון התהליך, טבלאות סמלים קטנות יותר מפחיתות באופן ישיר את גודל מחיצת המערכת ואת הזיכרון שבשימוש בזמן הריצה. - אופטימיזציות מעמיקות יותר של התוכנית כולה: אם מאפשרים ל-R8 לשנות את השם של מזהים, אפשר לבצע מיזוג של מחלקות, שיטוח של חבילות וביטול כפילויות של מחלקות סינתטיות בחבילות ש-R8 צריך לדלג עליהן אחרת כדי למנוע התנגשויות בשמות.
- הנגשת דוח קריסה בשפה אנושית (symbolication) מלאה של דוח קריסות: הפעלת ערפול קוד (obfuscation) ב-Soong לא מפחיתה את היכולת לבצע ניפוי באגים. לכל יעד שעבר קומפילציה ב-R8, Soong פולט קובץ מיפוי
proguard_dictionaryבספריית הביניים של המודול, חבילות של כל מילוני המודולים לתוך ארטיפקטproguard-dict.zipשל ה-build, מטמיע גיבוב של מיפוי (--map-id-template) בכותרת של DEX, וכותב מחדש מאפייני מחלקהSourceFile(--source-file-template) כדי ש-retraceיוכל לבצע סימבוליזציה של עקבות מחסנית באופן אוטומטי.
שומרים את obfuscate: false (או שומרים במפורש שמות של ממשקי API ציבוריים) רק במקרים הבאים:
- ספריות משותפות ב-bootclasspath או ב-
system_serverclasspath: מודולים (java_libraryאוjava_sdk_library) שחושפים ממשק API חיצוני שמקושר באופן דינמי על ידי מודולים אחרים בזמן ריצה (כמו יעדיframework.jar,services.jarאו<uses-library>) צריכים לשמור עלobfuscate: falseאו לשמר באופן מפורש את ממשק ה-API שלהם באמצעות כללי@KeepForApiאו-keep(יחד עםprotect_api_surface: trueליעדי bootclasspath), כדי שקודים שקוראים למודולים האלה, שעברו קומפילציה מול קובצי ה-stub שלהם, יוכלו לפתור שמות של מחלקות ושל חברים בזמן ריצה.
לפני שמפעילים את obfuscate: true באפליקציה, צריך לוודא שמתקיימות הדרישות המוקדמות הבאות להעברה:
- תלות חיצונית ב-APK של בדיקה: אם קבוצות חיצוניות של
android_testAPKinstrumentation_for: "MyApp"מפעילות ישירות מחלקות פנימיות או שיטות שלMyApp, שינוי השם של הסמלים האלה גורם ל-NoSuchMethodErrorאו ל-NoClassDefFoundErrorבזמן הריצה של הבדיקה. מומלץ לבנות את הבדיקה כ-APK עם כלי מעקב עצמי שמקושרMyApp.implבאופן סטטי, להוסיף הערות לנקודות ההתחברות של הבדיקה באמצעות@VisibleForTesting, או להגדיר אתtrace_references_from(ראו שלב 2: העברת כללי שמירה מבוססי-בדיקה) כך שהסמלים הפנימיים שנדרשים לבדיקות יישמרו, וכל שאר האפליקציה תעבור טשטוש. - String-based reflection and JNI: אם מודול מחפש מחלקות, מתודות או שדות לפי שמות מחרוזות מילוליים (
Class.forName,getDeclaredMethodאו JNIFindClassו-GetMethodID) בלי כללי שמירה או הערות, טשטוש השמות משנה את השמות של יעדי החיפוש האלה וגורם לחיפושים בזמן הריצה להיכשל. (שימו לב שגם רפלקציה לא מוערת נכשלת במסגרתshrink: trueו-optimize: true). צריך להוסיף הערות לנקודות הכניסה האלה באמצעות Guide to Keep Annotations (@UsesReflection,@UsedByReflectionאו@UsedByNative) כדי ש-R8 ישמור את השמות שלהן ויטשטש את שאר המודול.
פועלים לפי העיקרון של אפס דגלים בהתאמה אישית
מודול פלטפורמת Android אידיאלי לא כולל קובצי proguard.flags או keep.xml מותאמים אישית. ב-build של פלטפורמת Android, רוב כללי השמירה המותאמים אישית הם מיותרים:
- קווים מנחים לפלטפורמה ו-AAPT2: רוב נקודות הכניסה כבר נשמרות באופן אוטומטי על ידי קווים מנחים גלובליים לפלטפורמה (כמו
@Keep, שיטות JNInativeו-@VisibleForTesting) או נוצרות על ידי AAPT2 מ-AndroidManifest.xmlומשאבי פריסה (ראו הסבר על כללי השמירה שמוגדרים כברירת מחדל בפלטפורמה). - חלופות ממוקדות: במקרים שבהם צריך לשמור על נקודות הכניסה, עדיף להשתמש בהערות באתר ההצהרה (
keepanno,@VisibleForTesting), בכללים של ספריות מיוצאות (export_proguard_flags_files: true) או בקישור סטטי לבדיקה, במקום בקובצי.flagsמנותקים (ראו ביקורת והעברה של כללי שמירה קיימים).
לפני שמוסיפים או שומרים קובץ מותאם אישית מסוג proguard.flags או keep.xml, צריך לוודא אם הכלל כבר מטופל על ידי קווי בסיס שמוגדרים כברירת מחדל או שאפשר להעביר אותו להערות קוד.
הסבר על כללי ברירת המחדל לשמירה בפלטפורמה
מערכת Soong מעבירה באופן אוטומטי את כללי הבסיס הגלובליים הבאים לכל הפעלה של R8 ביעדים של קומפילציית DEX (android_app, android_test ומודולים ניתנים להתקנה java_library), שמוגדרים ב-build/soong/java/dex.go:
-
build/make/core/proguard.flags: שומרת על מחלקות ועל חברים עם ההערה@com.android.internal.annotations.VisibleForTestingבכל החבילות, ועם ההערה@VisibleForTesting(androidx.annotation.VisibleForTestingאוcom.google.common.annotations.VisibleForTesting) בחבילותandroid.**,com.android.**ו-com.google.android.**. בנוסף, נשמרים גם@TestApi,@Keep(androidx.annotation,android.support.annotation,com.android.internal.annotations),@KeepForWeakReference,@WeaklyReferencedCallbackוגם@dalvik.annotation.optimization.**. -
build/make/core/proguard_basic_keeps.flags: שומרת על מאפייניSourceFileשל דוחות קריסות, הערות לגבי הרשאות גישה בזמן ריצה (RuntimeVisible*Annotations), מאפייניExceptionsו-AnnotationDefault, שיטותnative, רכיביSerializable, שיטות@JavascriptInterface, בנאיםThrowable(String), שדותParcelable$CREATORושדותMessageLiteשל פרוטוקול protobuf. -
build/make/core/proguard/kotlin.flags: משתיק אזהרות לא מזיקות לגבי הערות מטא ספציפיות של Kotlin (kotlin.Metadataו-kotlin.annotation.{AnnotationRetention,AnnotationTarget,Retention,Target}) ומסיר הערותDebugMetadataשל Kotlin בגרסאות build של הפצה. -
build/make/core/proguard/checknotnull.flags: מחליף קריאות נפוצות לפונקציות עזר לבדיקת ערך null (com.google.common.base.Preconditions.checkNotNullו-dagger.internal.Preconditions.checkNotNull*) בבדיקות תמציתיות של ערך null בבייטקוד. השמטנו אתObjects.requireNonNullמהקובץ הזה בכוונה, כדי לשמור על הודעות חריגות מפורשות בגבולות של ממשקי API של מסגרות. -
build/make/core/proguard/enumvalues.flags: שומר את השיטותvaluesו-valueOfבסוגיenum, אלא אם הן מושבתות על ידי הגדרת ה-build. - כללים שנוצרו אוטומטית על ידי AAPT2: כלי AAPT2 בודק קובצי XML של פריסות, קובצי XML של העדפות וקובצי
AndroidManifest.xmlשמוזגו כדי ליצור כללי שמירה מדויקים לכלActivity,Service,BroadcastReceiver,ContentProvider,BackupAgent,Application, מחלקת משנה מותאמת אישית שלView, מחלקת משנה שלPreferenceושיטת XML שלandroid:onClickשרשומים.
ביקורת והעברה של כללי שמירה קיימים
כשבודקים קובצי proguard.flags או keep.xml קיימים במאגר של פלטפורמה, צריך להעריך כל כלל לפי הסדר בהיררכיה הבאה בת ארבעת השלבים:
שלב 1: מוחקים כללים מיותרים או לא רלוונטיים
מומלץ למחוק כללים שכבר נכללים בקווי בסיס גלובליים, במניפסט של AAPT2 ובכללי הפריסה, או בהגדרות ברירת המחדל של R8, וגם כללים שמפנים למחלקות או לחבילות שכבר לא קיימות.
אם כל הכללים בקובץ .flags מיותרים, מוחקים את הקובץ ומסירים את proguard_flags_files מ-Android.bp. אם הבלוק optimize שנותר מכיל רק את הגדרות ברירת המחדל, מסירים את הבלוק המיותר ומעצבים את קובץ ה-build באמצעות bpfmt -w Android.bp. כשמנקים חבילה, בודקים אם מודולים נלווים בספריות משנה (כמו וריאציות של יעד Kotlin) מפנים לאותו קובץ דגלים ומעדכנים אותם יחד.
שלב 2: העברת כללי שמירה מבוססי-בדיקה
כדי להעביר כללי שמירה מבוססי-בדיקה מקובצי .flags ייצור מותאמים אישית, אפשר להשתמש באחת מהתבניות הבאות:
קישור ספריית ההטמעה לבדיקות (
static_libs): כש-android_testמפעיל פרטים של הטמעה פנימית או פרטים שמוגבלים לחבילה של אפליקציה, ארכיטקטורת הפלטפורמה המומלצת היא למקם את קובצי המקור של האפליקציה ב-android_library(MyApp.impl) ולקשר באופן סטטי אתMyApp.implגם ל-MyAppוגם ל-MyAppTests:android_library { name: "MyApp.impl", srcs: ["src/**/*.java"], manifest: "AndroidManifest.xml", } android_app { name: "MyApp", static_libs: ["MyApp.impl"], optimize: { obfuscate: true, shrink_resources: true, }, } android_test { name: "MyAppTests", srcs: ["tests/**/*.java"], static_libs: ["MyApp.impl"], }התבנית הזו עם 3 המודולים מאפשרת ל-
MyAppTestsלפעול כבדיקה עם מכשור עצמי עם גישה מלאה למחלקות פנימיות, מאפשרת ל-MyAppלהפעיל אתobfuscate: trueבאופן חופשי בלי לשבור בדיקות, מונעת משליחת נקודות כניסה לבדיקה בלבד ב-APK של ההפקה ומבטלת את הצורך בבנייה מחדש שלMyAppכשקוד הבדיקה משתנה.הוספת הערה לנקודות בדיקה באמצעות
@VisibleForTesting: אם קובץ APK חיצוני לבדיקה קורא למספר קטן של שיטות או בנאים פנימיים ביעדandroid_app, ושינוי המבנה לספריית.implלא מעשי, מוסיפים הערה להצהרות האלה באמצעות@VisibleForTesting. הבסיס הגלובליbuild/make/core/proguard.flagsשומר באופן אוטומטי על@VisibleForTestingפריטים בחבילותandroid.**, com.android.**ו-com.google.android.**, בלי קובצי.flagsבהתאמה אישית. (למודולים של ספקים מחוץ למרחבי השמות האלה, צריך להשתמש ב-@UsedByReflectionאו בכללים של ספרייה מיוצאת).שימוש ב-
trace_references_fromמעבר לגבולות של ספריות בפלטפורמה: אם אי אפשר לקשר באופן סטטי את הטמעת היעד (לדוגמה, בדיקות של שירותי שרת מערכת או קובצי JAR של פלטפורמה כמוframework-connectivity), צריך להגדיר אתtrace_references_fromבמודול היעד כך שיצביע על מודול נלווה של ספריית Java שמכיל את מקורות הבדיקה. R8 עוקב אחרי כל המחלקות והחברים שאליהם יש הפניה בבייטקוד של קובץ ה-companion ושומר אותם.
שלב 3: ייצוא כללים מהספרייה שבבעלותכם
אם ספרייה משותפת java_library או android_library דורשת כללי שמירה בשביל ההשתקפות הפנימית שלה או קריאות חוזרות של JNI, צריך להגדיר את הכללים ביעד הספרייה ולהגדיר את export_proguard_flags_files: true:
java_library {
name: "my-shared-library",
srcs: ["src/**/*.java"],
optimize: {
proguard_flags_files: ["proguard.flags"],
export_proguard_flags_files: true,
},
}
כל יעדי הספרייה וה-android_app במורד הזרם שמקושרים באופן סטטי ל-my-shared-library מקבלים את הכללים האלה בירושה באופן אוטומטי, כך שלא צריך לשכפל אותם באפליקציות במורד הזרם. אם יש כמה מודולים בספריות שונות שצריכים לחלוק קבוצת כללים שלא קשורה לספריית קוד יחידה, צריך לפרסם את קובץ .flags באמצעות filegroup מפורש ב-Android.bp כדי שהמודולים יוכלו להפנות ל-:my-shared-flags בצורה נקייה בגבולות החבילה. הנחיות כלליות לגבי יצירת כללי שמירה של צרכנים בספריות בלי להגביל את האופטימיזציה של אפליקציות במורד הזרם זמינות במאמר אופטימיזציה למפתחי ספריות.
שלב 4: הוספת הערות להצהרות בקוד המקור
כשניגשים למחלקה, לשיטה, לקונסטרוקטור או לשדה באמצעות רפלקציה או JNI, והם לא מכוסים על ידי AAPT2 או קווי בסיס גלובליים, צריך להחליף כללי -keep
מנותקים בקובצי .flags בהערות מקור מתוך המדריך להערות Keep (ראו את הפניה ל-Javadoc keepanno):
-
@UsesReflection: מומלץ להשתמש בהערה הזו כשאתם שולטים בקוד שמבצע את ההשתקפות. ממקמים אותו באתר של קריאת הרפלקציה כדי להצהיר על מחלקות, מתודות או שדות יעד שאליהם מתבצעת גישה באופן דינמי. מכיוון ש-@UsesReflectionמקודד באופן אוטומטי תנאי מוקדם לכך שאפשר להגיע לאתר הקריאה עם ההערה, R8 מסיר גם את המתקשר וגם את יעד ההשתקפות אם אתר הקריאה לא נמצא בשימוש. -
@UsedByReflection: מיקום בכיתות, בשיטות, בשדות או בבוני אובייקטים שמופעלים או מופעלים באופן רפלקטיבי על ידי קוד או ספריות חיצוניים, כמו כיתות שנטענו באמצעותClass.forNameממפתחותBundleאוSettings, או כיתות של תוספים שנטענו באמצעות טועני כיתות דינמיים. מצייניםkind(כמוKeepItemKind.CLASS_AND_METHODS),preconditions(@KeepCondition) ואילוצים של פרמטרים כדי ש-R8 ישמור רק את החוזה הרפלקטיבי המדויק ועדיין יוכל לבצע אופטימיזציה או להסיר חברים לא בשימוש במחלקה. אל תוסיפו את@UsedByReflectionלרכיבים שרשומים במניפסט כמוJobServiceאוBroadcastReceiver, כי AAPT2 כבר שומר אותם. -
@UsedByNative: לשימוש בשיטות או בשדות שמתבצעת אליהם גישה מקוד JNI של C או C++ באמצעותGetMethodID,GetStaticMethodIDאוGetFieldID. -
@KeepForApi: מיקום במחלקות או בחברים של API בספרייה שצריכים להישאר ללא שינוי כשמצמצמים את הספרייה לפני ההפצה. -
@Keep(androidx.annotation.Keep): משמש כחלופה כשkeepannoלא רלוונטי. החלת@Keepעל כיתה שומרת את הכיתה ואת כל החברים בה ללא תנאי. הוספת@Keepלשיטה או לשדה פועלת כנקודת כניסה ללא תנאי ששומרת את החבר ואת המחלקה שמכילה אותו, גם אם המחלקה אף פעם לא מופעלת. לעומת זאת,keepannoמבטא נגישות מותנית.
כדי להשתמש בהערות keepanno של R8 במודול Soong:
הוספת
"keepanno-annotations"אלlibsב-Android.bp:libs: [ "keepanno-annotations", ],מייבאים את
com.android.tools.r8.keepanno.annotations.*ומבצעים הערות בהצהרה או באתר הקריאה בקוד המקור של Java או Kotlin:import com.android.tools.r8.keepanno.annotations.KeepItemKind; import com.android.tools.r8.keepanno.annotations.UsedByReflection; public final class CustomPluginController { @UsedByReflection( description = "Instantiated via Class.forName from plugin config", kind = KeepItemKind.CLASS_AND_METHODS) public CustomPluginController(Context context) { // ... } }R8 מתרגם הערות של
keepannoישירות למודל הפנימי של כללי השמירה, ומסיר את ההערות מהפלט הסופי של DEX, בלי להוסיף תקורה של בייטקוד בזמן הריצה.
איך להימנע מטעויות נפוצות
בקטעים הבאים מתוארות בעיות נפוצות בקוד של הפלטפורמה שקשורות ל-proguard.flags ול-keep.xml, ומוסבר איך לפתור אותן.
כללי שמירה רחבים של רכיבים
# Don't do this:
-keep class * extends android.app.Activity
-keep class * extends android.app.Service
-keep class * extends android.content.BroadcastReceiver
- למה זה פוגע בביצועים: הכלל הזה מיותר לחלוטין ב-AAPT2.
היא משתמשת בתו כללי בלי לבדוק אם הרכיב רשום ב-
AndroidManifest.xmlהממוזג הסופי, ולכן היא מכריחה את R8 לשמור כל מחלקת משנה שלActivity,ServiceאוBroadcastReceiverשנמצאת בכל מקום בנתיב המחלקה, כולל רכיבי ספרייה שלא נעשה בהם שימוש ופעילויות ניפוי באגים מושבתות. - התיקון המומלץ: מחיקת הכלל. AAPT2 בודק את המניפסט שעבר מיזוג ומפיק כללי
-keepמדויקים לרכיבים רשומים.
כללים לשמירה של רכיבי Click Handler רחבים ב-XML
# Don't do this:
-keepclassmembers class * {
public void *(android.view.View);
}
- למה זה פוגע בביצועים: שמירה של כל שיטת
public void *(View)בכל מחלקה במודול מונעת מ-R8 להסיר או להטמיע כל שיטה שמקבלת פרמטר יחיד שלView. - התיקון המומלץ: מחיקת הכלל. AAPT2 סורק קובצי XML של פריסות ויוצר כללי שמירה ממוקדים לשיטות שמפנות למאפייני
android:onClick.
כללי שמירה של getter ו-setter של תצוגה רחבה
# Don't do this:
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(...);
public *** get*();
}
- למה זה פוגע בביצועים: הכלל הזה מאלץ את R8 לשמור כל getter ו-setter בכל מחלקת משנה
Viewבאפליקציה ובכל הספריות המקושרות (כולל ספריות AndroidX ו-Material), וחוסם הסרה של שיטות לא פעילות והטמעה של קוד בתוך שורות בקוד של ממשק המשתמש. - התיקון המומלץ: מחיקת הכלל. AAPT2 כבר שומר את ה-constructors של מחלקות
Viewבהתאמה אישית שנוצרו מקובצי XML של פריסות. אם הקוד מנפיש מאפיין של תצוגה באמצעות שמות מחרוזות רפלקטיביים כמוObjectAnimator.ofFloat(view, "translationZ", ...), צריך להחליף את שם המחרוזת בהפניה למאפיין מוקלד (View.TRANSLATION_Zאו הטמעה מותאמת אישית שלFloatPropertyאוIntProperty) כדי להימנע לחלוטין מרפלקציה. אם אי אפשר להימנע מגישה רפלקטיבית למאפיין, צריך להוסיף את ההערה@UsedByReflectionלשיטת ה-getter או ה-setter הספציפית.
השבתה של אופטימיזציה או הסתרה גורפת
# Don't do this in proguard.flags:
-dontoptimize
-dontshrink
-dontobfuscate
- למה זה פוגע בביצועים: הצבת התגים
-dontoptimizeאו-dontshrinkבתוך קובץ.flagsמבטלת בשקט את ההגדרות של המודולAndroid.bpומשביתה את מעברי האופטימיזציה בכל היעד. גרוע מכך, אם ספרייה מייצאת קובץ.flagsשמכיל-dontoptimize, היא משביתה את האופטימיזציה של R8 לכלandroid_appבמורד הזרם שמקשר לספרייה. - התיקון המומלץ: צריך להסיר את
-dontoptimize,-dontshrinkו--dontobfuscateמקובצי.flags. שליטה בהתנהגות האופטימיזציה באופן מפורש ב-Android.bpבאמצעות הבלוקoptimize(shrink,optimize,obfuscate) ביעד העלה.
התרעות אבחון בכללים מחויבים
# Don't do this in proguard.flags:
-verbose
-printmapping proguard.map
-printusage usage.txt
-printconfiguration config.txt
- למה זה פוגע ב-build: דגלים של אבחון יוצרים עומס ביומני ה-build או מנסים לכתוב קובצי פלט לנתיבים מקומיים במהלך build של Soong בארגז חול.
- התיקון המומלץ: מחיקת הדגלים האלה מקובצי
.flagsשאושרו. מערכת Soong כותבת באופן אוטומטי מיפוי R8 ופלט שימוש, כמוproguard_dictionaryו-proguard_usage.zip, לספריית הביניים של המודול ב-out/soong/.intermediates/.
כללי שמירה בסביבת הייצור לקוד בדיקה
- למה זה פוגע בביצועים: הוספה של כללי
-keepמותאמים אישית רק לצורך גישה לבדיקה מאלצת את הקוד הזה להישאר לא מוצפן ולשמור אותו בגרסאות הייצור. כשמוסיפים הערות לשיטות באמצעות@VisibleForTestingאו כשמגדירים אתtrace_references_from, לא צריך לתחזק כללי-keepבאופן ידני, אבל הסמלים האלה עדיין נשלחים בקובץ הבינארי של הייצור. - התיקון המומלץ: כדי להוציא לחלוטין את נקודות הכניסה לבדיקה בלבד מ-APK של ייצור, צריך לבנות את האפליקציה בספריית
.implולקשר אותה ל-APK של בדיקה עם כלי מדידה עצמי באמצעותstatic_libs, כמו שמתואר בשלב 2: העברת כללי שמירה מבוססי-בדיקה.
Blanket resource wildcards in keep.xml
<!-- Don't do this in res/raw/keep.xml: -->
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@raw/*,@drawable/*,@string/*" />
- למה זה פוגע בביצועים: תווים כלליים גורפים שומרים כל משאב בסוג התאמה במודול ובתלות שלו, וכך מבטלים את צמצום המשאבים (
shrink_resources: true) ומנפחים את ה-APK ואת טבלת המיפויresources.arsc. - התיקון המומלץ:
- עדיף להשתמש בהפניות סטטיות למשאבים במקום ב-
keep.xml: אל תשתמשו בשיטהResources.getIdentifierכדי לחפש באופן דינמי קבוצה מוגבלת של משאבים, כמו מחרוזות ממוספרות של ניסויים או תמונות drawable עם נושא. במקום זאת, משתמשים בהצהרתswitchבזמן קומפילציה או ממפים מעל קבועים סטטיים שלR.id,R.stringאוR.drawable. הפניות סטטיות מאפשרות ל-R8 ול-AAPT2 לעקוב אחרי המקורות הפעילים המדויקים, לבטל את התקורה של חיפוש מחרוזות בזמן ריצה ולהסיר את הצורך ב-keep.xmlלחלוטין. - רשימה של שמות משאבים ספציפיים: אם נדרש חיפוש דינמי, למשל על ידי קורא רישיונות של צד שלישי, צריך לציין את מזהי המשאבים המדויקים ב-
tools:keep(לדוגמה,tools:keep="@raw/third_party_licenses"). - מחיקת קובצי
keep.xmlמיותרים: אם המשאבים שמופיעים ברשימה כבר מוזכרים באופן סטטי בקוד (R.raw.foo) או ב-XML (@raw/foo), הכלי למחיקת קוד מיותר ישמור אותם באופן אוטומטי, כך שתוכלו למחוק אתres/raw/keep.xml.
- עדיף להשתמש בהפניות סטטיות למשאבים במקום ב-
אימות וביקורת של שינויים בכללים
בכל פעם שמסירים או מצמצמים כללי שמירה ב-proguard.flags או ב-keep.xml, צריך לוודא שהמודול נוצר בצורה תקינה, עובר את בדיקות היחידה והמכשור ושומר על כל נקודות הכניסה הנדרשות.
פיתוח ובדיקה של מודולים
כדי לוודא ש-R8 ו-AAPT2 מסיימים את הפעולה בלי אזהרות על הפניות חסרות, צריך לבנות את המודול בצורה נקייה:
m <MODULE_NAME>מריצים בדיקות יחידה ובדיקות אינסטרומנטציה למודול באמצעות
atest:atest <TEST_MODULE_NAME>כדי לבדוק אפליקציות מערכת, שירותים עם הרשאות או מודולים שתלויים בחומרה, מריצים בדיקות של מכשור וממשק משתמש במכשירים פיזיים או במעבדת הבדיקות של המכשיר. הבדיקות האלה מאפשרות לבדוק את נתיבי ההשתקפות בזמן הריצה, את הקשרים של IPC ואת ניפוח המשאבים.
בדיקת ההבדלים בין קובצי DEX ומשאבים
משווים את ה-APK או ה-JAR אחרי ההידור לפני ואחרי שינויים בכללים כדי לוודא ש-R8 מסיר קוד ומשאבים לא פעילים בלי להסיר נקודות כניסה צפויות:
משתמשים ב-
apkanalyzerאו ב-dexdumpכדי לבדוק את המחלקות, השיטות והשדות שנשמרו בקובצי ה-APK או ה-DEX של הפלט:apkanalyzer dex packages $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apkכשמשנים את
keep.xmlאו מפעילים אתshrink_resources, משתמשים ב-aapt2 dump resourcesכדי לוודא שה-build מסיר משאבים לא בשימוש ושומר את המשאבים הנדרשים:aapt2 dump resources $OUT/system/priv-app/<APP_NAME>/<APP_NAME>.apk
ניתוח של רדיוס השמירה ושל כלל ההכלה באמצעות R8
הקומפיילר R8 בקוד פתוח כולל כלי ניתוח Keep Radius (שמוצג גם ב-Android Studio וב-Gradle ככלי הניתוח של הגדרות R8) שמודד את ההשפעה המדויקת של כל כלל keep במהלך הקומפילציה. מערכת Soong משלבת את הכלי הזה ישירות ב-build של פלטפורמת Android (ההגדרה מתבצעת ב-build/soong/java/dex.go).
כדי לנתח כללי שמירה של מודול פלטפורמה או של כלל הבנייה:
מריצים את ה-build עם משתנה הסביבה
R8_DUMP_KEEP_RADIUS=true:R8_DUMP_KEEP_RADIUS=true m <MODULE_NAME>כשהערך של
R8_DUMP_KEEP_RADIUS=trueמוגדר, Soong מורה ל-R8 לתעד מדדים של כללי שמירה בקובץr8keepradius.pbביניים לכל מודול שעבר קומפילציה ב-out/soong/.intermediates/. לכל כלל שמירה (keep rule) והערהkeepanno, R8 מתעד:- רדיוס שמירה מיידי: המחלקות, השדות והשיטות המדויקים שנשמרים על ידי הכלל, יחד עם האילוצים הספציפיים שהוא אוכף כדי למנוע כיווץ, אופטימיזציה או הסתרה.
- הכללה של כלל אחד בתוך כלל אחר: אילו כללי שמירה או הערות אחרים כבר שומרים את אותם פריטים. אם כלל מותאם אישית נכלל באופן מלא בכלל של AAPT2 או בכלל בסיסי גלובלי, אפשר למחוק אותו בבטחה.
- כללים גלובליים וכללים שחלים על כל החבילה: כללים שמשתמשים בתווים כלליים רחבים שחלים על כל החבילה או שמחילים הנחיות גלובליות להגדרה.
תמיר את הפלט
r8keepradius.pbלדוח HTML אינטראקטיבי באמצעותKeepRadiusHtmlReportGenerator(כלול ב-prebuilts/r8/r8.jar):# Generate an HTML report for a single module INTERMEDIATES=out/soong/.intermediates/packages/apps/<APP_NAME>/<APP_NAME> java -cp prebuilts/r8/r8.jar \ com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \ $INTERMEDIATES/android_common/r8keepradius.pb \ keep_radius_report.html # Scan all built modules under out/soong/.intermediates and generate # per-module HTML reports plus an aggregate keepradius.html summary java -cp prebuilts/r8/r8.jar \ com.android.tools.r8.keepradius.KeepRadiusHtmlReportGenerator \ out/soong/.intermediates \ out/keep_radius_reportsכשמציינים ספרייה,
KeepRadiusHtmlReportGeneratorסורק את כל*keepradius*.pbהקבצים, יוצר דוח HTML לכל מודול ו יוצרout/keep_radius_reports/keepradius.htmlשמסכם את מספר הפריטים הפעילים , מספר הפריטים שנשמרו ואת כללי השמירה עם הרדיוס הגבוה ביותר בכל הבנייה. למידע נוסף על פירוש הציונים של כיווץ, אופטימיזציה והסתרה בדוח שנוצר, אפשר לעיין במאמר בנושא שימוש בכלי לניתוח ההגדרות של R8.
מערכת Soong תומכת גם בשני משתני סביבה נוספים של אבחון R8 ב-build/soong/java/dex.go:
-
R8_DUMP_INPUT=true: כותב אתr8inputs.zipלספריית הביניים של המודול שמכילה את כל קובצי ה-JAR של הקלט, קובצי ה-JAR של הספרייה והגדרות ProGuard שמוזגו לשחזור עצמאי של R8. -
R8_DUMP_PERFETTO_TRACE=true: כותבת אתr8trace.ptraceלספריית הביניים של המודול כדי לבדוק את שלבי ההידור של R8 ב-Perfetto.
אימות כללים של צרכני ספריות
כשמייצאים כללים של java_library או android_library לצרכנים במורד הזרם (export_proguard_flags_files: true), הכללים האלה לא יכולים לכלול דגלים גלובליים שמשנים את האופטימיזציה של האפליקציה הצרכנית או משביתים אותה.
R8 מספק כלי לניתוח כללי שמירה בקוד פתוח (ProcessKeepRules), שמוצג בגרסת ה-build של פלטפורמת Android ככלי המארח process-keep-rules (מוגדר ב-prebuilts/r8/Android.bp):
# Build the R8 keep rules validator host binary
m process-keep-rules
# Validate one or more ProGuard configuration files
out/host/linux-x86/bin/process-keep-rules <PROGUARD_FLAGS_PATH>
הכלי process-keep-rules מנתח כל קובץ תצורה, ואם הוא נתקל בהוראות שלא מותרות בכללים של צרכני ספריות, הוא נכשל ומציג אבחון של הקובץ והשורה. ההנחיות האסורות כוללות אופטימיזציה גלובלית, כיווץ, השבתת הסוואה (כמו -dontoptimize או -dontshrink), אריזה מחדש של חבילות ודגלים של שינוי גישה, דגלים של אבחון או מיפוי ו--keepattributes ברמת האפליקציה.
ביקורת על עצי מקור באמצעות pgaudit.py
כדי לבצע ביקורת בספריות קובצי המקור שלא נבנו לצד כלי הניתוח של R8 בזמן הבנייה, עץ פלטפורמת Android כולל את הסקריפט pgaudit.py בתיקייה build/make/core/proguard/tools/. כלי הניתוח הסטטי הזה סורק קבצים מסוג Android.bp,
proguard.flags ו-keep.xml במאגר כדי לסמן כללים שכבר מכוסים על ידי AAPT2 או קווי בסיס של פלטפורמות גלובליות, תווים כלליים רחבים, -dontoptimizeהגדרות ברירת מחדל, וכללי שמירה שרלוונטיים רק לבדיקות:
# Audit a single package directory
./build/make/core/proguard/tools/pgaudit.py packages/apps/Provision/
# Scan a subsystem tree and write a structured JSON report
./build/make/core/proguard/tools/pgaudit.py packages/ --json=audit_report.json
צוותי פלטפורמה ויצרני ציוד מקורי יכולים לשלב את pgaudit.py כדי לבצע טריאז' מהיר של עץ המקור, את process-keep-rules כדי לאמת כללים של ספריות מיוצאות ואת R8_DUMP_KEEP_RADIUS=true עם KeepRadiusHtmlReportGenerator כדי למדוד את רדיוס השמירה המדויק של כלל, שדה ושיטה בכל כלל ב-build.