פתרון בעיות ידועות במשחק Unity הוא תהליך שיטתי:
איך מקבלים תמונת מצב של הזיכרון
כדי לקבל תמונת מצב של הזיכרון שמנוהל על ידי Unity, משתמשים ב-Unity Profiler. באיור 2 מוצגות שכבות ניהול הזיכרון שבהן Unity משתמש כדי לנהל את הזיכרון במשחק.
זיכרון מנוהל
ב-Unity, ניהול הזיכרון מבוסס על שכבת זיכרון מבוקרת שמשתמשת בערימה מנוהלת ובאיסוף אשפה כדי להקצות זיכרון באופן אוטומטי. מערכת הזיכרון המנוהל היא סביבת סקריפטים של C# שמבוססת על Mono או על IL2CPP. היתרון של מערכת הזיכרון המנוהלת הוא שהיא משתמשת באיסוף אשפה כדי לפנות הקצאות זיכרון באופן אוטומטי.
זיכרון לא מנוהל ב-C#
שכבת הזיכרון הלא מנוהלת של C# מספקת גישה לשכבת הזיכרון המקורית, ומאפשרת שליטה מדויקת בהקצאות זיכרון בזמן השימוש בקוד C#. אפשר לגשת לשכבת ניהול הזיכרון דרך מרחב השמות Unity.Collections ודרך פונקציות כמו UnsafeUtility.Malloc ו-UnsafeUtility.Free.
זיכרון מקומי
ליבת C/C++ הפנימית של Unity משתמשת במערכת זיכרון מקורית כדי לנהל סצנות, נכסים, ממשקי API של גרפיקה, דרייברים, מערכות משנה ומאגרי מידע של תוספים. הגישה הישירה מוגבלת, אבל אפשר לבצע מניפולציות בנתונים בצורה בטוחה באמצעות C# API של Unity וליהנות מקוד Native יעיל. בדרך כלל אין צורך באינטראקציה ישירה עם הזיכרון המקורי, אבל אפשר לעקוב אחרי ההשפעה של הזיכרון המקורי על הביצועים באמצעות כלי הפרופיל, ולשנות את ההגדרות כדי לשפר את הביצועים.
הזיכרון לא משותף בין C# לבין קוד Native, כמו שמוצג באיור 3. הנתונים שנדרשים על ידי C# מוקצים במרחב הזיכרון המנוהל בכל פעם שנדרשים.
כדי שהקוד של המשחק המנוהל (C#) יוכל לגשת לנתוני הזיכרון המקוריים של המנוע, למשל, קריאה ל-GameObject.transform מבצעת קריאה מקורית כדי לגשת לנתוני הזיכרון באזור המקורי, ואז מחזירה ערכים ל-C# באמצעות Bindings. הקישורים מבטיחים מוסכמות קריאה נכונות לכל פלטפורמה ומטפלים בהמרת סוגים מנוהלים לאקוויוולנטים המקוריים שלהם באופן אוטומטי.
זה קורה רק בפעם הראשונה, כי מעטפת מנוהלת לגישה למאפיין transform נשמרת בקוד Native. שמירת המאפיין transform במטמון יכולה להפחית את מספר הקריאות הלוך ושוב בין קוד מנוהל לקוד Native, אבל יעילות השמירה במטמון תלויה בתדירות השימוש במאפיין. בנוסף, חשוב לזכור ש-Unity לא מעתיקה חלקים מזיכרון מקורי לזיכרון מנוהל כשניגשים לממשקי ה-API האלה.
מידע נוסף זמין במאמר מבוא לזיכרון ב-Unity.
בנוסף, חשוב להגדיר תקציב זיכרון כדי שהמשחק יפעל בצורה חלקה, ולהטמיע מערכת ניתוח או דיווח של צריכת הזיכרון כדי לוודא שכל גרסה חדשה לא חורגת מתקציב הזיכרון. אסטרטגיה נוספת לקבלת תובנות טובות יותר היא שילוב של בדיקות של מצב הפעלה ב-Play עם שילוב רציף (CI) כדי לאמת את צריכת הזיכרון באזורים ספציפיים במשחק.
ניהול הנכסים
זהו החלק הכי משמעותי והכי פרקטי בצריכת הזיכרון. פרופיל בהקדם האפשרי.
השימוש בזיכרון במשחקי Android יכול להשתנות באופן משמעותי בהתאם לסוג המשחק, למספר הנכסים ולסוגים שלהם ולאסטרטגיות האופטימיזציה של הזיכרון. עם זאת, בין הגורמים הנפוצים שמשפיעים על השימוש בזיכרון אפשר למנות בדרך כלל טקסטורות, רשתות, קובצי אודיו, הצללות, אנימציות ותסריטים.
זיהוי נכסים דיגיטליים כפולים
השלב הראשון הוא לזהות נכסים שההגדרה שלהם לא טובה ונכסים כפולים באמצעות כלי פרופיל הזיכרון, כלי דוחות בנייה או Project Auditor.
מרקמים
מנתחים את התמיכה במכשירים במשחק ומחליטים על פורמט הטקסטורה הנכון. אפשר לפצל את חבילות הטקסטורה למכשירים מתקדמים ולמכשירים פשוטים יותר באמצעות Play Asset Delivery, Addressable או תהליך ידני יותר עם AssetBundle.
כדאי לפעול לפי ההמלצות המוכרות ביותר שזמינות במאמר שיפור הביצועים של משחקים לנייד ובפוסט לדיון בנושא אופטימיזציה של הגדרות ייבוא טקסטורות ב-Unity. אחר כך נסו את הפתרונות הבאים:
כדאי לדחוס טקסטורות באמצעות פורמטים של ASTC כדי לצמצם את השימוש בזיכרון, ולנסות להשתמש בשיעור בלוקים גבוה יותר, כמו 8x8.
אם נדרש שימוש ב-ETC2, צריך לארוז את הטקסטורות ב-Atlas. הצבת כמה טקסטורות בטקסטורה אחת מבטיחה שהיא תהיה חזקה פי שניים (POT), יכולה לצמצם את קריאות הציור ויכולה להאיץ את העיבוד.
אופטימיזציה של הפורמט והגודל של הטקסטורה RenderTarget. מומלץ להימנע משימוש בטקסטורות ברזולוציה גבוהה שלא לצורך. שימוש בטקסטורות קטנות יותר במכשירים ניידים חוסך זיכרון.
כדי לחסוך בזיכרון של הטקסטורה, משתמשים בTexture channel packing.
רשתות ומודלים
מתחילים בבדיקת ההגדרות הבסיסיות (עמוד 27) ומוודאים את ההגדרות הבאות של ייבוא רשת:
- מיזוג של רשתות מיותרות וקטנות יותר.
- צמצום מספר הקודקודים של אובייקטים בסצנות (לדוגמה, אובייקטים סטטיים או רחוקים).
- יצירת קבוצות של רמת פירוט (LOD) לנכסים עם גיאומטריה מורכבת.
חומרים ושיידרים
- הסרת וריאנטים של shader שלא בשימוש באופן פרוגרמטי במהלך תהליך ה-build.
- כדי להימנע משכפול של shader, כדאי לאחד בין וריאציות של shader שמשתמשים בהן בתדירות גבוהה לתוך uber shader.
- הפעלת טעינה דינמית של גופים מטילי צל כדי לטפל בשימוש הנרחב בזיכרון של גופים מטילי צל שנטענו מראש ב-VRAM/RAM. עם זאת, כדאי לשים לב אם קומפילציית ה-shader גורמת לבעיות בפריימים.
- כדי למנוע טעינה של כל הווריאציות, צריך להשתמש בטעינה דינמית של shader. מידע נוסף מופיע בפוסט בבלוג בנושא שיפורים בזמני בניית shader ובשימוש בזיכרון.
- כדי להשתמש נכון במופע חומר, צריך להשתמש במאפיין
MaterialPropertyBlocks.
אודיו
מתחילים בבדיקת ההגדרות הבסיסיות (עמוד 41) ומוודאים את ההגדרות הבאות של ייבוא רשת:
- אם משתמשים במנועי אודיו של צד שלישי כמו FMOD או Wwise, מומלץ להסיר הפניות מיותרות או כפולות של
AudioClip. - טעינה מראש של נתוני אודיו. משביתים את הטעינה מראש של קטעי וידאו שלא נדרשים באופן מיידי במהלך זמן הריצה או הפעלת הסצנה. כך אפשר לצמצם את התקורה של הזיכרון במהלך אתחול הסצנה.
אנימציות
- כדי למזער את מספר מסגרות המפתח ולסלק נתונים מיותרים, צריך לשנות את הגדרות הדחיסה של האנימציה ב-Unity.
- הפחתת פריימים מרכזיים: הסרה אוטומטית של פריימים מרכזיים מיותרים
- דחיסת קווטרניון: דוחסת נתוני סיבוב כדי לצמצם את השימוש בזיכרון
אפשר לשנות את הגדרות הדחיסה בהגדרות הייבוא של אנימציה בכרטיסייה Rig או Animation.
כדאי לעשות שימוש חוזר בקליפים של אנימציה במקום לשכפל אותם לאובייקטים שונים.
אפשר להשתמש ב-Animator Override Controllers כדי לעשות שימוש חוזר ב-Animator Controller ולהחליף קליפים ספציפיים עבור דמויות שונות.
הטמעה של אנימציות שמבוססות על פיזיקה: אם האנימציות שלכם מבוססות על פיזיקה או על יצירה פרוצדורלית, כדאי להטמיע אותן בקליפים של אנימציה כדי להימנע מחישובים בזמן הריצה.
אופטימיזציה של מערכת השלד: כדאי להשתמש בפחות עצמות במערכת כדי לצמצם את המורכבות ואת צריכת הזיכרון.
- מומלץ להימנע משימוש בעצמות רבות מדי לאובייקטים קטנים או סטטיים.
- אם עצמות מסוימות לא מונפשות או לא נחוצות, צריך להסיר אותן מהריג.
להקטין את האורך של קליפ האנימציה.
- חיתוך של קטעי אנימציה כך שיכללו רק את הפריימים הנדרשים. אל תאחסנו אנימציות שלא נמצאות בשימוש או אנימציות ארוכות מדי.
- כדאי להשתמש באנימציות שחוזרות על עצמן במקום ליצור קליפים ארוכים לתנועות חוזרות.
מוודאים שרק רכיב אנימציה אחד מצורף או מופעל. לדוגמה, אם אתם משתמשים ב-Animator, כדאי להשבית או להסיר רכיבים של אנימציה מדור קודם.
אם אין צורך, עדיף לא להשתמש ב-Animator. כדי ליצור אפקטים חזותיים פשוטים, אפשר להשתמש בספריות של tweening או להטמיע את האפקט החזותי בסקריפט. מערכת האנימציה יכולה להיות עתירת משאבים, במיוחד במכשירים ניידים ברמת כניסה.
אם יש לכם מספר גדול של אנימציות, מומלץ להשתמש במערכת העבודות, כי היא תוכננה מחדש כדי להיות יעילה יותר מבחינת זיכרון.
סצנות
כשסצנות חדשות נטענות, הן מביאות איתן נכסים כתלות. עם זאת, ללא ניהול מחזור חיים של נכסים, יחסי התלות האלה לא נבדקים על ידי מוני הפניות. כתוצאה מכך, יכול להיות שנכסים יישארו בזיכרון גם אחרי שהסצנות שלא בשימוש יוסרו, מה שיגרום לפיצול הזיכרון.
- כדאי להשתמש בObject Pooling של Unity כדי לעשות שימוש חוזר במופעים של GameObject עבור אלמנטים חוזרים במשחק, כי Object Pooling משתמש במחסנית כדי להכיל אוסף של מופעים של אובייקטים לשימוש חוזר, והוא לא בטוח לשימוש עם שרשורים. צמצום השימוש ב-
Instantiateוב-Destroyמשפר את ביצועי המעבד (CPU) ואת יציבות הזיכרון. - הסרת נכסים:
- כדאי לבטל את הטעינה של נכסים באופן אסטרטגי ברגעים פחות קריטיים, כמו מסכי פתיחה או מסכי טעינה.
- שימוש תכוף ב-
Resources.UnloadUnusedAssetsגורם לעלייה חדה בעיבוד המעבד בגלל פעולות ניטור גדולות של תלות פנימית. - בודקים אם יש עליות חדות בשימוש במעבד (CPU) בסמן הפרופיל GC.MarkDependencies.
מומלץ להסיר את הפונקציה או להפחית את תדירות ההפעלה שלה, ולפרוק באופן ידני משאבים ספציפיים באמצעות Resources.UnloadAsset במקום להסתמך על
Resources.UnloadUnusedAssets().
- כדאי לשנות את המבנה של הסצנות במקום להשתמש כל הזמן ב-Resources.UnloadUnusedAssets.
- התקשרות אל
Resources.UnloadUnusedAssets()עבורAddressablesעלולה לגרום לפריקה לא מכוונת של חבילות שנטענו באופן דינמי. חשוב לנהל בקפידה את מחזור החיים של נכסים שנטענים באופן דינמי.
שונות
פיצול שנגרם ממעברים בין סצנות – כשקוראים ל-method
Resources.UnloadUnusedAssets(), Unity מבצעת את הפעולות הבאות:- מפנה זיכרון לנכסים שכבר לא בשימוש
- מבצע פעולה שדומה לאיסוף אשפה כדי לבדוק את ערימת האובייקטים המנוהלים והמקומיים ולפרוק נכסים שלא נמצאים בשימוש
- מנקה את הזיכרון של הטקסטורה, הרשת והנכס, בתנאי שאין קובץ עזר פעיל
AssetBundleאוAddressable– ביצוע שינויים באזור הזה הוא מורכב ודורש מאמץ משותף של הצוות כדי ליישם את האסטרטגיות. עם זאת, אחרי שמיישמים את האסטרטגיות האלה, אפשר לשפר משמעותית את השימוש בזיכרון, להקטין את גודל ההורדה ולהפחית את העלויות בענן. מידע נוסף על ניהול נכסים ב-Unity עםAddressablesיחסי תלות משותפים מרכזיים – קיבוץ שיטתי של יחסי תלות משותפים, כמו shaders, טקסטורות וגופנים, בחבילות ייעודיות או ב
Addressableקבוצות. כך מצטמצם הסיכוי לכפילויות, והמערכת דואגת להסיר נכסים מיותרים בצורה יעילה.שימוש ב-
Addressablesלמעקב אחר תלות – Addressables מפשט את הטעינה והפריקה ויכול לפרוק באופן אוטומטי תלויות שכבר לא מפנים אליהן. יכול להיות שמעבר ל-Addressablesלניהול תוכן ולפתרון תלות יהיה פתרון אפשרי, בהתאם למקרה הספציפי של המשחק. מנתחים שרשראות של יחסי תלות באמצעות הכלי Analyze כדי לזהות כפילויות או יחסי תלות מיותרים. אפשרות אחרת היא לעיין בכלי הנתונים של Unity אם אתם משתמשים ב-AssetBundles.
TypeTrees– אם רכיבי ה-Addressablesוה-AssetBundlesשל המשחק שלכם נוצרו ונפרסו באמצעות אותה גרסה של Unity כמו הנגן, ולא נדרשת תאימות לדורות קודמים עם גרסאות אחרות של הנגן, כדאי להשבית את הכתיבה שלTypeTree. פעולה כזו אמורה להקטין את גודל החבילה ואת הזיכרון שבשימוש של אובייקט הקובץ שסודר. משנים את תהליך build בהגדרת חבילת Addressables המקומית ContentBuildFlags ל-DisableWriteTypeTree.
כתיבת קוד שמתאים לאיסוף אשפה
Unity משתמשת במנגנון איסוף זבל (GC) כדי לנהל את הזיכרון על ידי זיהוי אוטומטי של זיכרון לא בשימוש ופינוי שלו. למרות ש-GC הוא חיוני, אם לא מטפלים בו בצורה נכונה הוא עלול לגרום לבעיות בביצועים (למשל, עליות חדות בקצב הפריימים), כי התהליך הזה יכול להשהות את המשחק לרגע, מה שמוביל לבעיות בביצועים ולחוויית משתמש לא אופטימלית.
במדריך Unity מפורטות טכניקות שימושיות לצמצום התדירות של הקצאות זיכרון בערימה מנוהלת. בדף 271 של UnityPerformanceTuningBible מופיעות דוגמאות.
הפחתת ההקצאות של איסוף האשפה:
- מומלץ להימנע מ-LINQ, מביטויי למדה ומסגירות, שמקצים זיכרון בערימה.
- משתמשים ב-
StringBuilderלמחרוזות שניתנות לשינוי במקום בשרשור מחרוזות. - אפשר להשתמש מחדש באוספים על ידי קריאה ל-
COLLECTIONS.Clear()במקום ליצור אותם מחדש.
מידע נוסף זמין בספר הדיגיטלי Ultimate Guide to Profiling Unity games.
ניהול עדכונים בקנבס של ממשק המשתמש:
- שינויים דינמיים ברכיבי ממשק המשתמש – כשמעדכנים רכיבים בממשק המשתמש כמו Text, Image או מאפיינים (לדוגמה, שינוי תוכן הטקסט, שינוי הגודל של רכיבים או אנימציה של מיקומים), המנוע עשוי להקצות זיכרון לאובייקטים זמניים.
RectTransform - הקצאות של מחרוזות – רכיבי ממשק משתמש כמו Text לרוב דורשים עדכונים של מחרוזות, כי מחרוזות הן בלתי ניתנות לשינוי ברוב שפות התכנות.
- אזור עריכה לא נקי – כשמשהו באזור העריכה משתנה (לדוגמה, שינוי גודל, הפעלה והשבתה של רכיבים או שינוי מאפייני הפריסה), יכול להיות שאזור העריכה כולו או חלק ממנו יסומן כלא נקי וייבנה מחדש. הפעולה הזו יכולה להפעיל יצירה של מבני נתונים זמניים (לדוגמה, נתוני רשת, מאגרי קודקודים או חישובי פריסה), מה שמוסיף ליצירת האשפה.
- עדכונים מורכבים או תכופים – אם יש הרבה רכיבים באזור העריכה או שהעדכונים מתבצעים בתדירות גבוהה (לדוגמה, בכל פריים), הבנייה מחדש עלולה לגרום לשימוש מוגבר בזיכרון.
- שינויים דינמיים ברכיבי ממשק המשתמש – כשמעדכנים רכיבים בממשק המשתמש כמו Text, Image או מאפיינים (לדוגמה, שינוי תוכן הטקסט, שינוי הגודל של רכיבים או אנימציה של מיקומים), המנוע עשוי להקצות זיכרון לאובייקטים זמניים.
הפעלת GC מצטבר כדי לצמצם את העליות החדות הגדולות באיסוף על ידי פיזור ניקויים של הקצאות על פני כמה פריימים. פרופיל כדי לוודא שהאפשרות הזו משפרת את הביצועים של המשחק ואת הזיכרון שבשימוש.
אם המשחק שלכם דורש גישה מבוקרת, צריך להגדיר את מצב מנגנון איסוף זבל לערך manual. לאחר מכן, כשעוברים לשלב אחר או ברגע אחר שבו אין גיימפליי פעיל, קוראים למנגנון איסוף זבל.
הפעלת קריאות ידניות למנגנון איסוף זבל GC.Collect() למעברים בין מצבי משחק (לדוגמה, מעבר בין רמות).
כדי לבצע אופטימיזציה של מערכים, מתחילים בשיטות פשוטות לכתיבת קוד, ואם צריך, משתמשים במערכים מקוריים או במאגרי נתונים מקוריים אחרים למערכים גדולים.
כדי לעקוב אחרי הפניות לאובייקטים לא מנוהלים שנשמרים אחרי השמדה, אפשר להשתמש בכלים כמו Unity Memory Profiler כדי לעקוב אחרי אובייקטים מנוהלים.
כדי להשתמש בגישה אוטומטית, אפשר לשלוח סמן פרופיל אל כלי הדוחות על הביצועים.
איך מונעים דליפות ופיצול של זיכרון
דליפות זיכרון
בקוד C#, כשקיים הפניה לאובייקט Unity אחרי שהאובייקט נהרס, אובייקט העטיפה המנוהל, שנקרא Managed Shell, נשאר בזיכרון. הזיכרון המקורי שמשויך להפניה משוחרר כשהסצנה נפרקת או כשה-GameObject שהזיכרון מצורף אליו, או כל אחד מאובייקטי ההורה שלו, מושמדים באמצעות השיטה Destroy(). עם זאת, אם לא נוקו הפניות אחרות ל-Scene או ל-GameObject, יכול להיות שהזיכרון המנוהל יישאר כ-Leaked Shell Object. פרטים נוספים על אובייקטים של Managed Shell זמינים במדריך אובייקטים של Managed Shell.
בנוסף, דליפות זיכרון יכולות להיגרם ממינויים לאירועים, מ-lambdas ומ-closures, משרשור מחרוזות ומניהול לא תקין של אובייקטים מאוגדים:
- כדי להתחיל, כדאי לעיין במאמר בנושא איתור דליפות זיכרון כדי להשוות בין תמונות מצב של הזיכרון ב-Unity בצורה נכונה.
- בודקים אם יש מינויים לאירועים ודליפות זיכרון. אם אובייקטים נרשמים לאירועים (לדוגמה, באמצעות נציגים או UnityEvents) אבל לא מבטלים את הרישום שלהם בצורה תקינה לפני שהם נהרסים, יכול להיות שמנהל האירועים או המפרסם ישמרו הפניות לאובייקטים האלה. כך האובייקטים האלה לא נמחקים על ידי איסוף זבל, מה שמוביל לדליפות זיכרון.
- מעקב אחר אירועים של מחלקות גלובליות או מחלקות יחיד שלא בוטל הרישום שלהן בהשמדת אובייקט. לדוגמה, ביטול הרשמה או ביטול ההרשאה של נציגים ב-destructors של אובייקטים.
- מוודאים שהשמדה של אובייקטים במאגר מבטלת לחלוטין את ההפניות לרכיבי רשת של טקסט, לטקסטורות ולאובייקטים ראשיים של משחקים.
- חשוב לזכור שאם משווים בין תמונות מצב של Unity Memory Profiler ורואים הבדל בצריכת הזיכרון בלי סיבה ברורה, יכול להיות שההבדל נובע ממנהל ההתקן של הגרפיקה או ממערכת ההפעלה עצמה.
פיצול הזיכרון
פיצול הזיכרון מתרחש כשמפנים הקצאות קטנות רבות בסדר אקראי. הקצאות של Heap מתבצעות באופן רציף, כלומר, נתחי זיכרון חדשים נוצרים כשנגמר המקום בנתח הקודם. כתוצאה מכך, אובייקטים חדשים לא ממלאים את האזורים הריקים של חתיכות ישנות, מה שמוביל לפיצול. בנוסף, הקצאות זמניות גדולות עלולות לגרום לפיצול קבוע למשך הפעלת המשחק.
הבעיה הזו חמורה במיוחד כשמבצעים הקצאות גדולות לזמן קצר ליד הקצאות לזמן ארוך.
הקצאות לקבוצות על סמך משך החיים שלהן. מומלץ לבצע הקצאות לטווח ארוך ביחד, בשלב מוקדם במחזור החיים של האפליקציה.
משתתפים שצופים ומנהלי אירועים
- בנוסף לבעיה שצוינה בקטע דליפות זיכרון, דליפות זיכרון עלולות לתרום לפיצול הזיכרון לאורך זמן, כי הן משאירות זיכרון לא בשימוש שמוקצה לאובייקטים שכבר לא בשימוש.
- מוודאים שהשמדה של אובייקטים מאוגדים מבטלת לחלוטין את ההפניות לרכיבי רשת של טקסט, לטקסטורות ולאלמנט ההורה
GameObjects. - מנהלי אירועים יוצרים ומאחסנים לעיתים קרובות רשימות או מילונים כדי לנהל את המינויים לאירועים. אם הם גדלים וקטנים באופן דינמי במהלך זמן הריצה, הם יכולים לתרום לפיצול הזיכרון בגלל הקצאות וביטולים תכופים של הקצאות.
קוד
- לפעמים קורוטינות מקצות זיכרון, ואפשר להימנע מכך בקלות על ידי שמירת הצהרת ההחזרה של IEnumerator במטמון במקום להצהיר על הצהרה חדשה בכל פעם.
- כדי להימנע משמירה של
UnityEngine.Objectהפניות רפאים, צריך לעקוב באופן רציף אחרי מצבי מחזור החיים של אובייקטים במאגר.
נכסים
- כדי להימנע מטעינה מראש של כל הגופנים במקרים של משחקים רב-לשוניים, כדאי להשתמש במערכות דינמיות של חלופות למקרים של טקסט.
- ארגון הנכסים (למשל, טקסטורות וחלקיקים) לפי סוג ומחזור חיים צפוי.
- דחיסת נכסים עם מאפייני מחזור חיים לא פעילים, כמו תמונות מיותרות בממשק המשתמש ורשתות סטטיות.
הקצאות מבוססות-משך חיים
- כדי להבטיח הקצאות קומפקטיות, כדאי להקצות נכסים לטווח ארוך בתחילת מחזור החיים של האפליקציה.
- משתמשים ב-NativeCollections או במקצי זיכרון בהתאמה אישית עבור מבני נתונים זמניים או כאלה שדורשים הרבה זיכרון (לדוגמה, אשכולות פיזיקליים).
פעולת זיכרון שקשורה לקוד ולקבצים הפעלה
גם קובץ ההפעלה של המשחק ותוספים משפיעים על השימוש בזיכרון.
מטא-נתונים של IL2CPP
IL2CPP יוצר מטא-נתונים לכל סוג (למשל, מחלקות, גנריות ונציגים) בזמן הבנייה, ואז משתמש בהם בזמן הריצה לצורך רפלקציה, בדיקת סוגים ופעולות אחרות שספציפיות לזמן הריצה. המטא-נתונים האלה מאוחסנים בזיכרון ויכולים לתרום באופן משמעותי לזיכרון שבשימוש הכולל של האפליקציה. מטמון המטא-נתונים של IL2CPP תורם באופן משמעותי לזמני האתחול והטעינה. בנוסף, IL2CPP לא מבטל כפילויות של רכיבי מטא-נתונים מסוימים (לדוגמה, סוגים כלליים או מידע שעבר סריאליזציה), מה שעלול לגרום לשימוש מוגזם בזיכרון. הבעיה מחמירה בגלל שימוש חוזר או מיותר בסוגים בפרויקט.
אפשר לצמצם את המטא-נתונים של IL2CPP על ידי:
- הימנעות משימוש בממשקי API של רפלקציה, כי הם יכולים לתרום באופן משמעותי להקצאות של מטא-נתונים ב-IL2CPP
- השבתה של חבילות מובנות
- הטמענו את השיתוף הגנרי המלא של Unity 2022, שאמור לעזור לצמצם את התקורה שנגרמת על ידי גנריות. עם זאת, כדי להקטין עוד יותר את ההקצאות, כדאי להפחית את השימוש בגנריות.
הסרת קוד
בנוסף להקטנת הגודל של ה-build, הסרת הקוד מקטינה גם את השימוש בזיכרון. כשמבצעים build מול קצה העורף של סקריפטים IL2CPP, הסרת bytecode מנוהל (שמופעלת כברירת מחדל) מסירה קוד שלא נמצא בשימוש מאסמבלי מנוהל. התהליך מתבצע על ידי הגדרת הרכבות בסיסיות, ולאחר מכן שימוש בניתוח קוד סטטי כדי לקבוע איזה קוד מנוהל אחר נמצא בשימוש בהרכבות הבסיסיות האלה. כל קוד שלא ניתן להגיע אליו מוסר. מידע נוסף על הסרת קוד מנוהל זמין בפוסט בבלוג TTales from the optimization trenches: Better managed code stripping with Unity 2020 LTS ובמסמכי התיעוד בנושא הסרת קוד מנוהל.
הקצאות מקוריות
כדאי להתנסות בהקצאת זיכרון נייטיב כדי לכוונן את הקצאת הזיכרון. אם הזיכרון של המשחק נמוך, כדאי להשתמש בבלוקים קטנים יותר של זיכרון, גם אם זה כרוך בהקצאת זיכרון איטית יותר. מידע נוסף זמין בדוגמה של הקצאת זיכרון דינמית בערימה.
ניהול פלאגינים וערכות SDK מקוריים
מאתרים את הפלאגין הבעייתי – מסירים כל פלאגין ומשווים את תמונות המצב של זיכרון המשחק. התהליך הזה כולל השבתה של הרבה פונקציות קוד באמצעות Scripting (סקריפטים) > Define Symbols (הגדרת סמלים) ושינוי מבנה של מחלקות עם צימוד גבוה באמצעות ממשקים. כדי להקל על תהליך ההשבתה של תלות חיצונית בלי שהמשחק יהפוך ללא שמיש, אפשר לעיין במאמר שיפור הקוד באמצעות דפוסי תכנות של משחקים.
פונים למפתח הפלאגין או ה-SDK – רוב הפלאגינים הם לא קוד פתוח.
שחזור השימוש בזיכרון של התוסף – אפשר לכתוב תוסף פשוט (אפשר להשתמש בתוסף Unity הזה כהפניה) שמבצע הקצאות זיכרון. אפשר לבדוק את תמונות מצב הזיכרון באמצעות Android Studio (כי Unity לא עוקבת אחרי ההקצאות האלה) או להתקשר למחלקה
MemoryInfoולשיטהRuntime.totalMemory()באותו פרויקט.
פלאגין Unity מקצה זיכרון Java וזיכרון מקורי. כך עושים זאת:
Java
byte[] largeObject = new byte[1024 * 1024 * megaBytes];
list.add(largeObject);
Native
char* buffer = new char[megabytes * 1024 * 1024];
// Random data to fill the buffer
for (int i = 1; i < megabytes * 1024 * 1024; ++i) {
buffer[i] = 'A' + (i % 26); // Fill with letters A-Z
}