כדי לבצע אופטימיזציה יעילה של הזיכרון שבשימוש במשחק, קודם צריך להבין איך פלטפורמת Android מודדת את הזיכרון ואיך להשתמש בטלמטריה של המערכת, בממשקי API של אבחון ובכלי פרופילים. במדריך הזה מוסבר איך לעקוב אחרי הקצאות הזיכרון של המשחק, לתעד אותן ולנתח אותן בהתאם להנחיות החדשות של הפלטפורמה.
הסבר על מדדי RSS ו-swap
כדי לנתח ולנפות באגים ביעילות בהתנהגות הזיכרון של המשחק, צריך להבין את המדדים הטכניים המדויקים שפלטפורמת Android משתמשת בהם לניהול הזיכרון. מידע מפורט על האופן שבו פרמטר הטלמטריה הזה מעובד ומנוטר בסביבת ייצור זמין במסמך Android Vitals – שימוש בזיכרון (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) לא יתעדו את האירוע. כדי לשלוח שאילתות לגבי סיום הפעלת המשחקים ולתעד אותם באופן פרוגרמטי, המפתחים צריכים להשתמש ב-API ApplicationExitInfo כשמפעילים את המשחק.
- הטמעה: בתחילת ההפעלה, קוראים ל-
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.
לא אמיתי
בסביבת Unreal Engine, אפשר להעריך את הזיכרון שבשימוש של המשחק על ידי שילוב של אבחון המנוע עם טלמטריה של הפלטפורמה. הוראות מפורטות ותהליכי עבודה של פרופילים זמינים במאמר בדיקת השימוש בזיכרון באמצעות Unreal Engine.
בין הממשקים והכלים העיקריים לאבחון:
- C++ Diagnostics API: משתמשים ב-
GetMemoryUsedFastלשאילתות קלות משקל לגבי זיכרון ובממשקGetStatsלנתונים סטטיסטיים לגבי זיכרון ברמת החומרה. - פקודות במסוף: אפשר לעקוב אחרי מגמות של הקצאת זיכרון בזמן אמת בחומרה של המכשיר באמצעות פקודות המנוע
stat unitו-stat unitmax. - Unreal Insights: בדיקת צילומים של ציר זמן ברמת דיוק של פריים כדי לנתח מדדים ברמת הפלטפורמה ומונים מותאמים אישית של זיכרון.