מערכת ה-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במהלך ההידור בזמן ה-build או במכשיר. - פחות עומס על הזיכרון בזמן הריצה: מכיוון ש-Android ממפה את
.odex,.vdexוטבלאות המשאבים של ה-APK לזיכרון התהליך באמצעות קריאותmmapלדפים לפי דרישה, קובצי בינארי קטנים יותר מפחיתים את השגיאות החמורות בדפים במהלך הפעלת האפליקציה ומקטינים את הקוד התושב ואת זיכרון המשאבים בתהליכים. במאמר קוד האפליקציה הוא זיכרון מוסבר איך קוד האפליקציה המקומפל משפיע על זיכרון המכשיר. - אופטימיזציה יעילה יותר של כל התוכנית: אילוצי שמירה צרים מאפשרים ל-R8 להטמיע שיטות, לבטל וירטואליזציה של ממשקים וקריאות וירטואליות, להסיר שדות שלא בשימוש ולהפיץ קבועים בין מחלקות.
הגדרת אופטימיזציה של R8 ב-Soong
בקובצי Android.bp, מגדירים את האופטימיזציה של R8 באמצעות בלוק המאפיינים optimize ביעדי 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 |
ההגדרה קובעת אם להפעיל את התכונה tree shaking כדי להסיר מחלקות, שדות ושיטות שלא ניתן להגיע אליהם. ערך ברירת המחדל הוא true ליעדים של android_app (false למודולים עצמאיים של java_library ולמודולים של בדיקה, שמעבירים את -dontshrink אלא אם הוגדר אחרת).
|
optimize |
שולט באופטימיזציות של קוד בייט, כמו הטמעת מתודות, מיזוג מחלקות, הפצת קבועים והסרת ענפים לא פעילים. ברירת המחדל היא true ליעדי android_app (נשלטת על ידי הדגל RELEASE_R8_OPTIMIZE_BY_DEFAULT של גרסת ה-build להפצה).
|
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 בגרסאות build רגילות של פלטפורמות).
|
proguard_flags_files |
מציג קובצי .flags או .pro ספציפיים למודול שמכילים כללי שמירה בהתאמה אישית. הימנעו מהוספת קבצים בהתאמה אישית אם מספיק להשתמש בהערות רגילות או בברירות מחדל.
|
export_proguard_flags_files |
הפצה של proguard_flags_files של הספרייה הזו למודולים במורד הזרם שתלויים בה באופן סטטי.
|
trace_references_from |
מציג יעדים תלויים ישירים של הידור DEX (מחוברים דרך instrumentation_for או libs) שההפניות של קוד הבייט שלהם ליעד הזה R8 עוקב אחריהן ושומר אותן באופן אוטומטי (מופץ גם בין וריאנטים של override_android_app ו-override_android_test, ונתמך בספריות Java נלוות לא תלויות בשילוב עם פיצול .impl).
|
allow_traced_references |
מאפשר ליעדים תלויים של הידור DEX ישיר (שמחוברים דרך instrumentation_for או libs, כמו בדיקות של ספקים או בדיקות בין מאגרים שלא ניתן לציין ב-trace_references_from) להצטרף למעקב אחר הפניות ה-bytecode שלהם ליעד הזה על ידי הגדרת trace_references: true.
|
trace_references |
כשהערך מוגדר ל-true ביעד עם allow_traced_references: true שתלוי ישירות בהידור DEX, מריץ את R8 tracereferences במודול הזה ומעביר את כללי השמירה שנוצרו למודול היעד.
|
protect_api_surface |
העברת --protect-api-surface אל R8. כשהוא מוגדר ב-java_sdk_library, הוא גם יוצר ומצרף באופן אוטומטי כללי שמירה של Metalava API בכל היקפי ה-API המופעלים (ציבורי, מערכת, module-lib, שרת מערכת ובדיקה) לספריית ההטמעה.
|
ספריות סטטיות (android_library, java_library וייבוא של AAR) יכולות גם להגדיר את required_downstream_optimization: { enabled: true, optimize: true } ב-Android.bp כדי לדרוש מכל יעד של הידור DEX במורד הזרם שמקשר אותן באופן סטטי (באופן ישיר או טרנזיטיבי) להפעיל את כיווץ R8 ואת אופטימיזציית קוד הבייט. Soong מפעיל את זה באופן אוטומטי ב-androidx.compose.runtime_runtime, כך שיעדי הקישור של Jetpack Compose עוברים אופטימיזציה באמצעות 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בספריית הביניים של המודול, מקבץ את כל המילונים של המודול לקובץ ה-buildproguard-dict.zip, מטמיע גיבוב (hash) של מיפוי (--map-id-template) בכותרת ה-DEX ומשכתב את המאפיינים (--source-file-template) של המחלקהSourceFileכדי ש-retraceיוכל להמיר באופן אוטומטי את דוחות הקריסה לסמלים.
צריך לשמור את obfuscate: false (או לשמר באופן מפורש שמות של ממשקי API ציבוריים) רק בשביל:
- ספריות משותפות ב-bootclasspath או ב-classpath של
system_server: מודולים (java_libraryאוjava_sdk_library) שחושפים ממשק API חיצוני שמקושר באופן דינמי על ידי מודולים אחרים בזמן הריצה (כמו יעדים שלframework.jar,services.jarאו<uses-library>) צריכים לשמור עלobfuscate: falseאו לשמר באופן מפורש את ממשק ה-API שלהם באמצעות כללים שלprotect_api_surface: true,@KeepForApiאו-keep, כדי שרכיבי ה-caller שעברו הידור מול ה-stubs שלהם יוכלו לזהות את שמות המחלקות והאיברים בזמן הריצה. במודולים שלjava_sdk_library, הגדרתprotect_api_surface: trueיוצרת באופן אוטומטי כללי שמירה של Metalava ProGuard ומצרפת אותם לספריית ההטמעה בכל היקפי ה-API המופעלים (ציבורי, מערכת, module-lib, שרת מערכת ובדיקה). כשאופטימיזציית R8 כללית מופעלת ביעדי bootclasspath שלא ניתנים לעדכון (framework-minus-apex,extו-framework-location), הם כוללים גם אתframework-hiddenapi-proguard-rules(שנוצר על ידיhiddenapi_to_proguard) כדי לשמור את החברים שרשומים בקובציboot/hiddenapi/*.txtפעילים (השמטתhiddenapi-max-target-o.txt).
לפני שמפעילים את obfuscate: true באפליקציה, צריך לוודא שהתנאים המוקדמים הבאים להעברה מתקיימים:
- תלויות של APK לבדיקה חיצונית: אם קובץ APK חיצוני של
android_testמגדיר אתinstrumentation_for: "MyApp"ומפעיל ישירות מחלקות או שיטות פנימיות שלMyApp, שינוי השם של הסמלים האלה גורם ל-NoSuchMethodErrorאו ל-NoClassDefFoundErrorבזמן הרצת הבדיקה. מגדירים אתtrace_references_from(או אתallow_traced_referencesעםtrace_references: true), יוצרים את הבדיקה כ-APK עם אינסטרומנטציה עצמית שמקושר באופן סטטי ל-MyApp.impl, או מוסיפים הערות ל-test hooks באמצעות@VisibleForTesting(ראו שלב 2: העברת כללי שמירה מבוססי-בדיקה) כדי שהסמלים הפנימיים שנדרשים לבדיקות יישמרו בזמן ערפול הקוד של שאר האפליקציה. - רפלקציה מבוססת-מחרוזת ו-JNI: אם מודול מחפש מחלקות, מתודות או שדות לפי שמות מילוליים של מחרוזות (
Class.forName,getDeclaredMethodאו JNIFindClassו-GetMethodID) בלי כללי שמירה או הערות, ערפול הקוד משנה את השמות של היעדים האלה ומשבש את החיפושים בזמן הריצה. (שימו לב שרפלקציה ללא הערות נכשלת גם ב-shrink: trueוב-optimize: true). הוסיפו הערות לנקודות הכניסה האלה באמצעות המדריך להוספת הערות Keep (@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ושדות של protobufMessageLite. -
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 בודק קובצי
AndroidManifest.xml, פריסת XML והעדפות XML ממוזגים כדי ליצור כללי שמירה מדויקים לכלActivity,Service,BroadcastReceiver,ContentProvider,BackupAgent,Application, מחלקת משנה מותאמת אישית שלView, מחלקת משנה שלPreferenceושיטתandroid:onClickשל XML רשומים.
בדיקה והעברה של כללי שמירה קיימים
כשבודקים קובצי proguard.flags או keep.xml קיימים במאגר של פלטפורמה, צריך להעריך כל כלל לפי הסדר, בהתאם להיררכיה הבאה בת ארבעת השלבים:
שלב 1: מוחקים כללים מיותרים או מיושנים
מוחקים כללים שכבר נכללים בקווי הבסיס הגלובליים, במניפסט של AAPT2, בכללי הפריסה או בהגדרות ברירת המחדל של R8, וגם כללים שמפנים למחלקות או לחבילות שכבר לא קיימות.
אם כל הכללים בקובץ .flags מיותרים, צריך למחוק את הקובץ ולהסיר את proguard_flags_files מ-Android.bp. אם הבלוק optimize שנותר רק חוזר על הגדרות ברירת המחדל, צריך להסיר את הבלוק המיותר ולעצב את קובץ ה-build באמצעות bpfmt -w Android.bp. כשמנקים חבילה, צריך לבדוק אם מודולים נלווים בספריות משנה (כמו וריאנטים של יעדי Kotlin) מפנים לאותו קובץ דגלים, ולעדכן אותם ביחד.
שלב 2: העברת כללי שמירה מבוססי-בדיקה
מעבירים את כללי השמירה שמבוססים על בדיקות מחוץ לקובצי .flags מותאמים אישית בסביבת הייצור, באמצעות אחת מהתבניות הבאות:
מעקב אחרי הפניות מיעדי בדיקה או מיעדים נלווים (
trace_references_fromאוallow_traced_references): כשגם היעד וגם מודול תלוי ישיר מקמפלים את ה-DEX שלהם (android_app,android_testאוjava_libraryשעבר dexing) ומחוברים דרךinstrumentation_forאוlibs, מערכת Soong יכולה להריץ את R8tracereferencesבמודול התלוי ולהזין את כללי השמירה שנוצרו לקומפילציית R8 של היעד בלי ליצור מחזור תלות או לדרוש פיצול ידני של ספריית.impl:בדיקות באותו מאגר (
trace_references_from): מפרטים את הרכיבים הישירים שלinstrumentation_forאוlibsשתלויים במודול היעד (הפעולה הזו גם מופצת באופן אוטומטי בין וריאנטים שלoverride_android_appו-override_android_test, וממשיכה לתמוך בספריות Java נלוות שלא תלויות במודול כשהן משויכות לפיצול.impl):android_app { name: "MyApp", srcs: ["src/**/*.java"], optimize: { obfuscate: true, shrink_resources: true, trace_references_from: ["MyAppTests"], }, } android_test { name: "MyAppTests", instrumentation_for: "MyApp", srcs: ["tests/**/*.java"], }בדיקות של ספקים או של מאגרים שונים (
allow_traced_referencesו-trace_references): כשמודול היעד לא יכול להציג את הרכיב התלוי ישירות ב-trace_references_from(למשל בדיקות במאגר אחר או בעץ ספקים אחר), מגדירים אתallow_traced_references: trueבמודול היעד ומצטרפים מכל רכיב תלוי באמצעותtrace_references: true:android_app { name: "MyApp", srcs: ["src/**/*.java"], optimize: { allow_traced_references: true, }, } android_test { name: "MyAppVendorTests", instrumentation_for: "MyApp", srcs: ["tests/**/*.java"], optimize: { trace_references: true, }, }רפלקציה מבוססת-מחרוזת בבדיקות:
tracereferencesבודק הפניות למאגר קבוע של קוד בייטים, ולא יכול לזהות רפלקציה מבוססת-מחרוזת בבדיקות (Class.forName,getDeclaredField, getDeclaredMethodאו רפלקציה של עוזרי הזרקת שדות). הוסיפו הערות לשדות ולשיטות של סביבת הייצור שאליהם ניגשים באמצעות רפלקציה, באמצעות@VisibleForTesting(או@UsedByReflection).
קישור ספריית ההטמעה לבדיקות (
static_libs): אם אתם רוצים שאף נקודת כניסה לבדיקה בלבד לא תישמר ב-APK בסביבת הייצור, ואתם לא רוצים ששינויים בבדיקה יפעילו מחדש את R8 ביעד בסביבת הייצור, הציבו את קובצי המקור של האפליקציה ב-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ולטשטש אותו רק מול נקודות כניסה בסביבת הייצור.הוספת הערות ל-test hooks באמצעות
@VisibleForTesting: אם קובץ APK חיצוני לבדיקה קורא רק לכמה שיטות פנימיות או בנאים ביעדandroid_app, אפשר להוסיף הערות להצהרות האלה באמצעות@VisibleForTesting. ההגדרה הגלובלית שלbuild/make/core/proguard.flagsשומרת באופן אוטומטי@VisibleForTestingפריטים בחבילותandroid.**,com.android.**ו-com.google.android.**בלי קובצי.flagsמותאמים אישית. (למודולים של ספקים מחוץ למרחבי השמות האלה, צריך להשתמש ב-trace_references_from, ב-@UsedByReflectionאו בכללי ספרייה מיוצאים). כשמוסיפים הערות באופן ידני באמצעות@VisibleForTestingבמקום להשתמש ב-trace_references_fromאו ב-trace_references, צריך להוסיף הערות גם למחלקות שמועברות ל-Mockitomock(...)או ל-spy(...)(לא רק ל-stubbed methods), כיmockito-target(dexmaker-mockito) במכשיר יוצר מחלקות משנה בזמן ריצה, ו-R8 יכול לסמן מחלקות ללא מחלקות משנה בתורfinal(MockitoException: Can't mock/spy class ... - final class).הימנעו מספריות סטטיות חופפות או ארזו אותן מחדש (
jarjar_rules): כשגםandroid_appוגם חבילת ה-APK לבדיקהinstrumentation_forמקשרים באופן סטטי לאותה ספרייה (באופן ישיר, כמוguava, או באופן זמני),tracereferencesרואה את המחלקות האלה שמוגדרות בבדיקה ולא פולט כללי שמירה לאפליקציה. בזמן הריצה, ה-classloader ההורה של האפליקציה מסתיר את העותק של הבדיקה באמצעות המחלקות המכווצות של R8, מה שגורם ל-IllegalAccessErrorאו ל-NoSuchMethodError. אם הבדיקה לא צריכה עותק משלה, צריך להסיר את העותק הכפול מה-static_libsשל הבדיקה (או לקשר אותו דרךlibs) כדי למנוע את ההתנגשות. אחרת, צריך לארוז מחדש את חבילות הספרייה הסטטיות החופפות של האפליקציה תחת קידומת ספציפית למודול באמצעותjarjar_rules.
שלב 3: מייצאים את הכללים מהספרייה שבבעלותכם
אם קובץ java_library או android_library משותף דורש כללי שמירה עבור רפלקציה פנימית משלו או קריאות חוזרות (callbacks) של 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: הוספת הערות להצהרות בקוד המקור
כשניגשים למחלקה, לשיטה, ל-constructor או לשדה באמצעות השתקפות או JNI, והם לא נכללים ב-AAPT2 או בקווי בסיס גלובליים, מחליפים את כללי -keep המנותקים בקובצי .flags בהערות מקור מהמדריך לשמירת הערות (אפשר לעיין בהפניה ל-Javadoc של keepanno):
-
@UsesReflection: עדיף להשתמש בהערה הזו כשאתם שולטים בקוד שמבצע את הרפלקציה. צריך להציב אותו באתר הקריאה לרפלקציה כדי להצהיר לאילו מחלקות, מתודות או שדות יעד מתבצעת גישה באופן דינמי. מכיוון ש-@UsesReflectionמקודד באופן אוטומטי תנאי מוקדם לכך שאתר הקריאה עם ההערה עצמו נגיש, R8 מסיר גם את המתקשר וגם את יעד ההשתקפות אם אתר הקריאה לא בשימוש. -
@UsedByReflection: מציבים במחלקות, בשיטות, בשדות או בבנאים שנוצר מהם מופע או שהם מופעלים באופן רפלקטיבי על ידי קוד או ספריות חיצוניים, כמו מחלקות שנטענות באמצעותClass.forNameממפתחותBundleאוSettings, או מחלקות של פלאגינים שנטענות באמצעות טועני מחלקות דינמיים. ציינו אתkind(למשלKeepItemKind.CLASS_AND_METHODS), אתpreconditions(@KeepCondition) ואת מגבלות הפרמטרים כדי ש-R8 ישמור רק את החוזה הרפלקטיבי המדויק ועדיין יוכל לבצע אופטימיזציה או להסיר איברים לא בשימוש של המחלקה. אל תוסיפו את@UsedByReflectionלרכיבים שרשומים במניפסט, כמוJobServiceאוBroadcastReceiver, כי AAPT2 כבר שומר אותם. -
@UsedByNative: צריך להוסיף ל-methods או לשדות שניגשים אליהם מקוד JNI ב-C או ב-C++ באמצעותGetMethodID,GetStaticMethodIDאוGetFieldID. -
@KeepForApi: צריך להוסיף למחלקות או לאיברים של API של ספרייה שחייבים להישאר ללא שינוי כשמכווצים את הספרייה עצמה לפני ההפצה. -
@Keep(androidx.annotation.Keep): משמש כחלופה כשאי אפשר להשתמש ב-keepanno. החלה של@Keepעל מחלקה שומרת את המחלקה ואת כל הגורמים שלה ללא תנאי. החלת@Keepעל שיטה או שדה פועלת כנקודת כניסה ללא תנאי ששומרת על האיבר ועל המחלקה שמכילה אותו, גם אם המחלקה אף פעם לא נוצרת, ואילוkeepannoמבטא נגישות מותנית.
כדי להשתמש בהערות R8 keepanno במודול 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מפנים אליהן.
כללי שמירה של Broad View 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) כדי להימנע לחלוטין מרפלקציה. אם אי אפשר להימנע מגישה רפלקטיבית למאפיינים, צריך להוסיף הערה ל-getter או ל-setter הספציפיים באמצעות@UsedByReflection.
השבתה של אופטימיזציה או ערפול קוד (obfuscation) גורפים
# 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שבוצע להם commit. Soong כותב באופן אוטומטי את פלטי המיפוי והשימוש של R8, כמוproguard_dictionaryו-proguard_usage.zip, לספריית הביניים של המודול ב-out/soong/.intermediates/.
כללי שמירה בסביבת הייצור לקוד בדיקה
- למה זה פוגע בביצועים: הוספת כללי
-keepבהתאמה אישית רק בשביל גישה לבדיקות מאלצת את הקוד להישאר לא מעורפל ולהישמר בגרסאות ה-build בסביבת הייצור. אפשר להגדיר אתtrace_references_from(או אתallow_traced_referencesעםtrace_references: true) או להוסיף הערות לשיטות באמצעות@VisibleForTestingכדי להימנע מתחזוקה ידנית של כללי-keepולשמור רק את מה שהבדיקות מפנות אליו, אבל הסמלים האלה עדיין נשלחים בקובץ הבינארי בסביבת הייצור. - התיקון המומלץ: כדי להסיר כללי
-keepלבדיקות בהתאמה אישית, צריך להפעיל אתtrace_references_from(או אתallow_traced_referencesו-trace_references: true). לחלופין, אפשר להגדיר את האפליקציה כספריית.implשמקושרת ל-APK לבדיקת אינסטרומנטציה עצמית באמצעותstatic_libs, כדי להשאיר את נקודות הכניסה לבדיקה בלבד מחוץ ל-APK לסביבת הייצור, כפי שמתואר בשלב 2: העברת כללי keep מבוססי-בדיקה.
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כדי לחפש באופן דינמי קבוצה מוגבלת של משאבים, כמו מחרוזות ניסוי ממוספרות או פריטים גרפיים עם עיצוב. במקום זאת, השתמשו בהצהרת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כשמריצים את
prebuilts/r8/r8.jarישירות ב-classes.jarשל מודול לפני R8 כדי לאמת שינויים בדגלים אופליין בלי build מלא של הפלטפורמה, צריך להעביר גם את קובצי ה-JAR של classpath--libשל המודול (android.jar,framework.jarו-libsהמשותף) וגם את כלליproguard.optionsשנוצרו על ידי AAPT2 מ-AndroidManifest.xmlומקובצי ה-XML של הפריסה. בלי הקלט הזה, R8 לא יכול לזהות את סוגי העל של ה-framework (Activity, Service,Phone, Handler) והוא לא שומר מספיק שיטות היררכיה.
ניתוח של רדיוס השמירה והכללת הכללים באמצעות R8
הקומפיילר R8 בקוד פתוח כולל מנתח Keep Radius (שנחשף גם ב-Android Studio וב-Gradle בתור R8 Configuration Analyzer) שמודד את ההשפעה המדויקת של כל כלל שמירה במהלך ההידור. 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/. לכל כלל שמירה והערת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שמסכם את מספר הפריטים הפעילים, מספר הפריטים שנשמרו וכללי השמירה עם הרדיוס הגבוה ביותר ב-build. מידע נוסף על פירוש הציונים של הכיווץ, האופטימיזציה וההסתרה בדוח שנוצר זמין במאמר שימוש ב-R8 Configuration Analyzer.
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. -
SOONG_SKIP_TRACE_REFERENCES=true: דילוג על הפעלת כלליtracereferencesשהוגדרו על ידיtrace_references_fromאוtrace_referencesכדי להאיץ את ה-builds של הלולאה הפנימית המקומית כשמבצעים איטרציה על קוד הבדיקה.
אימות כללי הצרכנים של הספרייה
כש-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
כדי לבדוק ספריות קובצי מקור שלא נבנו, לצד מנתחי זמן ה-build של 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
צוותי פלטפורמה ו-OEM יכולים לשלב את pgaudit.py כדי לבצע מיון מהיר של עץ המקור, את process-keep-rules כדי לאמת כללים של ספרייה שיוצאה, ואת R8_DUMP_KEEP_RADIUS=true עם KeepRadiusHtmlReportGenerator כדי למדוד את רדיוס השמירה המדויק של המחלקה, השדה והשיטה של כל כלל ב-build.