אחרי שמבינים איך מתבצע ניהול הזיכרון ב-Android ומגדירים כלים למדידת השימוש בזיכרון במשחק, השלב הבא הוא לצמצם את השימוש בזיכרון ולבצע אופטימיזציה שלו. הקפדה על המגבלות המחמירות של Android עוזרת למנוע את סגירת המשחק על ידי המערכת, מקצרת את זמני ההפעלה הארוכים ומבטיחה שהמשחק יפעל בצורה טובה בכל המכשירים.
מדריך זה מספק טכניקות מעשיות לצמצום הזיכרון שבשימוש של המשחק, עם התמקדות ספציפית באופטימיזציה ברמת הנכס, בהגדרות ספציפיות למנוע ובשיטות מומלצות לניהול הזיכרון.
הפחתת נפח הזיכרון ב-Unity
בגלל העיצוב הארכיטקטוני של Unity, אחרי שהמנוע מרחיב את מקצי הבלוקים המקוריים ואת ה-heap המנוהל, הוא נוטה לשמור את דפי הזיכרון האלה לשימוש חוזר במקום להחזיר אותם מיד למערכת ההפעלה (OS), גם אחרי שמשחררים נכסים. באופן ספציפי, מרחב הכתובות הווירטואלי (הזיכרון השמור) נשאר שמור למשך חיי התהליך, והזיכרון הפיזי (RSS) לא משוחרר מיד עד שמתרחשים כמה מחזורים של מנגנון איסוף זבל (GC) וגיזום. לכן, שיא זמני בזיכרון יכול לגרום לזיכרון התושב להישאר מנופח למשך זמן רב גם אחרי שהשימוש בפועל יורד. התנהגות כזו מגדילה את הסיכון לקריסות בגלל חוסר זיכרון (OOM) במכשירים ברמת כניסה, ופוגעת ביציבות הכוללת של זמן הריצה.
לכן, כדי לבצע אופטימיזציה של הזיכרון ב-Unity, צריך להתייחס לשלושה עקרונות מרכזיים שמותאמים להתנהגויות האלה של המנוע:
- כדי למנוע עליות חדות בשימוש בזיכרון, כדאי לשלוט בשימוש בזיכרון הפעיל.
- כדי למנוע יצירה של נכסים ומשאבים מיותרים, צריך לנהל את פורמטים של טקסטורות ואת הווריאציות של הצללות.
- שינוי מבנה של קוד זמן הריצה כדי לבטל הקצאות מיותרות ב-heap המנוהל, וכך לצמצם את תדירות ה-GC ואת הרחבת ה-heap.
מידע נוסף זמין במאמר בנושא אופטימיזציה של הזיכרון ב-Unity.
הפחתת השימוש בזיכרון ב-Unreal Engine
ב-Unreal Engine, צינורות עיבוד (rendering) באיכות גבוהה וגרפים מורכבים של תלות באובייקטים יכולים להגדיל באופן משמעותי את ה-RSS האנונימי ואת הלחץ על הזיכרון שנתמך על ידי קבצים. בפרט, הסתמכות על הפניות קשיחות והיררכיות עמוקות של ירושת תוכניות Blueprint גורמת לטעינה של נכסים מחוברים שלא נמצאים בשימוש לזיכרון. בנוסף, פרמוטציות מוגזמות של תוכנות הצללה (shader), מאגרי סטרימינג של טקסטורות לא ממוטבות וטבלאות מיקום מחדש של ELF לא דחוסות תורמים לשימוש גבוה בזיכרון הבסיסי, ומגדילים את הסיכון לסגירה על ידי Low Memory Killer (LMK).
לכן, כדי לבצע אופטימיזציה של הזיכרון ב-Unreal Engine, צריך להתייחס לשלושה עקרונות מרכזיים שמותאמים להתנהגויות האלה של המנוע:
- מפרידים בין הנתונים לבין הלוגיקה, ומחליפים הפניות קשיחות או חזקות בהפניות רכות או חלשות.
- כדי למזער את מספר האובייקטים של מצב צינור העיבוד (PSO) ואת יעדי הרינדור המיותרים, כדאי להסיר תכונות לא בשימוש של תאורה בנייד ואפשרויות פרמוטציה, תוך שימוש בדחיסת ASTC והתאמה אישית של מאגרי סטרימינג של טקסטורות באמצעות פרופילי מכשירים.
- כדי להקטין את הגודל של קובץ ה-ELF הבינארי ולצמצם את הזיכרון שבשימוש בזמן הריצה, מפעילים את הדחיסה של טבלת המיקום מחדש של RELR ו-APS.
מידע נוסף זמין במאמר בנושא אופטימיזציה של הזיכרון ב-Unreal.
אופטימיזציה של ריבוי תהליכים
השימוש בזיכרון של תהליכים שנשמרו במטמון לא נכלל בחישוב של מגבלות הזיכרון, כי הוא לא משפיע על אפליקציות פעילות. הפעלת השירות בתהליך נפרד ומבודד עוזרת לתהליך הראשי לעבור למצב מטמון מהר ככל האפשר, וכך לשפר את תקינות המשחק.
מידע נוסף זמין במאמרים איך עוקבים אחרי מצב התהליך והזיכרון, איך מבודדים תהליך של שירות באמצעות Unity ואיך מבודדים תהליך של שירות באמצעות Unreal.
הפחתת השימוש בזיכרון בשירותים שהשפיעו על המשתמשים
יכול להיות שהמשחק שלכם יצטרך להריץ לוגיקה בשירות שנתפס על ידי המשתמש, למקרים כמו השלמת הורדה גדולה או למערכות צ'אט קולי ברקע. האסטרטגיות האלה יכולות לעזור לכם לנהל את השימוש בזיכרון ולהפחית אותו בתרחישים האלה.
אסטרטגיות להורדות גדולות
האסטרטגיות האלה יכולות להתאים להורדות גדולות שרוצים להמשיך גם אחרי שהמשתמש מצמצם את המשחק.
1. בידוד תהליך ההורדה
מה: כדי לוודא שמערכת ההפעלה יכולה לשחרר באופן מיידי את הזיכרון שהאפליקציה לא משתמשת בו, צריך לבצע את ההורדה בתהליך נפרד. הסיבה לכך היא שאולי מקצה הזיכרון ישמור דפים של מאגר זיכרון וישאיר את הזיכרון של Anonymous RSS גבוה באופן מלאכותי גם אחרי שחרור מערכים. כשמפסיקים או יוצאים מהשירות ומהתהליך באופן מפורש, הזיכרון מוחזר למאגר של מערכת ההפעלה והתהליך הראשי לא מושפע.
ב-Unity: מעבירים את ההורדה ל-Android מקורי
Serviceשהוגדר באמצעות תהליך כמוandroid:process=":downloader"במניפסט מותאם אישית, ומפעילים אותו באמצעותAndroidJavaClassJNI של Unity. חשוב להפסיק את התהליך כשההורדה מסתיימת. הנחיות מפורטות זמינות במאמר בנושא הפעלת שירות מורגש בתהליך נפרד באמצעות Unity.ב-Unreal: מצהירים על Android
Serviceבהתאמה אישית באמצעות תהליך כמוandroid:process=":downloader"באמצעות Unreal Plugin Language, ומפעילים אותו באמצעות C++ JNI. חשוב להפסיק את התהליך כשההורדה מסתיימת. הנחיות מפורטות יותר זמינות במאמר הפעלת שירות מורגש בתהליך נפרד באמצעות Unreal.ב-Android מקורי: צריך להצהיר על
Serviceב-AndroidManifestבתהליך כמוandroid:process=":downloader". מריצים את ההורדה בתהליך המבודד הזה, וקוראים ל-Process.killProcess(Process.myPid())כשההורדה מסתיימת.
איך זה עוזר: זה מקצר את משך הזמן שבו הזיכרון מוחזק, ומאפשר להורדה להמשיך תוך שחרור הזיכרון שבו נעשה שימוש בתהליך הראשי הגדול יותר.
2. הורדה בסטרימינג ישירות לדיסק
מה: הזרמת נתונים ישירות משקע הרשת לדיסק באמצעות מאגר קטן וקבוע לשימוש חוזר, במקום לצבור תגובות מהרשת במערך גדול לפני הכתיבה.
ב-Unity: אל תשתמשו ב-
DownloadHandlerBufferבשביל חבילות נכסים או קבצים גדולים, כי הפונקציה הזו מקצה מאגר זיכרון מקומי ששווה לגודל הקובץ (זיכרון RSS אנונימי). במקום זאת, אפשר להשתמש ב-DownloadHandlerFileכדי להזרים בייטים באופן מקורי לדיסק בשרשור ברקע.ב-Unreal: מעבירים נתחים של נתונים נכנסים ישירות אל
FArchive(ארכיון מגובה בקובץ באמצעות הכלי לניהול קבצים של Unreal) עםSetResponseBodyReceiveStream()במקום לצרף מטען ייעודי (payload) מ-IHttpRequestאלTArray<uint8>.ב-Android Native: מעבירים את
InputStreamל-FileOutputStreamבאמצעות מאגר משותף במקום להפעיל את.readBytes()או את.string()בתגובת HTTP.
איך זה עוזר: מצמצם את השימוש בזיכרון בשיא
3. העברת קובץ דחוס שהורד בסטרימינג
מה: אם ההורדה דחוסה, עוטפים את זרם הקלט של הרשת במפענח סטרימינג כמו ZipInputStream במקום להוריד את הקובץ, לטעון אותו ל-RAM ואז לחלץ אותו.
איך זה עוזר: מצמצם את השימוש בזיכרון בשיא
4. האצלת סמכות למערכת ההפעלה
מה: כדי להימנע מניהול זיכרון ברקע, אפשר להעביר את העבודה לממשקי API מקוריים של Android.
WorkManagerהוא wrapper מודרני ומומלץ שלJobSchedulerברמת מערכת ההפעלה. ב-Android 14 ואילך,WorkManagerמטפל באופן אוטומטי בהורדות שהמשתמשים מפעילים כמשימה של העברת נתונים ביוזמת המשתמשים (UIDT). הפעולה הזו מתבצעת בתהליך של האפליקציה, ולכן עדיין צריך להזרים נתונים ישירות לדיסק כדי לצמצם את השימוש בזיכרון. ה-UIDT מגן על האפליקציה מפני קריסות שנגרמות בגלל זיכרון נמוך, כי הוא מאפשר למערכת ההפעלה להשהות את ההורדה ולהמשיך אותה בצורה חלקה אם משאבי המערכת מוגבלים.
DownloadManagerפועל בתהליך מערכת נפרד ולא משייך את השימוש בזיכרון להורדה לאפליקציה שלכם. האפליקציה תקבל התראה על שידור כשהקובץ יורד ויהיה מוכן.
איך זה עוזר: WorkManager עוזר להתמודד עם תרחישים של זיכרון נמוך, ו-DownloadManager מקטין את השימוש בזיכרון של האפליקציה.
אסטרטגיות לשירותים נלווים
האסטרטגיות האלה עשויות לחול על שירותים משניים שמופעלים במשחק במקביל לתהליך המשחק הראשי, כמו צ'אט קולי ברקע.
1. בידוד התהליך
מה: הפרדת התכונה, למשל פתרון הצ'אט הקולי, ממנוע המשחק הראשי. לדוגמה, אפשר להפעיל את לכידת המיקרופון ואת הסטרימינג ברשת בתוך שירות שפועל בחזית של Android שהוקצה לתהליך נפרד (מוצהר במניפסט, למשל באמצעות android:process=":voice").
איך זה עוזר: כשהאפליקציה ממוזערת, התהליך הראשי הכבד של המשחק יכול לעבור למצב מטמון עם עדיפות נמוכה יותר, בזמן שהשירות העזר הקל יותר ממשיך במצב שירות שהמשתמש יכול לראות.
2. קיצוץ זיכרון לא בשימוש בתהליך
מה: אם שירות העזר משולב עמוק מדי במנוע המשחק ואי אפשר להפריד אותו, נסו להפחית כמה שיותר את העומס על התהליך ברגע שהמשחק עובר לרקע או ממוזער. כדאי לשקול לרוקן מידע ממטמון הטקסטורה, הסרת הנתונים שנטענו של סצנות לא חיוניות, להוריד את קצב הטיקים של המנוע ואת שיעור הנכסים שעובדו ל-0, ולקרוא באופן מפורש למנגנון איסוף זבל.
ב-Unity: חיתוך מתבצע כשמופעל
OnApplicationPause(). אולי המידע ב-Resources.UnloadUnusedAssets()יעזור לך.ב-Unreal: מקשרים את הלוגיקה של החיתוך לנציג
ApplicationWillEnterBackgroundDelegate.ב-Android מקורי: מבצעים חיתוך ב-
onPause()או ב-onStop()לפי הצורך. יכול להיות שמערכת ההפעלה תנסה לקרוא להטמעה שלonTrimMemory()לפני שתפסיק תהליכים באמצעות זיכרון גבוה.
איך זה עוזר: מצמצם את השימוש בזיכרון שלא נחוץ כשהמשחק לא פועל ברקע.