מהנדס ביצועים ב-Android
אפליקציות ל-Android נשלחות לעיתים קרובות בלי אופטימיזציה מספקת בגלל כללי שמירה שמרניים מדי, בדרך כלל מספריות של צד שלישי. כתוצאה מכך, קובצי ה-APK מנופחים, ההפעלות הראשוניות איטיות יותר וביצועי זמן הריצה נפגעים.
ב-Google Play יש ערכי סף חדשים לביצועים של אופטימיזציית DEX, ולכן החשיפה של האפליקציה תלויה ישירות בשמירה על רמת אופטימיזציה גבוהה.
בהרצאה הזו נציג תהליך עבודה מודרני של R8 כדי להתמודד עם ריבוי כללים. בסדנה תלמדו:
תמליל
שלום לכולם,
שמי בריאנה, ואני מצוות Android DevRel.
ואני כריס קרייק, אני בצוות הביצועים של Android.
ברוכים הבאים לכללי שמירה למניעת שגיאות הקלדה ולאופטימיזציה של DEX.
היום נדבר על מהי אופטימיזציה של DEX, למה כדאי לכם להתעניין בה, וגם על כמה עדכונים מעניינים מהזמן האחרון.
במשך זמן רב, אופטימיזציה של DEX נחשבה למשהו שפשוט נחמד שיש.
אז אולי הגדרתם את זה פעם אחת ואז פשוט השארתם את זה כמו שזה, או שאולי התפניתם לזה כשהיה לכם קצת זמן פנוי.
עם זאת, תעשיית המכשירים הניידים מתמודדת עם מגבלות משמעותיות באספקת החומרה שמשנות את הזמינות של זיכרון המכשיר. לכן, קובץ DEX שעבר אופטימיזציה חיוני כדי שהאפליקציה תשתמש בפחות זיכרון, תופעל מהר יותר, תצמצם את מקרי ה-ANR ותשפר את העיבוד ואת הביצועים בזמן הריצה באופן כללי.
כדי לשמור על סביבה עסקית תקינה של Android ולוודא שהאפליקציות שלכם ממשיכות לספק חוויות פרימיום, Google Play הודיעה על הדרישה החדשה לאיכות האפליקציה עם ערכי סף לביצועים שייכנסו לתוקף בפברואר.
זה כולל ערכי סף לאופטימיזציה של קוד DEX.
האפליקציות צריכות להגיע ליותר מ-25% בכיווץ, באופטימיזציה ובערפול קוד.
אז מה זה אומר מבחינתכם כמפתחים ל-Android?
קוד DEX שלא עבר אופטימיזציה לא רק פוגע בחוויית המשתמש, אלא גם עלול להשפיע על החשיפה ועל יכולות הפרסום של האפליקציה בחנות Play.
אבל הגעתם למקום הנכון, ובסוף ההרצאה הזו יהיו לכם הכלים לא רק לעמוד בדרישות האלה, אלא גם לעבור אותן.
אפשר לבצע אופטימיזציה של קוד DEX באמצעות R8, כלי האופטימיזציה של אפליקציות ל-Android.
אמנם לא חובה להשתמש ב-R8 אם כלי אחר לאופטימיזציה של אפליקציות יכול להביא את האפליקציה שלכם מעל לסף הזה, אבל זו הדרך המומלצת והיא מובנית ישירות ב-toolchain של Android.
למרות ש-DEX שעבר אופטימיזציה טובה ו-App Bundle משתמשים בפחות זיכרון, היתרונות העיקריים כאן עדיין קשורים לביצועים: פחות שגיאות ANR, הפעלה מהירה יותר וביצועים משופרים של זמן הריצה.
לאופטימיזציה של האפליקציה יש תוצאות חיוביות, אמיתיות ומוכחות, גם לאפליקציה וגם למשתמשים.
לדוגמה, על ידי שימוש במצב המלא של R8 ושיפור כללי השמירה, חברת Grab הצליחה להשיג 78% אופטימיזציה, 79% כיווץ ו-82% ערפול קוד באפליקציה שלה.
יכול להיות שאתם מסתכלים על השקף הזה וחושבים, "אוקיי, המספרים האלה די גבוהים, אבל מה זה אומר בפועל?"
כתוצאה מהשיפורים האלה, בשילוב עם פרופילים של Baseline, הם הפחיתו את מקרי ה-ANR ב-32% ושיפרו את ההפעלה ב-35%.
כשאפליקציות מובילות מתחילות להשתמש ב-R8, הן רואות ירידה ממוצעת של 38% בגודל ה-DEX וירידה של 16% במקרי ה-ANR שהשפיעו על המשתמשים.
אם אתם כבר משתמשים ב-R8, יש לנו חדשות נהדרות בשבילכם.
באפליקציות שכבר הפעילו את R8, ואז נקטו צעדים כדי לשפר עוד יותר את רמות האופטימיזציה, הכיווץ וההסתרה ליותר מ-50%, נרשמה ירידה נוספת של 25% בממוצע בגודל ה-DEX, ירידה של 4% בזמן ההפעלה וירידה נוספת של 17% במקרי ה-ANR שהשפיעו על המשתמשים, בנוסף ל-16% שהראינו בשקף הקודם.
לכן, היום אנחנו רוצים להסביר לכם איך אתם יכולים להגיע למספרים כאלה.
נסביר על הכלים שיכולים לעזור לכם לבצע אופטימיזציה של קוד ה-DEX ולעבור מכללי שמירה רחבים מדי ומוגדרים באופן שגוי לכללים ספציפיים ומצומצמים.
קודם כל, חשוב לציין ש-R8 קיים כבר הרבה שנים.
אנחנו מודעים לכך שלמרות שהכלי הזה מאפשר אופטימיזציה משמעותית, עדיין צריך להשקיע הרבה מאמץ כדי להגדיר אותו.
קריאות רפלקטיביות גורמות לכך שהקלט לא צפוי, ועלולות לגרום לקריסות בזמן הריצה.
בעבר, זה גרם לצוותי האפליקציות והספריות להוסיף כללי שמירה רחבים מדי, רק כדי להיות בטוחים.
אנחנו יודעים ש-41% מהאפליקציות המובילות נמצאות מתחת לרמות האופטימיזציה של 25% שמוגדרות על ידי Google Play, אבל אנחנו גם יודעים ש-13% מהאפליקציות המובילות כבר מגיעות לרמה שגבוהה פי שלושה מהסף הזה.
הנתון הזה מתייחס לאפליקציות ממגוון רחב של תעשיות ותהליכים שעוברים המשתמשים.
כדי להשיג ציוני אופטימיזציה גבוהים לאפליקציה, אתם צריכים את הכלים המתאימים ואת הניראות הנכונה כדי לנהל גם את כללי השמירה שלכם וגם את אלה שעוברים בירושה מספריות.
לפני שנתחיל לדבר על הכלים והשיפורים החדשים שיעזרו לכם, אנחנו רוצים להדגיש כמה שיפורים אחרים ב-R8 מהשנה האחרונה, שגם הם מקלים קצת על האופטימיזציה.
קודם כול, תבניות חדשות של אפליקציות כוללות היקף חבילה, שמועיל להבטחת רמה מינימלית של אופטימיזציה כבר מההתחלה.
הכלי לניתוח ההגדרות של R8, שעליו נדבר עוד רגע, נתמך עכשיו בגרסאות ישנות יותר של AGP.
ויש גם תמיכה לא רשמית ב-Bazel.
וב-AGP 9.2, פרימיטיבים של קורוטינה הפכו למהירים פי שניים.
כתוצאה מהשיפורים ב-R8 שבוצעו ב-AGP 9.2, ב-Tinder נרשמה ירידה של 13% בשגיאות ANR אחרי השדרוג מגרסה 9.0.1.
בנוסף, ב-AGP 9.3 יש ממשק API מעוצב מחדש של R8 עם שמות ברורים יותר, הגדרות ברירת מחדל פשוטות יותר לכללי ברירת המחדל ולכיווץ מקורות המידע, ותמיכה של ספריית קובצי המקור של AGP בכללי השמירה.
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}העדכונים שנדבר עליהם היום הם גם שיפורים בחוויית המפתחים.
נתחיל בהסבר על איך להשתמש בכלי לניתוח ההגדרות של R8 ובמיומנות של R8 כדי לזהות את התקורה של כללים מדור קודם ולקבוע דרך לשיפור.
לאחר מכן, כריס יסביר איך לעבור מכללי שמירה רחבים עם תווים כלליים לשימוש בהערות החדשות שנוצרות אוטומטית, מדויקות ומונעות שגיאות הקלדה.
ולבסוף, נראה איך להשתמש בכלי אימות חדשים של CLI כדי להבטיח אופטימיזציה אגרסיבית בלי לגרום לקריסות.
לכן, הכלי לניתוח ההגדרות של R8 הוא המקום האידיאלי להתחיל בו את בדיקת האופטימיזציה של DEX.
אם עדיין לא השתמשתם בכלי, כך הוא נראה.
זהו דוח HTML שמציג באופן חזותי מידע על הגדרות R8.
הדוח הזה מיועד לאפליקציה שלי, שכוללת ספרייה בשם Analytics Bloat.
כמו שאתם יכולים לנחש, זו לא אפליקציה אמיתית או ספרייה אמיתית, אבל היא מודגשת בכחול בחלק העליון.
הכלי לניתוח ההגדרות מודד את הדברים הבאים: כיווץ, שהוא אחוז המחלקות, השיטות והשדות שכללי השמירה לא מונעים מ-R8 לכווץ.
החישוב הזה מתבצע אחרי ש-R8 מבצע את סבב הכיווץ הראשון שלו.
הוא גם מודד את האופטימיזציה, שהיא אחוז הקוד שפתוח לאופטימיזציות של כל התוכנית, כמו הטמעת מתודות ומיזוג מחלקות.
וגם ערפול, שהוא אחוז החברים שאפשר לערפל או לשנות את השם שלהם למזהים קצרים יותר.
חשוב גם לציין שהכלי R8 Config Analyzer מחשב את הציונים האלה לפני האופטימיזציות הסופיות והקימפול של DEX, ולכן הערכים שמוצגים בדוח הזה לגבי אופטימיזציה, כיווץ וערפול קוד לא יהיו זהים בדיוק לערכים שמוצגים ב-Play Console.
הסיבה לכך היא שב-Play נמדד פלט ה-DEX הסופי.
הסיבה לפער הזה היא כדי להבטיח שבזמן הפיתוח, תהיה לכם גישה ללולאת משוב מקומית ומהירה.
עם זאת, הציונים שמוצגים בכלי לניתוח הגדרות R8 עדיין שימושיים מאוד לאימות פוטנציאל האופטימיזציה של DEX לפני ההעלאה לחנות Play.
הדוח כולל גם את הטבלה "ניתוח כללי שמירה", שהיא התצוגה הראשית שמופיעה כשנכנסים לדוח.
משם, אתם יכולים ללחוץ על כל כלל ולראות את המספר המדויק של המחלקות, השיטות והשדות שכלל ספציפי שומר, כדי שתוכלו לראות אילו כללים שומרים יותר ממה שהם צריכים.
יש בו גם כללים מוכללים, שמוצגים כאן בהגדלה.
אלה כללים שחופפים לכלל רחב יותר.
הן מודגשות כדי שתוכלו להסיר את מילת המפתח הרחבה ולהשאיר רק את הממוקדת יותר.
הוא גם מציג כללים שלא נמצאים בשימוש, כלומר כללים שאפשר למחוק בלי לסכן את היציבות של האפליקציה.
לכן, Config Analyzer נועד לטפל בכמה בעיות, החל מהצטברות של כללים מדור קודם.
בעבר, בלי חשיפה לכל כלל בנפרד, היה קצת מסוכן למחוק כללים ישנים.
אז הצוותים פשוט לא נגעו בהם או הוסיפו תווים כלליים רחבים יותר.
שנית, הוא עוזר לכם לגלות נקודות עיוורות בכללי הספרייה.
לכן, כשמוסיפים SDK, קשה לדעת אם כלל שכלול בספרייה הזו הוא רחב מדי או אם הוא בכלל נחוץ, למשל, אחרי 10 גרסאות או יותר.
בהמשך נרחיב גם על ספריות.
שלישית, הוא עוזר לכם להעריך את ההשפעה של האופטימיזציה.
בעבר, לא הייתה דרך קלה לדעת אם כלל מסוים שומר שתי כיתות או 2,000 כיתות.
בואו נראה איך R8 פועל בהקשר של האפליקציה שלי ושל הספרייה שראינו קודם.
אז כאן יש לי מודול של אפליקציה ושלוש ספריות.
שימו לב לספרייה השלישית משמאל.
זוהי ספריית Analytics Bloat, שכוללת כלל שמירה ברמת החבילה.
קודם כול, R8 מזהה את נקודת הכניסה הבסיסית מכל כללי השמירה הממוזגים, שמודגשים כאן בצהוב.
ובאופן כללי, אלה צריכות להיות נקודות הכניסה העיקריות של האפליקציה, כמו מחלקות במניפסט.
ובגלל כלל השמירה של התווים הכלליים בספריית Analytics Bloat, כל מחלקה בספרייה נחשבת לנקודת כניסה.
לאחר מכן, R8 עוקב אחר הנגישות בתרשים הקריאות מהשורשים האלה.
כל מחלקה שאפשר להגיע אליה מהשורש מסומנת בירוק לשמירה, ומחלקות ללא הפניה מסווגות כקוד מת ומסומנות כאן באדום.
לאחר מכן, R8 מבצע tree shaking ומסיר את הקוד המיותר.
אז אחרי ה-tree shaking הזה, או שלב הכיווץ הראשון, זה מה שנשאר.
שיעור ההתכווצות של האפליקציה שלנו הוא 42.5%, שזה מעל הסף של 25% ב-Google Play, אבל זה עדיין לא המצב שהיינו רוצים.
וזה בעיקר בגלל כלל השמירה שחל על כל החבילה מספריית הניתוח.
לכן, רק 17 מתוך 40 הכיתות שנותרו יכולות להצטמצם עוד יותר.
הנקודה שחשוב לי להדגיש היא שכלל שמירה אחד שגוי יכול לגרום לבעיות משמעותיות באופטימיזציה של האפליקציה.
אז בואו נתמקד לרגע בספריות.
בשנה האחרונה, הצוות שלנו השקיע מאמצים רבים כדי להבין איך כללי השמירה של צרכני הספרייה משפיעים על האופטימיזציה של הסביבה העסקית.
ספריות רבות כוללות כללים רחבים מדי בשם היציבות.
או שהם יציינו כללים כלליים במסמכים שלהם, כדי שמפתחי האפליקציות יוסיפו אותם לאפליקציה בעצמם.
אבל לא צריך להשתמש בכללים רחבים כדי לשמור על יציבות, והם פוגעים בביצועים. העלויות של הפגיעה הזו חלות בעיקר עליכם, כמפתחי האפליקציה.
ראינו מקרים שבהם כל הספרייה נשמרה בגלל כלל שמירה שחל על כל החבילה, כמו ספריית הניתוח המנופחת, ובמקרה של הספרייה הזו, הייתה לכך השפעה מרחיקת לכת על פוטנציאל האופטימיזציה של האפליקציה.
חשוב לציין שגם ערכות ה-SDK של Google לא היו חסינות לבעיה הזו, ובשנה האחרונה תוקנו כללי שמירה רחבים מדי ביותר מתריסר ספריות של Google.
אם אתם רוצים להשתמש בכלי לניתוח הגדרות אישיות, הוא משולב ישירות ב-AGP 9.3, אבל הוא זמין גם לגרסאות ישנות יותר אם משדרגים את גרסת R8.
לכן, הפעלת הפקודה הזו תיצור את דוח ה-HTML שהראינו, שמציג את כללי השמירה, ההשפעות על הכיווץ, האופטימיזציה וההסתרה של כללים ספציפיים ושל האפליקציה כולה.
אם אתם מריצים גרסת build מלאה להפצה של R8, הדוח נוצר באופן אוטומטי ואפשר למצוא אותו בנתיב הקובץ הזה.
היתרון של המשימה העצמאית הזו הוא שלוקח פחות זמן ליצור אותה מאשר את ה-build המלא.
הצוות של WHOOP, שאולי נמצא כאן איפשהו, הפעיל את מנתח ההגדרות וקיבל כיווץ ב-74%, ערפול ב-74% ואופטימיזציה ב-0.
כמו שאתם יכולים לתאר לעצמכם, הצוות הופתע מאוד לראות את ערך האופטימיזציה הזה.
הניראות שסיפק כלי הניתוח של קובצי התצורה עזרה להם לגלות שה-build שלהם השתמש בקובץ ProGuard מדור קודם שהשבית את האופטימיזציה. לכן, על ידי מעבר לגרסה המעודכנת, הם הצליחו להפעיל את האופטימיזציה של האפליקציה שלהם.
התוצאה הייתה אופטימיזציה של 55% והקטנה של 4% בגודל ה-APK.
בשילוב עם פרופילים של Baseline, הם שיפרו את זמני ההפעלה במצב התחלתי ב-38%, והפחיתו את מספר הפריימים הקפואים ב-28%.
המסך בצד ימין הוא בלי פרופילים של Baseline, והמסך בצד שמאל הוא עם פרופילים של Baseline. כמו שאפשר לראות, האפליקציה מופעלת מהר יותר כשמוסיפים פרופילים של Baseline.
אני רוצה להשמיע את זה שוב.
מגניב.
הקישור הזה יוביל אתכם למדריך של כלי הניתוח של קובצי התצורה. בדיקה ידנית של הדוח היא אפשרות מצוינת, אבל כמו שאתם יכולים לתאר לעצמכם, בהתאם למצב של הגדרת ה-R8 של האפליקציה, זה יכול לקחת הרבה זמן.
כדי לזרז את העניינים, יש לנו מיומנות של R8 Analyzer.
$ android skills list
$ android skills add r8-analyzerאתם יכולים לבקש מהנציג לנתח את הגדרות ה-R8 שלכם, והמיומנות לא רק תיצור את דוח ניתוח ההגדרות של R8, אלא גם תפנה אתכם לכללים הכי משפיעים.
הנה פלט לדוגמה, ורק כדי לציין כמה פרטים, הוא מציין את ציון האופטימיזציה של 42%, מה שאומר ש-52%-- 58%, ליתר דיוק, של בסיס הקוד לא יכולים לעבור אופטימיזציה על ידי R8.
ואם נסתכל ספציפית על הקטע של כללי השמירה, כלל השמירה שחל על כל החבילה של ספריית הניתוח המיותרת מונע אופטימיזציה של 22% מבסיס הקוד. בנוסף, המיומנות ממליצה לנו לחדד את כלל השמירה הזה כי, כמו שאנחנו כבר יודעים, הוא רחב מדי.
עכשיו אתם יכולים לאבחן מה גורם לאפליקציה להתנפח ולחסום את האופטימיזציה של DEX באמצעות הכלי לניתוח ההגדרות של R8 והמיומנות.
אבל אחרי שמוצאים כלל שמירה רחב מדי, איך מחליפים אותו בצורה בטוחה בלי לגרום בטעות לקריסה בלתי צפויה?
עכשיו אעביר את השרביט לכריס, שיראה לכם איך לעבור מכללי השמירה הקודמים להערות השמירה החדשות שנוצרות באופן אוטומטי ומונעות שגיאות הקלדה.
תודה, Breana.
אוקיי, אז בואו נדבר על איך הגענו לכאן, נכון?
בספריית הניתוח המנופחת הזו, עם כל כללי השמירה האלה, שבעצם שומרים על הכל, הבעיה העיקרית שאנחנו נתקלים בה היא השתקפות.
אז בואו נסתכל על קטע קטן מהקוד הרפלקטיבי, רק כדי להזכיר לכם.
fun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}זו פונקציה ליצירת מופע של אובייקט, שרק מנפחת אובייקט מסוים, ועושה זאת באופן רפלקטיבי.
החיסרון של זה, בהקשר של אופטימיזציה של DEX, הוא שזה מעכב את כל הקריאות הסטטיות האלה עד לזמן הריצה.
רק על ידי התבוננות בקוד, בהתאם למקור של שם המחלקה, כלי האופטימיזציה R8 שלנו לא יכול לראות מה קורה כאן.
אלה המצבים שבהם אופטימיזציית Dex לא פועלת כברירת מחדל, ולכן בדרך כלל צריך להוסיף כלל Keep.
עכשיו, באופן אידיאלי, פשוט נמנעים מזה, נכון?
באופן אידיאלי, אנחנו פשוט נמנעים לחלוטין משיקוף, במיוחד עכשיו בשנת 2026, כשיש לנו כלים מדהימים ליצירת קוד לסריאליזציה, להזרקת תלות, שיכולים פשוט לחסל לחלוטין את קטגוריית הבעיות הזו.
אבל זה לא הכל, נכון?
עדיין צריך להשתמש ב-reflection במקרים מסוימים.
אז בואו נראה איך זה קרה בספרייה המנופחת הזו.
אז הנה דוגמה אחת, וזו דוגמה נפוצה, של תלות אופציונלית.
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}כאן אנחנו מנפחים סוג מסוים שלא נגיש ב-classpath של הספרייה, אז אנחנו צריכים לעשות את זה עם מחרוזת.
ההתייחסות היא למחלקה הספציפית הזו, שיכול להיות שהייתה קיימת או לא הייתה קיימת בזמן שהספרייה נוצרה. לכן, סביר מאוד שהספרייה הזו משתמשת ברפלקציה כדי למצוא איבר בתוך ספרייה קשורה.
מקרה נוסף שיש לנו כאן הוא יצירת מופעים של מחלקות משנה באופן דינמי.
לכן, במיוחד אם מדובר במשהו שיוצא מקובץ מניפסט או במשהו שנשלח ממשהו בצד השרת, אנחנו מודיעים לאפליקציה המקומית, "היי, בבקשה תנפחי את הדבר הזה, תיצרי את הדבר הזה מאוחר יותר".
והקטגוריה השלישית שאנחנו רואים כאן היא, למרבה המבוכה, סריאליזציה.
כלומר, יש לי סריאליזציה של JSON שמתרחשת על ידי איטרציה דרך כל החברים.
זה ממש מגעיל, אבל אתם יודעים, לפעמים דברים כאלה נשארים.
לא תמיד מתקנים אותה ברגע שצריך.
אבל בואו נדבר על התוצאה של זה, נכון?
ככל שהקריאות הרפלקטיביות האלה מצטברות, יש לכם יותר מוטיבציה לעשות את הדבר הקל, ולבצע אופטימיזציה לחוויית המפתח על פני חוויית המשתמש.
זה לא נעים, אז אני אוסיף משימה וזה יהיה בסדר.
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };יום אחד, אני אבוא ואתקן את זה, נכון?
אז בואו נדבר על מה שאנחנו אמורים לעשות, מה שהיינו אמורים לעשות לפחות לפני היום, איך הייתם כותבים כלל שמירה אידיאלי כאן.
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}אוקיי, הנה זה בא.
אז החלק הראשון כאן הוא שאנחנו רוצים לשמור את המחלקה הספציפית שאנחנו מחזירים מהפונקציה הרפלקטיבית הזו.
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}בעצם, לא, רגע.
אתם יודעים מה?
זה לא נכון.
זו מחלקה של Kotlin, אני צריך לוודא שהיא מוגדרת כמחרוזת, אז אוסיף את ה-KT.
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}למעשה, הדבר החשוב כאן הוא שאני לא באמת מחזיר EventFilter, אלא מחזיר מחלקת משנה של EventFilter, אז אני צריך לשמור מחלקות שמרחיבות את EventFilter כדי שאוכל לנפח את זה בצורה נכונה.
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}ובאמת, אם אני רוצה לעשות את זה בצורה יעילה יותר עכשיו, זה היה עובד, אבל כדי להפוך את זה ליעיל יותר, כמפתח ספרייה טוב, מה עוד כדאי לי לעשות?
כלומר, צריך להגיד -keepclasseswithmembers, ולשמור רק את המחלקות שיש להן את ה-constructor הספציפי <init> שבו אני משתמש כדי להרחיב את האובייקט.
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}ואם אני רוצה להיות ממש נחמד, אוסיף את allowaccessmodification כדי לאפשר אופטימיזציה נוספת בכל ממשקי ה-API שאליהם מכוון כלל ה-Keep הזה, כי אני לא רוצה למנוע אופטימיזציה נוספת מ-R8, אני רוצה לנסות להיות אזרח טוב.
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}והשלב האחרון, שכמעט אף אחד לא עושה כי הוא דורש קצת יותר עבודה, אבל הוא מעולה לספרייה, הוא להפוך את זה למותנה.
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}עכשיו, מבחינה רעיונית, אתם לא צריכים לשמור את כל מחלקות המשנה האלה, אלא אם אתם באמת קוראים לפונקציה הזו, וזה מה שהצהרת ה-if הזו עושה בחלק העליון של כלל Keep.
אבל, אתם יודעים, אני יכול לבקש מכם להרים ידיים, אבל אני יודע שכמעט אף אחד לא עושה את זה, נכון?
כלומר, ממש לכתוב אותו, להפוך אותו למותנה.
אז מה אם היינו יכולים לשפר את זה קצת?
בואו נסתכל קודם על, נגיד, אוקיי.
אולי יש דרך אחרת לעשות את זה.
אז דיברנו על מה קורה אם אני כותב כלל שמירה מצומצם באופן ידני?
טכנית זה אפשרי, אבל קשה לכתוב ולקרוא את זה, וקשה מאוד לתחזק את זה כי יש שם הרבה חלקים קטנים עם טיפוסים מסוג מחרוזת שקל לשבור כשמשהו מתחתיהם קורה.
מתחת למשהו, משהו מתחתיו משתנה.
ויש לנו חלופות.
יש לנו הערות כמו @Keep.
ההערה @Keep נמצאת בחבילת ההערות של AndroidX כבר הרבה זמן, אבל למעשה לא מומלץ להשתמש בה בימים אלה.
@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }זה לא פתרון טוב למפתחי ספריות, ולמעשה, זה די אסור באופן פנימי, כי למרות שקל יותר לתחזק אותו, הוא מאוד לא גמיש, והוא לא כולל חלק מהאופטימיזציות שדיברתי עליהן קודם.
הוא לא ממש יכול.
הוא לא מאפשר ניתוח של אתר השיחה.
עכשיו, אם נסתכל על המקרה הספציפי הזה שהיה לנו קודם, "טוב, אני יכול פשוט לשים את @Keep ב-EventFilter שלי, אני בסדר, נכון?"
@Keep
class EventFilter { … }לא ממש.
@Keep לא באמת מטרגט מחלקות משנה, אלא רק את המחלקה הזו.
@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }אז בעצם אני צריך לטרגט את כל מחלקות המשנה, ועכשיו אני כותב יותר הערות @Keep מאשר אם הייתי משתמש בכלל Keep היחיד שהיה קשה לכתוב.
אז בשתי האפשרויות האלה יש בעיות, נכון?
קל לשכוח אותם, מגלים אותם מאוחר, והם לא מותנים במתקשר כברירת מחדל.
אנחנו רוצים דרך טובה יותר, במיוחד לספריות.
אנחנו רוצים שתהיה דרך קלה יותר להגדיר את הדברים בצורה נכונה כבר מההתחלה.
ואחת הבעיות הגדולות שאנחנו רואים כאן היא שהדברים האלה נמצאים רחוק מההשתקפות שאנחנו בעצם מנסים להגן מפניה.
בסופו של דבר, הפתרון שאנחנו רוצים הוא משהו שפועל ישירות מול קוד ההשתקפות הבעייתי כאן, שלמעשה מנפח את הדברים המטורגטים.
אז בואו נתחיל לדבר על Jetpack Keep, הספרייה והפלאגין החדשים מ-AndroidX שנועדו לעזור בדיוק בבעיה הזו.
אז כדי להשתמש… המטרה של Jetpack Keep היא להתחיל ביצירת כללי Keep מהערות, אבל אפילו מעבר לזה, ליצור הערות ישירות מההשתקפות שכבר נמצאת בבסיס הקוד.
ההגדרה מאוד פשוטה.
plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}זה פלאגין של Gradle, זו ספריית הערות.
אבל בואו נראה איך זה פועל בפועל בזמן ה-build.
אז אם יש לי את הדוגמה הקודמת שבה אני מנפח תת-מחלקה של סוג ידוע, מה עליי לעשות?
אני פשוט מפעיל את lintFix.
./gradlew analyticsbloat:lintFixה-Lint כאן מנתח את כל הרפלקציה שמתרחשת בתוך השיטה הזו, ואומר, "היי, אתה צריך את ההערה הזו".
@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}ההערה הזו מבטאת בדיוק את מה שההשתקפות בתוך הקריאה הזו עושה.
הוא אומר, "אני מנפח כיתה מהסוג הזה".
במקרה הספציפי הזה, EventFilter היא מחלקה מוכרת.
אני לא משתמש במחרוזת כדי להפנות אליו כי אין צורך בכך.
זו שכבת בטיחות נוספת, ואני אפילו מקודד את ה-parameterTypes כך שהוא אומר בדיוק איזה בנאי נשמר.
אבל מה זה עושה?
למשל, איך זה משפיע על גרסת ה-build הסופית של R8?
הוא יוצר את כלל השמירה, ואם ראיתם קודם, זה בדיוק אותו כלל שמירה אידיאלי שהיה לנו קודם.
# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}הוא מטרגט בדיוק את אותן מחלקות, את מחלקות המשנה, והוא מותנה בנקודת ההשתקפות הזו.
ואני לא צריך לתחזק-- אני לא צריך לדאוג להוספת הערות לכל מחלקות המשנה.
אני לא צריך לשמור קובץ נפרד של כללי שמירה בשום מקום אחר.
זו דרך הרבה יותר קלה לנהל את כללי השמירה.
נחזור לדוגמה של התלות האופציונלית.
@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}עכשיו, המחלקה לא הייתה בנתיב המחלקה, אבל עדיין אפשר להפנות אליה.
אני פשוט עושה את זה באמצעות מחרוזת, אבל כל השאר, סוגי הפרמטרים, אפשר להפנות אליהם באמצעות הפניות למחלקות, אז אני עושה את זה.
וגם כאן, אנחנו יוצרים את כלל השמירה האידיאלי למקרה הזה.
# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}הוא מותנה, ספציפי ומטורגט, והוא מאפשר אופטימיזציות בכל מקום אפשרי במחלקות המטורגטות האלה.
כאן יש השתקפות בשדות, ובמקרה הספציפי הזה, חזרתי על כל השדות בתוך אובייקט, אבל גם כאן, אנחנו מטפלים בזה בסדר גמור על ידי אמירה לטרגט את כל השדות עם אופרטור הכוכב הזה.
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}זה מאפשר לי לטרגט את כל השדות, ותראו, זה יוצר בדיוק את כלל השמירה שהייתם מצפים לו בשביל, אתם יודעים, הרפלקציה המצערת הזו שלא אמורה להיות באפליקציה שלי, אבל, היי, אני אחזור ואנקה את זה, אני מבטיח.
# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}אז מה Jetpack Keep עושה?
הוא מצמצם באופן משמעותי את התחזוקה של כללי השמירה.
כל מה שאפשר ליצור באופן אוטומטי, פשוט מסיר את זה מתחום הדאגה ומתחום הסיכון שלכם לשמירה על כללי השמירה.
הוא גם נותן לכם את הדטרמיניזם של lint.
הוא לא רק מתקן דברים תוך כדי איטרציה וניסיון להעביר, הוא גם תופס רפלקציה חדשה ושינויים רפלקטיביים בכל פעם שהקוד משתנה, והבטיחות הזו היא קריטית לחלוטין.
אפשר להשתמש בזה?
אז מהם יחסי התלות כאן?
הפלאגין וה-lint דורשים AGP 8.7.
זה אמור להיות כמעט כולם.
אבל אפשר להשתמש בהם בהרבה מקומות.
אפשר להשתמש בזה בתוך אפליקציות.
אפשר להשתמש בזה בספריות של Android, בקובצי AAR, בספריות KMP ובקובצי JAR – במקרה כזה צריך לבצע קצת חיווט ידני.
אבל התכונה העיקרית כאן היא שזה בטוח לשימוש כמפתח ספרייה.
גם אם מישהו משתמש בגרסה ישנה של R8 או אפילו בכלי של צד שלישי שמשתמש בספרייה שלכם, אנחנו עדיין מייצאים כללי צריכה סטנדרטיים בפועל כדי שכל אחד יוכל להשתמש בכם ולא יצטרך לדאוג לגבי שימוש בפורמט חדש שלא תואם.
אז עכשיו, אחרי שדיברתי על מה ש-Jetpack Keep יכול לעשות כדי לעזור לכם לבצע אופטימיזציה ולהפחית את הדאגה לגבי הגנה על קריאות הקוד הרפלקטיביות שלכם, בואו נדבר על התכונות החדשות שהשקנו לאחרונה ב-Android CLI כדי לבדוק שוב את שלב ההשקה האחרון.
בריאנה ציינה קודם שיש כלים שבהם הממצאים של Play יספקו לכם מידע על הגודל, על גודל ה-DEX ועל אחוזי האופטימיזציה.
עכשיו התחלנו להפוך את זה לזמין גם דרך Android CLI.
הוא משתמש באותו היגיון כמו חנות Play, כך שאתם לא צריכים להעלות חבילה לחנות Play, אלא פשוט להשתמש ב-CLI כדי לנתח את החבילה.
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>אתם מקבלים את אותו המידע בדיוק, שמדווח לכם ישירות ובאופן מיידי באמצעות ניתוח מקומי.
במקרה הספציפי הזה, האפליקציה הזו מקבלת אזהרה על שיעור אופטימיזציה נמוך מאוד, וגם נאמר לה, "היי, זה גודל ה-DEX שלך, והנה אחוזי האופטימיזציה שלך", בדיוק כמו שחנות Play הייתה אומרת לך.
אבל יש כאן גם פלט JSON, כך שאם אתם רוצים לכתוב סקריפט כדי להפוך את בדיקות הרגרסיה לאוטומטיות, למקרה שאחד מהערכים האלה יורד באופן דרסטי, כי שוב, אלה המספרים ש-Play אוכפת, אתם רוצים לעקוב אחריהם, אתם יכולים לעשות את זה בקלות רבה עם ערך ה-JSON הזה – דוחות ה-JSON האלה.
{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}כאן אנחנו רואים שמופיעים בדיוק אותם מספרים.
אבל זו לא התכונה היחידה של android analyze.
אפשר להפעיל את android analyze גם בספריות.
$ android analyze <library.aar>
$ android analyze <library.jar>וכמו שבריאנה ציינה, יש הרבה בעיות בספריות שפועלות עם משלוחים עם כללי שמירה מצערים בתוכן.
אז בואו נשתמש בזה כדי לבחון כמה מהבעיות האלה.
אז כשאני מריץ את זה על קובץ AAR מסוים שיש לי, אני יכול לראות, היי, לספרייה הזו יש כמה בעיות שמודגשות ממש בחלק העליון.
בספרייה הזו יש יחס שמירה גבוה, כלומר היא שומרת יותר מדי פריטים עם כללי שמירה לא מוגבלים.
הוא מכיל כללי שמירה רחבים שמונעים ממנו לעבור אופטימיזציה, ויש כמה מספרים למטה שמראים בדיוק עד כמה זה גרוע. הוא גם מכיל כללי שמירה שלא מטרגטים שום דבר, וזה אולי הדבר הכי מפחיד, כי זה כנראה אומר שהם מטרגטים מישהו אחר בצורה לא מוגבלת.
קוד מסוכן מאוד.
אז אתם יכולים לבדוק את כל זה ב-AAR, במיוחד אם אתם מפרסמים ספרייה. זו דרך מצוינת לוודא שאתם לא שולחים כללי שמירה לא תקינים, ושהם לא דולפים פנימה כשמישהו שאולי לא מומחה בכתיבת כללי שמירה עובד על הספרייה.
כאן אפשר לראות שאנחנו גם מוציאים יחס שמירה.
זו דרך טובה להעריך את האיכות הכוללת של הספרייה.
אם אני מבצע אופטימיזציה לספרייה נפרדת באופן עצמאי לחלוטין, ללא הפניות, באמצעות R8, אני אמור לראות שהיא בגודל אפס בייטים, נכון?
הוא אמור להיות ריק, כי לא נשמר בו שום דבר.
זה היה אידיאלי.
במקרה הזה, לא.
אתם יכולים לראות את הגודל של R8 ו-D8 ישירות.
למעשה, אנחנו גם מדגישים כמה מהכללים הכי פחות יעילים כדי שתוכלו לראות אותם בקלות.
גם כאן יש פורמט JSON, אז שוב, אם אתם מפתחי אפליקציות שמנסים להגביל עדכוני ספריות או אם אתם מפתחי ספריות שמנסים למנוע רגרסיות גדולות באיכות הספרייה, אתם יכולים לבצע את אותו ניתוח ולמנוע את הבעיות האלה באמצעות סקריפט פשוט שעוטף את Android CLI.
{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}לכן, אם אתם נתקלים בבעיה בספרייה, קודם כל נסו לעדכן אותה.
כמו שבריאנה ציינה קודם, תיקנו יותר מתריסר ספריות של Google כדי לתקן כמה כללי שמירה מצערים מאוד, ויש ספריות אחרות שבוצעו בהן תיקונים, אבל הן פשוט לא מתעדכנות על ידי האפליקציות שמשתמשות בהן.
זה צעד ראשון מצוין.
דבר שני, אתם יכולים לחפש בעיה ולפנות למפתח הספרייה, ואולי לצרף דוח ניתוח שאומר לו, "היי, אתה שומר יותר מדי.
אני רוצה עזרה באופטימיזציה של האפליקציה שלי".
אז לסיכום, דיברנו על R8 Config Analyzer, שעוזר לכם לבדוק את כל מה שקורה ב-build של R8, כל מה שמשפיע על מה שאפשר לבצע לו אופטימיזציה ומה שלא.
דיברנו על Jetpack Keep, הספרייה החדשה שעוזרת לכם לכתוב כללי שמירה באופן אוטומטי בלי להתאמץ.
וגם Android CLI, הכלי החדש שעוזר לכם לבדוק ולוודא, לבצע בדיקה כפולה מול השלב האחרון, ולוודא שאתם, הנציג שלכם או מהנדס זוטר בצוות שלכם לא הרסתם שום דבר לצרכנים שלכם.
אם אתם רוצים לקבל מידע נוסף, אתם יכולים לעיין במסמכי התיעוד של R8, של מנתח ההגדרות ושל Jetpack Keep.
בנוסף, יש מסמכי תיעוד על Android CLI שבהם מוסבר על הפקודה analyze ואיך להשתמש בה.
תודה רבה.
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}$ android skills list
$ android skills add r8-analyzerfun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# TODO: clean this up
-keep com.example.analytics.bloat.** { * };fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }@Keep
class EventFilter { … }@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}./gradlew analyticsbloat:lintFix@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}$ android analyze <library.aar>
$ android analyze <library.jar>{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}