מדריכים

קביעת עדיפות ליעילות הזיכרון: שלבים חיוניים ל-Android 17

משך הקריאה: 10 דקות

למרות שלרוב משווים בין ביצועי האפליקציה לבין ממשק משתמש חלק וזמני הפעלה מהירים, הזיכרון הוא הבסיס השקט שעליו נבנים המדדים הגלויים האלה. זה לא סוד שזיכרון המכשיר חשוב יותר מאי פעם. בנוסף לשיפורים שביצענו באופטימיזציה של הזיכרון ב-Android 17, אנחנו מספקים כלים ותמיכה ב-API שיעזרו לכם לעמוד בדרישות זיכרון מחמירות יותר בהמשך השנה.

כדי להבטיח את יציבות המכשיר, החל מ-Android 17, המערכת תתחיל לאכוף מגבלות על זיכרון האפליקציה על סמך ה-RAM הכולל של המכשיר. אם אפליקציה חורגת מהמגבלות האלה, מערכת Android תסיים את התהליך בלי ליצור דוח קריסות.

בנוסף לסגירה הכפויה הזו, שימוש לא אופטימלי בזיכרון פוגע בחוויית המשתמש. כשהאפליקציה מתקרבת למגבלות של זיכרון הערימה, היא מפעילה מנגנון איסוף זבל תכוף, מה שגורם לגמגום ניכר בממשק המשתמש. בנוסף, כשנגמר הזיכרון הזמין במכשיר, המערכת מנסה לשחרר דפים, מה שגורם לעומס על המעבד, להשהיה בממשק המשתמש ולהתרוקנות הסוללה. אם המחסור בזיכרון חמור מדי, הוא עלול לגרום לאירועים של הפסקת תהליכים בגלל מחסור בזיכרון (LMK), שמפסיקים באופן פתאומי תהליכים ברקע ומאלצים את האפליקציות לבצע הפעלה קרה איטית ולאבד את מצב המשתמש.

כדי ליצור אפליקציות עם ביצועים גבוהים ולמנוע את הסגירות הכפויות האלה, מומלץ להשתמש באסטרטגיות הבאות לאופטימיזציה של הזיכרון:

  1. אופטימיזציה של קוד בייט באמצעות R8
  2. אופטימיזציה של טעינת תמונות
  3. זיהוי ותיקון של דליפות זיכרון באמצעות Android Studio
  4. חיתוך הזיכרון כשהאפליקציה יוצאת ממצב גלוי
  5. התבוננות מתקדמת בזיכרון באמצעות ProfilingManager

גרסה מקוצרת של הפוסט הזה בבלוג זמינה גם בפורמט וידאו. כדאי לצפות בה!

הסבר על מגבלות הזיכרון של אפליקציות ב-Android 17

אנחנו משיקים ב-Android 17 מגבלות על הזיכרון של האפליקציות, כדי למנוע מצב שבו "גורם זדוני אחד" יפגע בחוויית הריבוי-משימות וביציבות של המכשיר כולו.

הנה פירוט של הסיבות לשינוי הזה בארכיטקטורה:

  • מניעת השבתה מדורגת: כשאפליקציה מתנפחת או מתרחשת בה דליפת זיכרון בזמן שהיא נמצאת במצב הרשאה (למשל, היא מפעילה שירות בחזית), היא מוגנת בהתחלה מפני תהליך ההשבתה של המערכת בגלל זיכרון נמוך (LMK). כשהאפליקציה הזו גדלה ללא בקרה וצוברת זיכרון RAM, ליבת ה-LMK נאלצת לפצות על כך על ידי סגירה של עשרות אפליקציות קטנות יותר שמאוחסנות במטמון ושל משימות ברקע, כדי לפנות מקום לאפליקציה שצורכת הרבה זיכרון.
     
  • שמירה על ריבוי משימות ומצב המשתמש: כשהמערכת נאלצת למחוק אפליקציות שמורות במטמון כדי לפנות מקום לתהליך דליפה יחיד, חוויית ריבוי המשימות נפגעת באופן משמעותי. משתמשים שחוזרים לאפליקציות קודמות שנשמרו במטמון נתקלים בהפעלות קרות איטיות במקום בהפעלות חמות כמעט מיידיות. חוסר היעילות הזה יוצר עומס רב יותר על ה-CPU ומגביר את קצב ההתרוקנות של הסוללה. הפעולה הזו יכולה גם להרוס את ההקשר של המשתמש באפליקציות שהיו בשימוש לאחרונה, כמו מיקומי גלילה, ערימות ניווט והתקדמות במשחק.

כדי לבדוק אם ההגבלות האלה השפיעו על סשן האפליקציה בשטח, אפשר לקרוא ל-getDescription()‎ בתוך ApplicationExitInfo. אם המערכת החילה מגבלה, סיבת היציאה תדווח כ- REASON_OTHER ומחרוזת התיאור תכיל את הערך MemoryLimiter:AnonSwap. אפשר גם להשתמש בפרופילים מבוססי-טריגר באמצעות TRIGGER_TYPE_ANOMALY כדי לתעד באופן אוטומטי dump ערימה כשמגיעים למגבלת הזיכרון. בנוסף, צוות Android פועל באופן פעיל כדי להציג למפתחים מדדי זיכרון נוספים בשטח ב-Google Play Console.

הרחבנו גם את התיעוד בנושא מגבלות הזיכרון כך שיכלול פקודות לניפוי באגים מקומי. כך תוכלו לדמות מגבלות זיכרון בסביבה המקומית שלכם ולאמת את התנהגות האפליקציה שלכם במסגרת אכיפה של מגבלת זיכרון כלשהי. 

אופטימיזציה של קוד בייט באמצעות R8

דרך יעילה מאוד לצמצם את הזיכרון שבשימוש של האפליקציה היא להפעיל את הכלי R8 Optimizer. על ידי כיווץ מחלקות, שיטות ושדות לשמות קצרים יותר והסרת קוד ומשאבים שלא נמצאים בשימוש, R8 מצמצם באופן משמעותי את הזיכרון שבשימוש של האפליקציה על ידי מזעור כמות הקוד שנדרשת במהלך ההפעלה. 

‫R8 מצמצם את הקוד שפועל בזיכרון, מקטין את הזיכרון שבשימוש ומפחית את הסיכון לסיום LMK. כך מתקבלות הפעלות במצב ביניים (warm start) בתדירות גבוהה יותר מאשר הפעלות איטיות במצב התחלתי (cold start). בנוסף, קוד בייט יעיל מפחית את התקורה של מעבד ה-CPU בשרשור הראשי, וכך מצמצם באופן ישיר את שיעורי ה-ANR ומשפר את חוויית המשתמש. לדוגמה, הבנק הדיגיטלי Monzo הפעיל אופטימיזציה מלאה של R8 ודיווח על ירידה של 35% בשיעור מקרי ה-ANR, שיפור של 30% בשיעור ההפעלה במצב התחלתי וירידה של 9% בגודל האפליקציה הכולל.

pic1-IO26_113_TSV-monzo-casestudy.jpg
הבנק הדיגיטלי Monzo הפעיל אופטימיזציה מלאה של R8 ושיפר את מדדי הביצועים בעד 35%.

כדי להגדיר את R8 בצורה נכונה בקובץ build.gradle:

  • מגדירים את isShrinkResources = true ואת isMinifyEnabled = true.
  • משתמשים ב-proguard-android-optimize.txt במקום ב-proguard-android.txt מדור קודם, שמונע אופטימיזציות וכבר לא נתמך ב-פלאגין של Android Gradle 9.
  • הסרת android.enableR8.fullMode = false מה-gradle.properties.

אם אתם משתמשים ברפלקציה בבסיס הקוד, אתם צריכים להוסיף כללי שמירה כדי למנוע מ-R8 לבצע אופטימיזציה של החלקים האלה בקוד. כדי להפיק את האופטימיזציה המקסימלית, חשוב להגדיר את כללי השמירה בצורה מצומצמת. 

כדי להפיק את המרב מהאופטימיזציה, חשוב לפעול לפי השיטות המומלצות הבאות בקובץ כללי השמירה.

  • מסירים הגדרות גלובליות כמו -dontoptimize-dontshrink ו--dontobfuscate שמונעות מ-R8 לבצע אופטימיזציה של כל בסיס הקוד
  • מסירים כללי שמירה שמונעים אופטימיזציה של רכיבי Android כמו Activity, ‏ Services, ‏ Views או Broadcast receivers.
  • כדאי לשנות את כללי השמירה הרחבים שחלים על כל החבילה כך שיטרגטו רק מחלקות או מתודות ספציפיות. 

כדי לראות עוד שיטות מומלצות, אפשר לעיין במאמרי העזרה בנושא כללי השמירה.

שיטות מומלצות למפתחים של ספריות R8

אם אתם מפתחים ספריות, הקפידו למקם את הכללים שהצרכנים שלכם צריכים בקובץ consumer-rules file, ולשמור את כללי ההגנה הפנימיים של הספרייה בקובץ proguard-rules.pro. מידע נוסף על אופטימיזציה של ספריות זמין במאמר בנושא אופטימיזציה למפתחי ספריות.

R8 Configuration Analyzer

כדי לבדוק את האופטימיזציה של R8, משתמשים בכלי לניתוח הגדרות. בכלי Configuration analyzer מוצג הסטטוס הנוכחי של האופטימיזציה באמצעות הציונים Obfuscation,‏ Optimization ו-Shrinking. בעזרת כלי ניתוח ההגדרות אפשר גם להבין כמה מחלקות, שיטות או שדות נמנעים מאופטימיזציה על ידי כל כלל שמירה. כדי להשיג אופטימיזציה מקסימלית, כדאי לשפר את הכללים הרחבים האלה של שמירת חבילות. 

באמצעות הכלי לניתוח הגדרות, אפשר גם לזהות כללי שמירה שכוללים כללי שמירה אחרים, כללי שמירה מיותרים וכללי שמירה שלא נעשה בהם שימוש.

pic2-r8-config-analyzer.png
בכלי Configuration Analyzer מוצג המצב הנוכחי של האופטימיזציה עם ציוני ההסתרה, האופטימיזציה והכיווץ.

R8 Agent Skill 

אתם יכולים גם להשתמש במיומנות R8 Agent עם סוכן Android Studio או עם כלי AI אחרים כדי לפתור בעיות בהגדרות ולשפר את הכללים, וכך לשפר את ביצועי האפליקציה. (תובנות ממיומנויות מבוססות-AI ידרשו אימות טכני)

אופטימיזציה של טעינת תמונות

מפות סיביות הן בדרך כלל האובייקטים הנפוצים הגדולים ביותר ששוכנים בזיכרון של האפליקציה. הם מייצגים את השלב האחרון בתהליך טעינת התמונה, שבו קבצים דחוסים, כמו קובצי JPEG או PNG, מפוענחים לנתוני פיקסלים גולמיים לצורך הצגה. כלומר, תמונה דחוסה קטנה בגודל 100KB יכולה לתפוס כמה מגה-בייט של זיכרון RAM, כי צריכת הזיכרון נקבעת לפי ממדי הפיקסלים ועומק הצבע של התמונה. מכיוון שפעולות של מפת סיביות נמצאות לעיתים קרובות בנתיב הקריטי לציור מסגרות, תמונות לא אופטימליות גורמות לניפוח חמור של הזיכרון ולתנודות בממשק המשתמש.

‫Google ממליצה להשתמש בספריות לטעינת תמונות Coil לפרויקטים שמתבססים על Kotlin, במיוחד כשמפתחים באמצעות Jetpack Compose ו-Glide לאפליקציות שמבוססות על Java.

חמש שיטות מומלצות שכדאי ליישם

  1. הקטנת תמונות: אם אתם טוענים מפות סיביות באופן ידני, אל תטענו תמונה גדולה מאוד לתצוגה ממוזערת קטנה. השתמשו ב-inSampleSize כדי לטעון גרסה קטנה יותר. ב-Glide וב-Coil, התמונות עוברות דגימת חסר כברירת מחדל, ואפשר להגדיר את אסטרטגיית דגימת החסר הזו באמצעות DownsampleStrategy ו-ImageLoader בהתאמה.
  2. חיתוך: לא מומלץ להטמיע שוליים ישירות בקובץ תמונה כדי ליצור מראה של תיבת מכתבים (למשל, ליצור גבול שקוף כדי להרחיב את המידות של תמונה). במקום להטמיע את הגבולות האלה, כדאי להשתמש ב-InsetDrawable או להחיל ריווח פנימי ישירות ב-View או ב-Composable שמכילים את מפת הסיביות.
  3. הגדרה: בחירת פורמט הפיקסלים המתאים כדי לאזן בין הזיכרון לאיכות. משתמשים ב-RGB_565 כשלא צריך שקיפות, והפורמט הזה משתמש בחצי מהזיכרון של ברירת המחדל ARGB_8888. ב-Glide אפשר להגדיר את זה באמצעות DecodeFormat וב-Coil אפשר להשתמש במאפיין bitmapConfig.
  4. תעדוף של נכסי וקטור: לנכסים גיאומטריים בסיסיים, כדאי להשתמש ב-ShapeDrawable כחלופה קלה לפענוח של מפות סיביות (bitmap) שעברו המרה לרסטר. אם מגדירים את הנכסים האלה פעם אחת באמצעות XML, אפשר לוודא שהם יותאמו בצורה חלקה לכל צפיפות פיקסלים, וגם למנוע ביעילות את הניפוח של הזיכרון שנובע משימוש במשאבים.
  5. שימוש חוזר: אם האפליקציה מנהלת מפות סיביות באופן ידני, כדי לצמצם את התנודתיות בזיכרון, כשהאפליקציה כבר לא צריכה מפת סיביות, היא צריכה לקרוא ל-bitmap.recycle() ולבטל מיד את ההפניה ל-Bitmap. אם אתם משתמשים בספרייה לטעינת תמונות כמו Glide או Coil, מחזירים את מפת הסיביות למאגר המנוהל של הספרייה. המאגר מספק מאגר קיים לצרכים עתידיים של זיכרון, וכך נמנעות הוצאות תקורה של הקצאות חדשות.

מידע נוסף זמין במאמר בנושא אופטימיזציה של הביצועים של תמונות.

כלים ל-Android Studio

אפשר גם להסיר מפות סיביות מיותרות באמצעות Android Studio Narwhal 4. כך מאתרים אותם בחמישה שלבים פשוטים:

  1. פותחים את הכרטיסייה Profiler ב-Android Studio
  2. לוחצים על Heap Dump (או על Analyze Memory Usage) ועל record כדי לצלם תמונת מצב של מצב הזיכרון הנוכחי של האפליקציה.
  3. סורקים את תוצאות הניתוח כדי למצוא את משולש האזהרה הצהוב ⚠️, שמשמש את Android Studio לסימון מפות סיביות כפולות שמאוחסנות כמה פעמים. אפשרות אחרת היא לנווט לכותרת של הכלי ליצירת פרופילים, לבחור באפשרות 'סינון לפי:' ולבחור בהגדרה 'מפות סיביות כפולות'.
  4. לוחצים על רשומה מסומנת כדי לפתוח את החלונית תצוגה מקדימה של מפת סיביות, שבה אפשר לראות בדיוק איזו תמונה היא הבעייתית.
  5. אפשר להשתמש באישור החזותי הזה כדי לאתר את הלוגיקה המיותרת של הטעינה בקוד ולהטמיע אסטרטגיית שמירה במטמון טובה יותר.
pic3-IO26_113_TSV -dup-bitmaps-cropped.jpg
כשמשתמשים בכלי ליצירת פרופילים ב-Android Studio, כדאי לחפש את משולש האזהרה הצהוב ⚠️ ב-heap dumps.

זיהוי ותיקון של דליפות זיכרון באמצעות Android Studio

דליפות זיכרון ב-Android מתרחשות כשהקוד שומר על הפניה לאובייקט הרבה אחרי שמחזור החיים שלו הסתיים. כך נמנעת שחזור הזיכרון על ידי Garbage Collector ‏ (GC), ובסופו של דבר הביצועים יהיו איטיים או שתופיע שגיאת OutOfMemoryError ‏ (OOM).

ב-Android Studio Panda 3 יש משימת פרופיל ייעודית של LeakCanary, שמאפשרת למפתחים לנתח דליפות זיכרון בזמן אמת ולמפות עקבות ישירות בסביבת הפיתוח המשולבת (IDE).

המשימה של LeakCanary profiler ב-Android Studio מעבירה באופן פעיל את ניתוח דליפת הזיכרון מהמכשיר למכונת הפיתוח, וכתוצאה מכך יש שיפור משמעותי בביצועים במהלך שלב ניתוח הדליפה בהשוואה לניתוח דליפה במכשיר.

pic4-android-studio-leaks.png
 ניתוח דליפת זיכרון ב-LeakCanary עם מעבר להצהרה לצורך ניפוי באגים

בנוסף, ניתוח הדליפות מוצג עכשיו בהקשר של סביבת הפיתוח המשולבת (IDE) ומשולב באופן מלא עם קוד המקור. כך מתקבלות תכונות כמו מעבר להצהרה וקשרים מועילים אחרים בין קטעי קוד, שמפחיתים באופן משמעותי את החיכוך ואת הזמן שנדרשים כדי לחקור ולתקן דליפות זיכרון.  

דוגמאות נפוצות לדליפות זיכרון 

דליפות זיכרון מתרחשות כשאובייקט נשאר בזיכרון מעבר למשך החיים המיועד שלו. בדרך כלל זה קורה בגלל:

  • שמירה של הפניות לרכיבי Fragment,‏ Activity או View שכבר לא נמצאים בשימוש.
  • ניהול לא תקין של הפניות להקשרים.
  • אי ביטול הרישום של משתנים מסוג observer,‏ listener ו-receiver בצורה תקינה.
  • יצירת הפניות סטטיות לאובייקטים שמשויכים לרכיבים עם מחזורי חיים קצרים יותר.

הנה כמה תרחישים לדוגמה:

תרחישדוגמה שמבוססת על Compose דוגמה מבוססת-תצוגה
הדלפת הקשר

דוגמה:
העברת LocalContext.current אל ViewModel

תיקון:
השאירו לוגיקה שתלויה בהקשר בשכבת ממשק המשתמש. בשכבות שאינן ממשק משתמש, מבצעים רפקטורינג כדי להשתמש בהזרקת תלות או עוקבים אחרי מצב ממשק המשתמש באמצעות Kotlin flow.

דוגמה: 
אחסון Activity באובייקט נלווה או במשתנה סטטי.

תיקון:
אין להחזיק הפניות סטטיות לרכיבי ממשק משתמש. אפשר לבצע רפקטורינג כדי להשתמש בהזרקת תלות או לצפות במצב ממשק המשתמש באמצעות Kotlin flow.

Leaking Listeners

דוגמה:
שימוש ב-DisposableEffect כדי להפעיל מאזין, אבל השארת onDispose ריק.

תיקון:
ביצוע ביטול הרישום והלוגיקה של הניקוי בתוך הבלוק onDispose.

דוגמה:
הרשמה לעדכונים של SensorManager ושכחה לבטל את ההרשמה.

פתרון:
מתקשרים אל unregisterListener() באופן ידני במהלך מחזור החיים onStop() או onDestroy().

צפיות שדולפות

דוגמה:
החזקת הפניה ל-View מדור קודם בתוך AndroidView ללא אסטרטגיית הפצה.


פתרון:
משתמשים בבלוק release של הרכיב הקומפוזבילי AndroidView כדי לנקות את View מהדור הקודם.

דוגמה:
שמירת הפניה לאובייקט של view binding אחרי שה-Fragment נהרס.

 

תיקון:
מגדירים את משתנה הקישור ל-null בתוך השיטה של מחזור החיים onDestroyView().

חיתוך הזיכרון כשהאפליקציה יוצאת ממצב גלוי

מערכת Android יכולה לפנות זיכרון מהאפליקציה או להפסיק את פעולת האפליקציה לחלוטין אם יש צורך בכך כדי לפנות זיכרון למשימות קריטיות, כפי שמוסבר במאמר סקירה כללית של ניהול הזיכרון. בדרך כלל, מערכת Android תפנה זיכרון מהאפליקציה שלכם כשהיא לא גלויה למשתמש. למשל, על ידי ביטול של חלק מהקוד של האפליקציה ודפי הנתונים בזיכרון, או על ידי דחיסה של הקצאות הזיכרון שלכם. כשהמשתמש מפעיל מחדש את האפליקציה והאפליקציה מנסה לגשת לזיכרון שהמערכת פינתה, מערכת ההפעלה מחזירה את הזיכרון הזה לפי דרישה. ההתנהגות הזו של החלפה יכולה להיות איטית, ולגרום לבעיות בממשק (jank) לא צפויות או לגמגום באפליקציה.

אם תתנו למערכת ההפעלה להחליט איזה זיכרון לשחרר מהאפליקציה, יכול להיות שהמערכת תשחרר זיכרון שתצטרכו זמן קצר אחרי שתפעילו מחדש את האפליקציה. במקום זאת, האפליקציה יכולה לשחרר מרצונה הקצאות זיכרון שהיא יכולה ליצור מחדש מאוחר יותר, לפי דרישה ובעלות נמוכה. כדי לעשות זאת, אפשר להטמיע את הממשק ComponentCallbacks2. אפשר להטמיע את onTrimMemory ב-Activity, ב-Fragment, ב-Service או אפילו בכיתה מותאמת אישית Application. השימוש בו בכיתה Application יעיל מאוד לניהול מטמון גלובלי.

שיטת הקריאה החוזרת onTrimMemory() שסופקה מודיעה לאפליקציה על אירועים שקשורים למחזור החיים או לזיכרון, שמהווים הזדמנות טובה לאפליקציה להפחית את השימוש בזיכרון באופן יזום.

במונחים של ניהול מחזור החיים של הזיכרון, ההטמעה צריכה להתמקד באופן בלעדי ב-TRIM_MEMORY_UI_HIDDEN וב-TRIM_MEMORY_BACKGROUND. החל מ-Android 14, המערכת הפסיקה לשלוח התראות לגבי קבועים אחרים מדור קודם, שהוצאו משימוש באופן רשמי ב-Android 15.

TRIM_MEMORY_UI_HIDDEN: האות הזה מציין שהממשק של האפליקציה כבר לא מוצג למשתמש. כך אפשר לשחרר הקצאות משמעותיות של זיכרון שקשורות באופן הדוק לממשק – כמו מפות סיביות, מאגרי נתונים זמניים של הפעלת סרטונים או משאבי אנימציה מורכבים.

TRIM_MEMORY_BACKGROUND: ברמה הזו, התהליך פועל ברקע ועכשיו הוא מועמד לסיום כדי לענות על צורכי הזיכרון הגלובליים של המערכת. כדי להאריך את משך הזמן שהתהליך נשאר במצב שמור במטמון, וכדי לצמצם את מספר ההפעלות הקרות של האפליקציה, כדאי לשחרר באופן אגרסיבי את כל המשאבים שאפשר לשחזר בקלות ברגע שהמשתמש מחדש את הסשן.

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

הערה: יכול להיות שהשילוב onTrimMemory תלוי בתמיכה ב-SDK. לדוגמה, משחקים מסוימים מסתמכים על מנוע המשחק שלהם כדי להפעיל את היכולת הזו. מומלץ לעיין במסמכים בנושא אופטימיזציה של זיכרון המשחק.

התבוננות מתקדמת בזיכרון באמצעות ProfilingManager

כדי לזהות בעיות בזיכרון בשטח שלא ניתן לשחזר באופן מקומי, מומלץ להשתמש ב-ProfilingManager API. ‫API המתקדם הזה, שהושק ב-Android 15, מאפשר לכם לאסוף באופן פרוגרמטי פרופילים של Perfetto ממשתמשים אמיתיים. 

לצוותים שאין להם תשתית ייעודית לניהול ולאירוח של ארטיפקטים של ביצועים, ב-Crashlytics בודקים פתרון מיוחד כדי לייעל את תהליך העבודה הזה. הם מזמינים מפתחים לשלוח משוב.

ב-Android 17 הושקו טריגרים חדשים מבוססי-אירועים, בעיקר TRIGGER_TYPE_OOM ו-TRIGGER_TYPE_ANOMALY:

  • הטריגר OOM אוסף באופן אוטומטי תמונת מצב של הזיכרון של Java בדיוק ברגע שמתרחשת קריסה של OutOfMemoryError, ומספק מצבי הקצאה מדויקים. פרופיל OOM שנאסף מסופק בפעם הבאה שהאפליקציה מופעלת ורושמת את הקריאה החוזרת registerForAllProfilingResults.
  • הפעלת האנומליה מזהה בעיות חמורות בביצועים, כמו ספאם מוגזם ב-binder או חריגה מספי זיכרון. האנומליה בזיכרון מספקת dump של ה-heap ממש לפני שהמערכת מסיימת את האפליקציה.
    val profilingManager = 
applicationContext.getSystemService(ProfilingManager::class.java)
    val triggers = ArrayList<ProfilingTrigger>()  


    triggers.add(ProfilingTrigger.Builder(
                 ProfilingTrigger.TRIGGER_TYPE_ANOMALY))
    val mainExecutor: Executor = Executors.newSingleThreadExecutor()
    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode != ProfilingResult.ERROR_NONE) {
            // upload profile result to server for further analysis          
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } 

    profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

אחרי שאוספים את ה-heap dump, אפשר להוריד את הפרופיל מהשרת או באופן מקומי באמצעות adb pull, ולגרור את הקובץ אל ממשק המשתמש של Perfetto. כדי לייעל את תהליך העבודה של ניפוי באגים בזיכרון, אפשר להשתמש בכלי Heap Dump Explorer, שהוא תצוגת ברירת המחדל החדשה של קובצי Heap Dump בממשק המשתמש של Perfetto. הכלי הזה מספק ממשק אינטואיטיבי לבדיקת קובצי dump של ערימת Java, ומאפשר לכם לראות את ההיררכיות של הקצאת האובייקטים, לחשב את גדלי הזיכרון שנשמרו ולזהות את הנתיב הקצר ביותר משורש מנגנון איסוף הזבל. בעזרת הכלי Heap Dump Explorer, תוכלו לזהות במהירות דליפות זיכרון, אובייקטים שמורים מנופחים כמו הקצאות מוגזמות של מפות סיביות ולנתח הקצאות של אובייקטים ב-heap – והכול במקום אחד.

pic5-perfettoheapdump-analyzer.png
כדי לבדוק חזותית אובייקטים עם הקצאות הזיכרון הכי גבוהות ולנווט ביניהם, אפשר להשתמש בתרשים הלהבות המוטמע בכלי Heap Dump Explorer.

סיכום

אופטימיזציה של קוד בייט באמצעות R8, אימוץ שיטות מומלצות לטעינת תמונות ופתרון של דליפות זיכרון הם שלבים קריטיים ליצירת חוויית משתמש איכותית תוך ניהול יעיל של משאבים בתנאי עומס. השימוש באמצעים פרואקטיביים כאלה עוזר לשמור על היציבות והביצועים של האפליקציה, למנוע סגירות לא צפויות ולשמור על ההקשר של המשתמשים. כדי לשפר את הידע שלכם בנושא ביצועים, כדאי לעיין בהנחיות המעודכנות שלנו בנושא זיכרון.

נכתב על ידי:
להמשך קריאה