כדי לבצע אופטימיזציה יעילה של הזיכרון שבשימוש במשחק, קודם צריך להבין איך פלטפורמת Android מודדת את הזיכרון ואיך להשתמש בטלמטריה של המערכת, בממשקי API של אבחון ובכלי פרופילים. במדריך הזה מוסבר איך לעקוב אחרי הקצאות הזיכרון של המשחק, לתעד אותן ולנתח אותן בהתאם להנחיות החדשות של הפלטפורמה.
הסבר על מדדי RSS ו-swap
כדי לנתח ולנפות באגים ביעילות את התנהגות הזיכרון של המשחק, צריך להבין את המדדים הטכניים המדויקים שפלטפורמת Android משתמשת בהם לאכיפת הזיכרון. מידע מפורט על האופן שבו פרמטר הטלמטריה הזה מעובד ומנוטר בסביבת ייצור זמין במסמך Android Vitals – שימוש בזיכרון (anonymous RSS + swap).
1. RSS אנונימי (RssAnon)
גודל קבוצת התושבים (RSS) הוא מדד לחלק מהזיכרון שתהליך תופס, שנשמר בזיכרון ה-RAM הפיזי של המכשיר. ה-RSS מחולק לזיכרון מגובה בקובץ ולזיכרון אנונימי. מדד האכיפה של Android מתמקד אך ורק ב-RSS אנונימי:
- מה זה כולל: דפי זיכרון שהוקצו ישירות על ידי תהליך המשחק ולא מקושרים לקובץ פיזי באחסון. הדפים האלה כוללים ערימות של Java או Kotlin, מחסניות של ביצוע שרשורים, וחשוב מכך, הקצאות של זיכרון מקורי (כמו מקצים מותאמים אישית של מנוע C++, או בלוקים של זיכרון שהתבקשו באמצעות malloc או new מקוריים ושהפכו ללא נקיים על ידי לוגיקת המשחק). מידע נוסף על המדד הזה זמין במילון המונחים Process Memory (RSS).
- למה זה חשוב: מנועי משחקים משתמשים במאגרי זיכרון מקומיים גדולים כדי לטפל בפיזיקה, בעיבוד ובלוגיקה. המאגרים האלה לא מגובים בקבצים, ולכן הם נמצאים רק ב-RSS אנונימי ומהווים את רוב הנפח הפיזי של המשחק.
2. החלפה לא דחוסה (VmSwap)
Android לא תומך באזור החלפה מסורתי מבוסס-דיסק בגלל בלאי של אחסון פלאש ומגבלות של זמן אחזור. במקום זאת, הוא משתמש ב-zRAM (החלפה לא דחוסה):
- מה זה כולל: כשעומס ה-RAM הפיזי עולה, דמון ניהול הזיכרון של ליבת המערכת דוחס דפים אנונימיים לא פעילים ומעביר אותם לחלק ייעודי ולא דחוס של ה-RAM הפיזי (zRAM).
- חישוב המדד: המערכת עוקבת אחרי המדד הזה על סמך הגודל הלא דחוס (VmSwap) כדי להעריך את הביקוש בפועל לזיכרון הפיזי של המשחק. אם המשחק מקצה זיכרון והמערכת מחליפה אותו ל-zRAM, הוא עדיין נספר כחלק מ-Total Memory Footprint (טביעת הזיכרון הכוללת) של המשחק.
3. מצבי תהליך
השימוש בזיכרון מפורט לפי מצבי תהליך ב-Android Vitals. מפתחי משחקים צריכים לדעת שערכות SDK או משחקים של צד שלישי יכולים להפעיל שירותים שגלויים למשתמשים או שירותים שפועלים ברקע באופן לא צפוי.
- מה היא כוללת: שירותים שפועלים בחזית, שירותים שניתן להבחין בהם, שירותים שפועלים ברקע ושירותים שמאוחסנים במטמון.
- למה זה חשוב: למצבי תהליך שונים יש השפעות שונות על ניהול הזיכרון במערכת ההפעלה Android. יכול להיות שלא תדעו שהמשחק פועל עם מצב תהליך רגיש אם אחד מ-SDK של צד שלישי מפעיל משימה ברקע שלא בכוונה. אפשר לעקוב אחרי המשחק כדי לראות אם הוא פועל ברקע באמצעות
RunningAppProcessInfo.
ממשקי תכנות יישומים (APIs)
מערכת Android מספקת ממשקי API של המערכת שמאפשרים למשחק להגיב באופן דינמי ללחץ על הזיכרון ולתעד אבחון מפורט של הזיכרון בזמן הריצה.
תגובה לאירועים של צמצום הזיכרון
המערכת משתמשת ב-onTrimMemory כדי להודיע לאפליקציה על אירועים במחזור החיים שלה, שמהווים הזדמנות טובה לאפליקציה להפחית באופן יזום את השימוש שלה בזיכרון, וכך להימנע מהפסקת תהליכים על ידי הפסקת תהליכים בגלל מחסור בזיכרון (LMK) כדי לפנות זיכרון לשימוש של אפליקציות אחרות.
אם המערכת סוגרת את האפליקציה ברקע, המשתמש חווה הפעלה במצב התחלתי (cold start) איטית כשהוא חוזר לאפליקציה. הפחתת השימוש בזיכרון ברקע עוזרת למנוע את הסגירות האלה ברקע.
כשמגיבים לאירועי חיתוך, צריך לשחרר הקצאות גדולות של זיכרון שניתן לשחזור ושלא נדרשות באופן מיידי:
דוגמה: חיתוך או ניקוי של מפות סיביות שמאוחסנות במטמון (שפוענחו מאחסון מקומי) בתגובה ל-
TRIM_MEMORY_UI_HIDDEN.
Kotlin
class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
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.
}
}
}
Java
public class MainActivity extends AppCompatActivity implements ComponentCallbacks2 {
public void onTrimMemory(int level) {
switch (level) {
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.
}
}
}
}
ProfilingManager
ProfilingManager API, שהושק ב-Android 15 (רמת API 35), מאפשר לאפליקציות לצלם תמונות מצב שמוגדרות באופן פרוגרמטי (כמו פרופילים של ערימה, עקבות מערכת ו-Java heap dumps) ישירות בזמן ריצה.
מפתחים יכולים להפעיל צילומים באופן ידני בסצנות ספציפיות, או לרשום טריגרים אוטומטיים כמו TRIGGER_TYPE_ANOMALY כדי להפעיל צילום באופן אוטומטי כשתהליך המשחק חורג מספי הזיכרון שהוגדרו ב-Memory Limiter. עם זאת,
מפתחי משחקים צריכים לקחת בחשבון מגבלות קריטיות במנועי משחקים מודרניים:
הערה: מנועי משחקים מודרניים (כמו Unity או Unreal) מנהלים את ביצועי ההרצה על ידי הקצאה מראש של בלוקים גדולים של זיכרון וירטואלי מהליבה באמצעות mmap עם הדגל MAP_ANONYMOUS. לאחר מכן, המנועים משתמשים במקצים משנה מותאמים אישית (לדוגמה, מנהל הזיכרון המקורי של Unity או BinnedAllocators של Unreal) כדי לחלק ולהקצות בלוקים של זיכרון באופן פנימי.
ApplicationExitInfo
אם המשחק נסגר ברקע או נסגר בגלל חריגה ממגבלות הזיכרון של תהליך בודד, מנגנונים רגילים של Java או של קובץ dump של קריסה (כמו Firebase Crashlytics) לא יתעדו את האירוע. כדי לשלוח שאילתות לגבי סיום הפעלת המשחקים ולתעד אותם באופן פרוגרמטי, המפתחים צריכים להשתמש בממשק ApplicationExitInfo API כשמפעילים את המשחק.
- הטמעה: בתחילת ההפעלה, קוראים ל-
ActivityManager.getHistoricalProcessExitReasons()כדי לאחזר את סיבות היציאה של סשנים אחרונים. - סיבות עיקריות ליציאה מהזיכרון:
-
REASON_LOW_MEMORY: מציין שהתהליך הופסק על ידי Low Memory Killer (LMK) של המערכת. הסגירה הזו מתרחשת כשיש עומס גבוה על הזיכרון בכל המכשיר, ומערכת ההפעלה צריכה לפנות מקום ב-RAM. סיבת היציאה הזו מציינת שהשימוש ברקע של המשחק גדול מדי, ולכן הוא לא יכול לפעול במקביל לאפליקציות אחרות. -
REASON_MEMORY_LIMITER(Android 17 (רמת API 37) ומעלה): מציין שהתהליך הופסק באופן ספציפי כי הוא חרג ממגבלת הזיכרון של cgroup (RssAnon + VmSwap) שהוקצתה על ידי Memory Limiter של הפלטפורמה. הסיום הזה יכול לקרות גם אם נשאר מספיק זיכרון פיזי במכשיר, מה שמצביע על הפרה ישירה של מגבלות התהליך האישי.
-
שימוש בכלים הזמינים
כדי למדוד בצורה מדויקת את השימוש בזיכרון במשחק, כדאי להשתמש בכלים הבאים של הפלטפורמה במהלך הפיתוח ובדיקת האיכות.
meminfo
הכלי הזה אוסף נתונים סטטיסטיים על הזיכרון כדי להראות כמה זיכרון PSS הוקצה והקטגוריות שבהן נעשה בו שימוש.
אפשר להדפיס את נתוני הסטטיסטיקה של meminfo באחת מהדרכים הבאות:
- משתמשים בפקודה
adb shell dumpsys meminfo package-name. - משתמשים בקריאה
MemoryInfoמ-Android Debug API.
הנתון הסטטיסטי PrivateDirty מציג את כמות ה-RAM בתהליך שלא ניתן להעביר לדף בדיסק ושלא משותף עם תהליכים אחרים. רוב הסכום הזה הופך לזמין למערכת כשתהליך זה מופסק.
נקודות מעקב בזיכרון
נקודות מעקב של זיכרון עוקבות אחרי כמות הזיכרון של RSS שבה משתמש המשחק. חישוב השימוש בזיכרון של RSS מהיר בהרבה מחישוב השימוש בזיכרון של PSS. החישוב של RSS מהיר יותר, ולכן הוא מציג רמת פירוט גבוהה יותר של השינויים בגודל הזיכרון, כדי למדוד בצורה מדויקת יותר את השימוש המקסימלי בזיכרון. לכן קל יותר לזהות שיאים שעלולים לגרום למשחק להגיע למצב של אין זיכרון פנוי (OOM).
Perfetto
Perfetto הוא חבילת כלים לאיסוף מידע על הביצועים והזיכרון במכשיר ולהצגתו בממשק משתמש מבוסס-אינטרנט. הוא תומך במעקב אחרי נתונים לאורך זמן, כך שאפשר לראות איך RSS משתנה לאורך זמן. אפשר גם להריץ שאילתות SQL על הנתונים שהוא יוצר לצורך עיבוד אופליין. מפעילים מעקבים ארוכים מאפליקציית מעקב המערכת. מוודאים שהקטגוריה memory:Memory מופעלת למעקב. כדי לבצע מדידה מותאמת אישית של הזיכרון בפיתוח ובבדיקות, אפשר גם להשתמש ב-heapprofd API (בטא).
בדיקת RssAnon והחלפה ב-Perfetto
כדי לבדוק את ההשפעה של הזיכרון האנונימי וההחלפה של zRAM במשחק, טוענים את קובץ המעקב בממשק המשתמש מבוסס האינטרנט בכתובת ui.perfetto.dev ופועלים לפי טכניקות הניתוח הבאות, שנועדו למחקרים מעמיקים בנושא זיכרון (פרטים נוספים זמינים במאמר Perfetto Memory Analysis Case Studies):
1. המחשה של מוני זיכרון בציר הזמן
- מאתרים את התהליך: ברשימת הניווט, מחפשים את שם החבילה או שם התהליך של המשחק.
- הרחבת קבוצת טראקים: לוחצים על שורת התהליך כדי להרחיב את טראקי השרשור שלה, ומאתרים את קבוצת המשנה שנקראת 'זיכרון'.
- מנתחים את הטראקים:
- mem.rss.anon (RSS אנונימי): בתרשים הקו הזה מוצג זיכרון ה-RAM הפיזי בזמן אמת שתפוס על ידי מאגרי הזיכרון הלא מנוהלים של המשחק. כדאי לעקוב אחרי ציר הזמן הזה במהלך טעינת סצנות, פריטים קופצים בממשק המשתמש או מעברים ב-gameplay כדי לבדוק אם יש שיאים גבוהים בהקצאה.
- mem.swap (Compressed Swap או VmSwap): בתרשים הזה מוצג הגודל לפני הדחיסה של בלוקים של זיכרון שהועברו ל-zRAM. פעילות גבוהה של החלפה שמתרחשת במהלך משחק מצביעה על כך שהמשחק פועל במכשיר עם זיכרון מוגבל, והמערכת דוחסת באופן פעיל נכסים ברקע.
2. הרצת שאילתות SQL (מעבד עקבות) כדי לבצע ניתוח מפורט אופליין, אפשר להריץ שאילתות SQL ישירות במסוף של ממשק המשתמש של Perfetto או להשתמש בספריית Python העצמאית Trace Processor כדי לחשב שיאים סטטיסטיים.
מאתרים את ההקצאה המקסימלית של RSS אנונימי:
SELECT max(value) / 1024 / 1024 AS max_rss_anon_mb FROM counter JOIN counter_track ON counter.track_id = counter_track.id WHERE counter_track.name = 'mem.rss.anon' AND counter_track.upid IN ( SELECT upid FROM process WHERE name = 'your.game.package.name' );התאמה בין RssAnon לבין VmSwap בחותמת זמן נתונה:
SELECT ts, track.name AS metric_type, value / 1024 / 1024 AS size_mb FROM counter JOIN counter_track track ON counter.track_id = track.id WHERE (track.name = 'mem.rss.anon' OR track.name = 'mem.swap') AND track.upid IN ( SELECT upid FROM process WHERE name = 'your.game.package.name' ) ORDER BY ts ASC;
פרטים נוספים על בדיקת קובצי מעקב באמצעות Android Studio זמינים במאמר בדיקת מעקבי מערכת: זיכרון תהליך (RSS). פרטים על פרופילים של זיכרון סקריפטים זמינים במאמר תיעוד הקצאות מקומיות.
heapprofd
heapprofd הוא כלי למעקב אחרי זיכרון שכלול ב-Perfetto. הכלי הזה יכול לעזור לכם למצוא דליפות זיכרון. הוא מראה איפה הוקצה זיכרון באמצעות malloc. אפשר להפעיל את heapprofd באמצעות סקריפט Python, ומכיוון שהתקורה של הכלי נמוכה, הוא לא משפיע על הביצועים כמו כלים אחרים כמו Malloc Debug.
דוח על באג
bugreport הוא כלי לרישום ביומן שמאפשר לגלות אם המשחק קרס בגלל אין זיכרון פנוי (OOM). הפלט של הכלי מפורט הרבה יותר מאשר הפלט של logcat. הוא שימושי לניפוי באגים בזיכרון כי הוא מראה אם המשחק קרס בגלל שנגמר לו הזיכרון או אם הוא הופסק על ידי ה-LMK.
מידע נוסף זמין במאמר איך שולחים דוחות על באגים וקוראים אותם.
כלים של מנוע משחק
יומנים וטלמטריה ברמת הפלטפורמה חיוניים למעקב אחרי ספי חסימה ותאימות של מערכת ההפעלה, אבל כלים ספציפיים למנוע המשחק עוזרים לכם לשייך הקצאות ישירות לאובייקטים של המשחק, להתנהגויות של סקריפטים ולהיררכיות פעילות של סצנות.
Unity
בסביבת Unity Engine, אפשר להעריך בצורה מדויקת את הזיכרון שבשימוש של Android Anonymous RSS + Swap בזמן ריצה עם מהימנות גבוהה (בדרך כלל מוצג שונות של פחות מ-10% בהשוואה לערכים אמיתיים ברמת מערכת ההפעלה) באמצעות כלי הפרופילים והמחלקות המקוריים של Unity.
מדריך מפורט, כולל כללי הגדרה וסקריפטים של זמן ריצה, זמין במאמר איך בודקים את הזיכרון באמצעות כלי Unity.
- Unity profiler API: אתם יכולים להעריך באופן פרוגרמטי את הזיכרון שבשימוש הלא מנוהל של המשחק בזמן הריצה על ידי שליחת שאילתה למדדי מנוע הליבה:
- באמצעות המחלקה Profiler: כדי לעקוב אחרי הקצאות הזיכרון הכוללות, מסכמים את הערכים של
Profiler.GetTotalReservedMemoryLong()ושלProfiler.GetMonoHeapSizeLong(). - שימוש במחלקה
ProfilerRecorder: מעקב דינמי אחרי קטגוריות של זיכרון. כדי ליצור קירוב מהימן של Baseline, צריך לאחזר את Total Reserved Memory (ב-Release builds) או להחסיר ממנו את Gfx Reserved Memory (ב-Development builds) כדי להסיר רכיבים של זיכרון ה-GPU שמוגדרים על ידי קבצים.
- באמצעות המחלקה Profiler: כדי לעקוב אחרי הקצאות הזיכרון הכוללות, מסכמים את הערכים של
- Unity memory profiler: כדי לזהות ולנפות באגים בדליפות זיכרון במצב אופליין, מצלמים תמונת מצב של הזיכרון ובודקים את התרשים Resident Memory on Device (זיכרון תושב במכשיר) שנמצא בקטע All of Memory (כל הזיכרון). כדי לחשב את טביעת הרגל המשוערת, מחברים את הסכומים של הקטגוריות הבאות: Untracked, Android Runtime, Native ו-Managed.
- מגבלת zRAM: בתנאי זיכרון מוגבלים, ליבת Android יכולה לדחוס דפי זיכרון לא פעילים לתוך מרחב החלפה (zRAM). מכיוון שכלי ה-Memory Profiler של הזיכרון ב-Unity לא יכול לזהות פרמטרים של החלפה ברמת מערכת ההפעלה, יכול להיות שתראו הבדלים קלים בטביעת הרגל של הזיכרון בסצנות שדורשות הרבה זיכרון. כדי לוודא מהם הערכים המדויקים, אפשר להשוות את ההערכות עם נתוני Perfetto.