ניהול יעיל של הזיכרון במשחקים

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

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

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

תגובה ל-onTrimMemory()‎

המערכת משתמשת ב-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.
            }
        }
    }
}

הגדרת תקציבי זיכרון שמרניים

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

  • גודל זיכרון ה-RAM הפיזי: משחקים משתמשים בדרך כלל ב-¼ עד ½ מכמות זיכרון ה-RAM הפיזי במכשיר.
  • הגודל המקסימלי של zRAM: ככל שיש יותר zRAM, כך יש למשחק יותר זיכרון להקצאה. הכמות הזו משתנה בהתאם למכשיר. כדי למצוא את הערך הזה, מחפשים את SwapTotal ב-/proc/meminfo.
  • שימוש בזיכרון של מערכת ההפעלה: במכשירים שבהם מוקצה יותר זיכרון RAM לתהליכי המערכת, נשאר פחות זיכרון למשחק. המערכת מפסיקה את התהליך של המשחק לפני שהיא מפסיקה את התהליכים של המערכת.
  • שימוש בזיכרון של אפליקציות מותקנות: כדאי לבדוק את המשחק במכשירים שמותקנות בהם הרבה אפליקציות. אפליקציות של רשתות חברתיות וצ'אטים צריכות לפעול כל הזמן, והן משפיעות על כמות הזיכרון הפנוי.

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

איך להימנע משימוש יתר בדיסק

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

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

שימוש בכלים הזמינים

ב-Android יש אוסף של כלים שעוזרים להבין איך המערכת מנהלת את הזיכרון.

Meminfo

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

מדפיסים את הנתונים הסטטיסטיים של meminfo באחת מהדרכים הבאות:

  • משתמשים בפקודה adb shell dumpsys meminfo package-name.
  • משתמשים בקריאה MemoryInfo מ-Android Debug API.

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

נקודות מעקב בזיכרון

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

‫Perfetto ומעקבים ארוכים

Perfetto הוא חבילת כלים לאיסוף מידע על הביצועים והזיכרון במכשיר ולהצגתו בממשק משתמש מבוסס-אינטרנט. הוא תומך במעקב ארוך ככל שנדרש, כך שאפשר לראות איך ה-RSS משתנה לאורך זמן. אפשר גם להריץ שאילתות SQL על הנתונים שהוא יוצר כדי לעבד אותם אופליין. מפעילים עקבות ארוכים מאפליקציית מעקב המערכת. מוודאים שהקטגוריה memory:Memory מופעלת למעקב. כדי לבצע מדידה מותאמת אישית של הזיכרון בפיתוח ובבדיקות, אפשר גם להשתמש ב-heapprofd API (בגרסת בטא).

heapprofd

heapprofd הוא כלי למעקב אחרי זיכרון, והוא חלק מ-Perfetto. הכלי הזה יכול לעזור לכם למצוא דליפות זיכרון. הוא מציג את המקומות שבהם הוקצה זיכרון באמצעות malloc. אפשר להפעיל את heapprofd באמצעות סקריפט Python, ומכיוון שהתקורה של הכלי נמוכה, הוא לא משפיע על הביצועים כמו כלים אחרים כמו Malloc Debug.

דוח על באג

bugreport הוא כלי לרישום ביומן שמאפשר לגלות אם המשחק שלכם קרס כי נגמר לו הזיכרון. הפלט של הכלי מפורט הרבה יותר מזה של logcat. הוא שימושי לניפוי באגים בזיכרון כי הוא מראה אם המשחק קרס כי נגמר לו הזיכרון או אם הוא הופסק על ידי LMK.

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