פתרון בעיות ידועות במשחק 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 וליהנות מקוד מקורי יעיל. בדרך כלל אין צורך באינטראקציה ישירה עם הזיכרון המקורי, אבל אפשר לעקוב אחרי ההשפעה של הזיכרון המקורי על הביצועים באמצעות כלי הפרופיל, ולשנות את ההגדרות כדי לשפר את הביצועים.
הזיכרון לא משותף בין C# לבין קוד Native, כמו שמוצג באיור 3. הנתונים שנדרשים על ידי C# מוקצים במרחב הזיכרון המנוהל בכל פעם שנדרשים.
כדי שהקוד של המשחק המנוהל (C#) יוכל לגשת לנתוני הזיכרון המקורי של המנוע, למשל, קריאה ל-GameObject.transform מבצעת קריאה מקורית כדי לגשת לנתוני הזיכרון באזור המקורי, ואז מחזירה ערכים ל-C# באמצעות Bindings. הקישורים מבטיחים מוסכמות קריאה נכונות לכל פלטפורמה ומטפלים בהמרת סוגים מנוהלים באופן אוטומטי למקבילים המקוריים שלהם.
זה קורה רק בפעם הראשונה, כי המעטפת המנוהלת לגישה למאפיין transform נשמרת בקוד המקורי. שמירת המאפיין transform במטמון יכולה להפחית את מספר הקריאות הלוך ושוב בין קוד מנוהל לקוד מקורי, אבל יעילות השמירה במטמון תלויה בתדירות השימוש במאפיין. בנוסף, חשוב לזכור ש-Unity לא מעתיקה חלקים מזיכרון מקורי לזיכרון מנוהל כשניגשים לממשקי ה-API האלה.

מידע נוסף זמין במאמר מבוא לזיכרון ב-Unity.
בנוסף, חשוב להגדיר תקציב זיכרון כדי שהמשחק יפעל בצורה חלקה, ולהטמיע מערכת ניתוח או דיווח על צריכת הזיכרון כדי לוודא שכל גרסה חדשה לא חורגת מתקציב הזיכרון. אסטרטגיה נוספת לקבלת תובנות טובות יותר היא שילוב של בדיקות במצב Play עם אינטגרציה רציפה (CI) כדי לאמת את צריכת הזיכרון באזורים ספציפיים במשחק.
ניהול נכסים
זהו החלק המשמעותי ביותר בצריכת הזיכרון, וזהו החלק שבו אפשר לבצע פעולות. פרופיל בהקדם האפשרי.
השימוש בזיכרון במשחקי Android יכול להשתנות באופן משמעותי בהתאם לסוג המשחק, למספר הנכסים ולסוגים שלהם, ולאסטרטגיות האופטימיזציה של הזיכרון. עם זאת, בין הגורמים הנפוצים שמשפיעים על השימוש בזיכרון אפשר למנות בדרך כלל טקסטורות, רשתות, קובצי אודיו, הצללות, אנימציות ותסריטים.
זיהוי נכסים דיגיטליים כפולים
השלב הראשון הוא לזהות נכסים שהוגדרו בצורה לא טובה ונכסים כפולים באמצעות כלי פרופיל הזיכרון, כלי דוחות הבנייה או Project Auditor.
מרקמים
מנתחים את התמיכה במכשירים במשחק ומחליטים על פורמט הטקסטורה הנכון. אפשר לפצל את חבילות הטקסטורה למכשירים מתקדמים ולמכשירים פשוטים יותר באמצעות Play Asset Delivery, Addressable או תהליך ידני יותר עם AssetBundle.
כדאי לפעול לפי ההמלצות המוכרות ביותר שזמינות במאמר אופטימיזציה של הביצועים של משחקים לנייד ובפוסט לדיון אופטימיזציה של הגדרות ייבוא טקסטורות ב-Unity. אחר כך נסו את הפתרונות הבאים:
כדי להקטין את נפח הזיכרון, כדאי לדחוס טקסטורות באמצעות פורמטים של ASTC ולנסות להשתמש בשיעור בלוקים גבוה יותר, כמו 8x8.
אם נדרש שימוש ב-ETC2, צריך לארוז את הטקסטורות ב-Atlas. הצבת כמה טקסטורות בטקסטורה אחת מבטיחה שהיא תהיה חזקה בחזקת 2 (POT), יכולה להפחית את קריאות הציור ויכולה להאיץ את העיבוד.
אופטימיזציה של הפורמט והגודל של טקסטורת RenderTarget. מומלץ להימנע משימוש בטקסטורות ברזולוציה גבוהה שלא לצורך. שימוש בטקסטורות קטנות יותר במכשירים ניידים חוסך זיכרון.
כדי לחסוך בזיכרון של הטקסטורה, משתמשים בTexture channel packing.
רשתות ומודלים
מתחילים בבדיקת ההגדרות הבסיסיות (עמוד 27) ומוודאים את הגדרות הייבוא של הרשת:
- מיזוג של רשתות מיותרות וקטנות יותר.
- צמצום מספר הקודקודים של אובייקטים בסצנות (לדוגמה, אובייקטים סטטיים או רחוקים).
- יצירת קבוצות של רמת פירוט (LOD) לנכסים עם גיאומטריה גבוהה.
חומרים ו-shaders
- הסרת וריאציות של Shader שלא בשימוש באופן פרוגרמטי במהלך תהליך ה-build.
- כדי להימנע משכפול של shader, כדאי לאחד בין וריאציות של shader שנמצאות בשימוש תדיר לתוך uber shader.
- הפעלת טעינה דינמית של shaders כדי לטפל בטביעת הזיכרון הגדולה של shaders שנטענו מראש ב-VRAM/RAM. עם זאת, חשוב לשים לב אם קומפילציית ה-shader גורמת לבעיות בפריימים.
- כדי למנוע טעינה של כל הווריאציות, כדאי להשתמש בטעינה דינמית של הצללה. מידע נוסף זמין בפוסט בבלוג בנושא שיפורים בזמני בניית הצללות ובשימוש בזיכרון.
- כדי להשתמש נכון במופע חומר, צריך להשתמש במאפיין
MaterialPropertyBlocks
.
אודיו
מתחילים בבדיקת ההגדרות הבסיסיות (עמוד 41), ומוודאים את הגדרות הייבוא של הרשת:
- אם משתמשים במנועי אודיו של צד שלישי כמו FMOD או Wwise, מומלץ להסיר הפניות מיותרות או לא בשימוש
AudioClip
. - טעינה מראש של נתוני אודיו. משביתים את הטעינה מראש של קליפים שלא נדרשים באופן מיידי במהלך זמן הריצה או הפעלת הסצנה. כך אפשר להפחית את התקורה של הזיכרון במהלך אתחול הסצנה.
אנימציות
- כדאי לשנות את הגדרות הדחיסה של האנימציה ב-Unity כדי למזער את מספר מסגרות המפתח ולסלק נתונים מיותרים.
- הפחתת פריימים מרכזיים: הסרה אוטומטית של פריימים מרכזיים מיותרים
- דחיסת קווטרניון: דוחסת נתוני סיבוב כדי לצמצם את השימוש בזיכרון
אפשר לשנות את הגדרות הדחיסה בהגדרות הייבוא של האנימציה בכרטיסייה Rig או Animation.
כדאי לעשות שימוש חוזר בקליפים של אנימציות במקום לשכפל אותם לאובייקטים שונים.
אפשר להשתמש ב-Animator Override Controllers כדי לעשות שימוש חוזר ב-Animator Controller ולהחליף קליפים ספציפיים עבור דמויות שונות.
הוספת אנימציות שמבוססות על פיזיקה: אם האנימציות שלכם מבוססות על פיזיקה או על פרוצדורות, כדאי להוסיף אותן לקליפים של אנימציה כדי להימנע מחישובים בזמן הריצה.
אופטימיזציה של מערכת השלד: כדאי להשתמש בפחות עצמות במערכת כדי להפחית את המורכבות ואת צריכת הזיכרון.
- מומלץ להימנע משימוש בעצמות רבות מדי לאובייקטים קטנים או סטטיים.
- אם עצמות מסוימות לא מונפשות או לא נחוצות, מסירים אותן מהריג.
לקצר את קטע האנימציה.
- חיתוך של קליפים של אנימציה כך שיכללו רק את הפריימים הנדרשים. אל תאחסנו אנימציות שלא בשימוש או אנימציות ארוכות מדי.
- כדאי להשתמש באנימציות חוזרות במקום ליצור קליפים ארוכים לתנועות חוזרות.
מוודאים שרק רכיב אנימציה אחד מצורף או מופעל. לדוגמה, אם אתם משתמשים ב-Animator, כדאי להשבית או להסיר רכיבים של Legacy animation.
מומלץ להימנע משימוש ב-Animator אם הוא לא נחוץ. כדי ליצור אפקטים חזותיים פשוטים, אפשר להשתמש בספריות של אנימציה בין פריימים או להטמיע את האפקט החזותי בסקריפט. מערכת האנימציה יכולה להיות עתירת משאבים, במיוחד במכשירים ניידים פשוטים.
כדאי להשתמש במערכת העבודות לאנימציות כשמטפלים במספר גדול של אנימציות, כי המערכת הזו עוצבה מחדש כדי להיות יעילה יותר מבחינת הזיכרון.
סביבות תאורה
כשסצנות חדשות נטענות, הן מביאות איתן נכסים כתלות. עם זאת, ללא ניהול מחזור חיים של נכסים, יחסי התלות האלה לא מנוטרים על ידי מוני הפניות. כתוצאה מכך, יכול להיות שנכסים יישארו בזיכרון גם אחרי שהסצנות שלא בשימוש יוסרו, מה שיגרום לפיצול הזיכרון.
- כדאי להשתמש במאגר האובייקטים של Unity כדי לעשות שימוש חוזר במופעים של GameObject עבור רכיבים חוזרים של משחק, כי מאגר האובייקטים משתמש במחסנית כדי להחזיק אוסף של מופעים של אובייקטים לשימוש חוזר, והוא לא בטוח לשימוש עם שרשורים. מזעור
Instantiate
ו-Destroy
משפר את ביצועי המעבד (CPU) ואת יציבות הזיכרון. - הסרת נכסים:
- כדאי לבצע פריקה של נכסים באופן אסטרטגי ברגעים פחות קריטיים, כמו מסכי פתיחה או מסכי טעינה.
- שימוש תכוף ב-
Resources.UnloadUnusedAssets
גורם לעלייה חדה בעיבוד המעבד (CPU) בגלל פעולות נרחבות של ניטור תלות פנימית. - בודקים אם יש עליות חדות בשימוש במעבד (CPU) בסמן הפרופיל GC.MarkDependencies.
מומלץ להסיר את התדירות של ההפעלה או להפחית אותה, ולבטל את הטעינה של משאבים ספציפיים באופן ידני באמצעות Resources.UnloadAsset במקום להסתמך על
Resources.UnloadUnusedAssets()
.
- עדיף לשנות את המבנה של הסצנות במקום להשתמש כל הזמן ב-Resources.UnloadUnusedAssets.
- התקשרות אל
Resources.UnloadUnusedAssets()
עבורAddressables
עלולה לגרום לפריקה לא מכוונת של חבילות שנטענו באופן דינמי. חשוב לנהל בקפידה את מחזור החיים של נכסים שנטענים באופן דינמי.
שונות
פיצול שנגרם ממעברים בין סצנות – כשקוראים לשיטה
Resources.UnloadUnusedAssets()
, Unity מבצעת את הפעולות הבאות:- מפנה זיכרון לנכסים שכבר לא בשימוש
- מבצע פעולה שדומה לאיסוף אשפה כדי לבדוק את ערימת האובייקטים המנוהלים והמקומיים, ולפרוק נכסים שלא נמצאים בשימוש
- מנקה את הזיכרון של הטקסטורה, הרשת והנכס, בתנאי שאין הפניה פעילה
AssetBundle
אוAddressable
– ביצוע שינויים באזור הזה הוא מורכב ודורש מאמץ משותף של הצוות כדי ליישם את האסטרטגיות. אבל אחרי שמיישמים את האסטרטגיות האלה, הן משפרות באופן משמעותי את השימוש בזיכרון, מצמצמות את גודל ההורדה ומפחיתות את העלויות בענן. מידע נוסף על ניהול נכסים ב-Unity עםAddressables
יחסי תלות משותפים מרכזיים – קיבוץ שיטתי של יחסי תלות משותפים, כמו shaders, טקסטורות וגופנים, בחבילות ייעודיות או ב
Addressable
קבוצות. כך מצטמצם השכפול ומתבצעת הסרה יעילה של נכסים מיותרים.משתמשים ב-
Addressables
למעקב אחר תלות – Addressables מפשט את הטעינה והפריקה ויכול לפרוק באופן אוטומטי תלויות שכבר לא מפנים אליהן. יכול להיות שמעבר ל-Addressables
לניהול תוכן ולפתרון תלות יהיה פתרון אפשרי, בהתאם למקרה הספציפי של המשחק. מנתחים שרשראות של יחסי תלות באמצעות הכלי Analyze כדי לזהות כפילויות או יחסי תלות מיותרים. לחלופין, אם אתם משתמשים ב-AssetBundles, תוכלו לעיין ב-Unity Data Tools.
TypeTrees
– אםAddressables
ו-AssetBundles
של המשחק בנויים ומוטמעים באמצעות אותה גרסה של Unity כמו הנגן, ולא נדרשת תאימות לאחור עם גרסאות אחרות של הנגן, כדאי להשבית את הכתיבה שלTypeTree
. פעולה כזו אמורה להקטין את גודל חבילת ה-bundle ואת נפח הזיכרון של אובייקט הקובץ הסדרתי. משנים את תהליך הבנייה בהגדרת חבילת Addressables המקומית ContentBuildFlags ל-DisableWriteTypeTree.
כתיבת קוד שמתאים לאיסוף אשפה
Unity משתמשת באיסוף אשפה (GC) כדי לנהל את הזיכרון על ידי זיהוי אוטומטי של זיכרון לא בשימוש ופינוי שלו. למרות ש-GC הוא חיוני, הוא עלול לגרום לבעיות בביצועים (לדוגמה, עליות חדות בקצב הפריימים) אם לא מטפלים בו בצורה נכונה, כי התהליך הזה יכול להשהות את המשחק לרגע, מה שמוביל לבעיות בביצועים ולחוויית משתמש לא אופטימלית.
במדריך Unity מפורטות טכניקות שימושיות לצמצום התדירות של הקצאות זיכרון בערימה מנוהלת. בדף 271 של UnityPerformanceTuningBible מופיעות דוגמאות.
הפחתת ההקצאות של איסוף האשפה:
- מומלץ להימנע מ-LINQ, מביטויי למדה ומסגירות, שמקצים זיכרון בערימה.
- משתמשים ב-
StringBuilder
למחרוזות שניתנות לשינוי במקום בשרשור מחרוזות. - אפשר להשתמש מחדש באוספים על ידי קריאה ל-
COLLECTIONS.Clear()
במקום ליצור מחדש את המופעים שלהם.
מידע נוסף זמין בספר הדיגיטלי Ultimate Guide to Profiling Unity games.
ניהול עדכונים של אזור הציור של ממשק המשתמש:
- שינויים דינמיים ברכיבי ממשק משתמש – כשמעדכנים רכיבי ממשק משתמש כמו טקסט, תמונה או מאפיינים של
RectTransform
(לדוגמה, שינוי תוכן טקסט, שינוי גודל של רכיבים או אנימציה של מיקומים), יכול להיות שהמנוע יקצה זיכרון לאובייקטים זמניים. - הקצאות של מחרוזות – רכיבי ממשק משתמש כמו Text לרוב דורשים עדכונים של מחרוזות, כי מחרוזות הן בלתי ניתנות לשינוי ברוב שפות התכנות.
- אזור עריכה לא נקי – כשמשהו באזור העריכה משתנה (לדוגמה, שינוי גודל, הפעלה והשבתה של רכיבים או שינוי מאפייני הפריסה), יכול להיות שכל אזור העריכה או חלק ממנו יסומן כלא נקי וייבנה מחדש. הפעולה הזו יכולה להפעיל יצירה של מבני נתונים זמניים (לדוגמה, נתוני רשת, מאגרי קודקודים או חישובי פריסה), מה שמוסיף ליצירת נתונים מיותרים.
- עדכונים מורכבים או תכופים – אם יש במסגרת מספר גדול של רכיבים או שהיא מתעדכנת לעיתים קרובות (לדוגמה, כל פריים), הבנייה מחדש הזו עלולה לגרום לשימוש רב בזיכרון.
- שינויים דינמיים ברכיבי ממשק משתמש – כשמעדכנים רכיבי ממשק משתמש כמו טקסט, תמונה או מאפיינים של
הפעלת GC מצטבר כדי לצמצם את העליות החדות הגדולות באיסוף על ידי פיזור הניקויים של ההקצאות על פני כמה פריימים. פרופיל כדי לוודא שהאפשרות הזו משפרת את הביצועים של המשחק ואת השימוש בזיכרון.
אם המשחק שלכם דורש גישה מבוקרת, צריך להגדיר את מצב איסוף האשפה (garbage collection) לערך manual. לאחר מכן, כשמשנים רמה או בכל רגע אחר שבו המשחק לא פעיל, קוראים לאיסוף האשפה.
הפעלת קריאות ידניות לאיסוף זבל GC.Collect() למעברים בין מצבי המשחק (לדוגמה, מעבר בין רמות).
כדי לבצע אופטימיזציה של מערכים, מתחילים בשיטות פשוטות לכתיבת קוד, ואם צריך, משתמשים במערכים מקוריים או במאגרי נתונים מקוריים אחרים למערכים גדולים.
כדי לעקוב אחרי הפניות לאובייקטים לא מנוהלים שנשמרים אחרי השמדה, אפשר להשתמש בכלים כמו Unity Memory Profiler כדי לעקוב אחרי אובייקטים מנוהלים.
כדי להשתמש בגישה אוטומטית, אפשר להשתמש בסמן פרופיל כדי לשלוח אל כלי הדוחות על הביצועים.
איך נמנעים מדליפות זיכרון ופיצול זיכרון
דליפות זיכרון
בקוד C#, כשקיים הפניה לאובייקט Unity אחרי שהאובייקט נהרס, אובייקט העטיפה המנוהל, שנקרא Managed Shell, נשאר בזיכרון. הזיכרון המקורי שמשויך להפניה משוחרר כשהסצנה נפרקת או כשה-GameObject שהזיכרון מצורף אליו, או כל אחד מאובייקטי ההורה שלו, מושמדים באמצעות השיטה Destroy()
. עם זאת, אם הפניות אחרות ל-Scene או ל-GameObject לא נוקו, יכול להיות שהזיכרון המנוהל יישאר כ-Leaked Shell Object. פרטים נוספים על אובייקטים של מעטפת מנוהלת זמינים במדריך אובייקטים של מעטפת מנוהלת.
בנוסף, דליפות זיכרון יכולות להיגרם ממינויים לאירועים, מ-lambdas ומ-closures, משרשור מחרוזות ומניהול לא תקין של אובייקטים מאוגדים:
- כדי להתחיל, כדאי לעיין במאמר בנושא איתור דליפות זיכרון כדי להשוות בין תמונות מצב של הזיכרון ב-Unity בצורה נכונה.
- בודקים אם יש מינויים לאירועים ודליפות זיכרון. אם אובייקטים נרשמים לאירועים (לדוגמה, באמצעות נציגים או UnityEvents) אבל לא מבטלים את הרישום שלהם בצורה תקינה לפני שהם נהרסים, יכול להיות שמנהל האירועים או המפרסם ישמרו הפניות לאובייקטים האלה. כך, האובייקטים האלה לא נאספים על ידי איסוף האשפה, מה שמוביל לדליפות זיכרון.
- מעקב אחר אירועים של מחלקות גלובליות או מחלקות יחיד שלא בוטל הרישום שלהן בהשמדת אובייקט. לדוגמה, ביטול הרשמה או ביטול ההתחברות של נציגים ב-destructors של אובייקטים.
- מוודאים שהשמדה של אובייקטים במאגר מבטלת לחלוטין את ההפניות לרכיבי text mesh, לטקסטורות ולאובייקטים הראשיים של המשחק.
- חשוב לזכור שכאשר משווים תמונות מצב של Unity Memory Profiler ורואים הבדל בצריכת הזיכרון ללא סיבה ברורה, יכול להיות שההבדל נובע מדרייבר הגרפיקה או ממערכת ההפעלה עצמה.
פיצול הזיכרון
פיצול הזיכרון מתרחש כשמפנים הקצאות קטנות רבות בסדר אקראי. הקצאות של Heap מתבצעות באופן רציף, כלומר, גושי זיכרון חדשים נוצרים כשנגמר המקום בגוש הקודם. כתוצאה מכך, אובייקטים חדשים לא ממלאים את האזורים הריקים של חתיכות ישנות, מה שמוביל לפיצול. בנוסף, הקצאות זמניות גדולות עלולות לגרום לפיצול קבוע למשך הפעלת המשחק.
הבעיה הזו חמורה במיוחד כשמקצים הקצאות גדולות לזמן קצר בסמוך להקצאות לזמן ארוך.
קיבוץ הקצאות על סמך משך החיים שלהן. מומלץ לבצע הקצאות לטווח ארוך ביחד, בשלב מוקדם במחזור החיים של האפליקציה.
משתתפים שצופים ומנהלי אירועים
- בנוסף לבעיה שצוינה בקטע (דליפות זיכרון)77, דליפות זיכרון עלולות לתרום לפיצול הזיכרון לאורך זמן, כי הן משאירות זיכרון לא בשימוש שמוקצה לאובייקטים שכבר לא נמצאים בשימוש.
- מוודאים שהשמדה של אובייקטים מאוגדים מבטלת לחלוטין את ההפניות לרכיבי רשת של טקסט, לטקסטורות ולרכיב ההורה
GameObjects
. - מנהלי אירועים יוצרים בדרך כלל רשימות או מילונים ומאחסנים אותם כדי לנהל את המינויים לאירועים. אם הם גדלים וקטנים באופן דינמי במהלך זמן הריצה, הם יכולים לתרום לפיצול הזיכרון בגלל הקצאות וביטולי הקצאות תכופים.
קוד
- לפעמים קורוטינות מקצות זיכרון, ואפשר להימנע מכך בקלות על ידי שמירת הצהרת ההחזרה של IEnumerator במטמון במקום להצהיר על הצהרה חדשה בכל פעם.
- כדי להימנע משמירה של
UnityEngine.Object
הפניות רפאים, צריך לעקוב באופן רציף אחרי מצבי מחזור החיים של אובייקטים במאגר.
נכסים
- כדי להימנע מטעינה מראש של כל הגופנים במקרים של משחקים רב-לשוניים, כדאי להשתמש במערכות דינמיות של חלופות למקרים של טקסט שלא נטען.
- ארגון הנכסים (למשל, טקסטורות וחלקיקים) לפי סוג ומחזור חיים צפוי.
- דחיסת נכסים עם מאפייני מחזור חיים לא פעילים, כמו תמונות מיותרות בממשק המשתמש ורשתות סטטיות.
הקצאות מבוססות-משך
- כדאי להקצות נכסים לטווח ארוך בתחילת מחזור החיים של האפליקציה כדי להבטיח הקצאות קומפקטיות.
- משתמשים ב-NativeCollections או במקצים מותאמים אישית למבני נתונים זמניים או כאלה שדורשים הרבה זיכרון (לדוגמה, אשכולות פיזיקליים).
פעולת זיכרון שקשורה לקוד ולקבצים הפעלה
גם קובץ ההפעלה של המשחק ותוספים משפיעים על השימוש בזיכרון.
מטא-נתונים של IL2CPP
IL2CPP יוצר מטא-נתונים לכל סוג (למשל, מחלקות, גנריות ונציגים) בזמן הבנייה, ואז משתמש בהם בזמן הריצה לצורך רפלקציה, בדיקת סוגים ופעולות אחרות שספציפיות לזמן הריצה. המטא-נתונים האלה מאוחסנים בזיכרון ויכולים לתרום באופן משמעותי לטביעת הרגל הכוללת של הזיכרון של האפליקציה. מטמון המטא-נתונים של IL2CPP תורם באופן משמעותי לזמני האתחול והטעינה. בנוסף, IL2CPP לא מבטל כפילויות של רכיבי מטא-נתונים מסוימים (לדוגמה, סוגים גנריים או מידע שעבר סריאליזציה), מה שעלול לגרום לשימוש מוגזם בזיכרון. הבעיה מחמירה בגלל שימוש חוזר או מיותר בסוגים בפרויקט.
אפשר לצמצם את המטא-נתונים של IL2CPP על ידי:
- הימנעות משימוש בממשקי API של רפלקציה, כי הם יכולים לתרום באופן משמעותי להקצאות של מטא-נתונים ב-IL2CPP
- השבתה של חבילות מובנות
- הטמענו את השיתוף הגנרי המלא של Unity 2022, שאמור לעזור לצמצם את התקורה שנגרמת על ידי גנריות. עם זאת, כדי להקטין עוד יותר את ההקצאות, כדאי להפחית את השימוש בגנריקה.
הסרת קוד
בנוסף להקטנת הגודל של ה-build, הסרת הקוד מקטינה גם את השימוש בזיכרון. כשמבצעים build מול קצה העורף של סקריפטים IL2CPP, הסרת bytecode מנוהל (שמופעלת כברירת מחדל) מסירה קוד שלא נמצא בשימוש מ-assemblies מנוהלים. התהליך מתבצע על ידי הגדרת הרכבות בסיסיות, ולאחר מכן שימוש בניתוח קוד סטטי כדי לקבוע איזה קוד מנוהל אחר נמצא בשימוש בהרכבות הבסיסיות האלה. כל קוד שלא ניתן להגיע אליו מוסר. מידע נוסף על Managed Code Stripping זמין בפוסט בבלוג TTales from the optimization trenches: Better managed code stripping with Unity 2020 LTS ובמסמכי התיעוד בנושא Managed code stripping.
הקצאות מקוריות
כדאי להתנסות בהקצאת זיכרון נייטיב כדי לכוונן את הקצאת הזיכרון. אם הזיכרון של המשחק נמוך, כדאי להשתמש בבלוקים קטנים יותר של זיכרון, גם אם זה כרוך בהקצאת זיכרון איטית יותר. מידע נוסף זמין בדוגמה של הקצאת זיכרון דינמית בערימה.
ניהול פלאגינים וערכות SDK מקוריים
מאתרים את הפלאגין הבעייתי – מסירים כל פלאגין ומשווים את תמונות המצב של זיכרון המשחק. התהליך כולל השבתה של הרבה פונקציות קוד באמצעות Scripting (סקריפטים) > Define Symbols (הגדרת סמלים) וארגון מחדש של מחלקות עם צימוד גבוה באמצעות ממשקים. כדי להקל על תהליך ההשבתה של תלות חיצונית בלי לפגוע באפשרות לשחק במשחק, אפשר לעיין במאמר שיפור הקוד באמצעות דפוסי תכנות של משחקים.
פונים למפתח הפלאגין או ה-SDK – רוב הפלאגינים הם לא קוד פתוח.
לשחזר את השימוש בזיכרון של הפלאגין – אפשר לכתוב פלאגין פשוט (אפשר להשתמש בפלאגין Unity הזה כהפניה) שמבצע הקצאות זיכרון. בודקים את תמונות מצב הזיכרון באמצעות Android Studio (כי Unity לא עוקבת אחרי ההקצאות האלה) או קוראים לסיווג
MemoryInfo
ולשיטהRuntime.totalMemory()
באותו פרויקט.
פלאגין Unity מקצה זיכרון Java וזיכרון Native. כך עושים זאת:
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
}