आजकल के मोबाइल डिवाइसों में रैम की क्षमता लगातार बढ़ रही है. हालांकि, ज़्यादा रिज़ॉल्यूशन वाली ऐसेट और रेंडरिंग के मुश्किल पाइपलाइन की वजह से, गेम में मेमोरी का इस्तेमाल, इस बढ़ोतरी से ज़्यादा तेज़ी से बढ़ रहा है.
मेमोरी का ज़्यादा इस्तेमाल करने की वजह से होने वाली मुख्य समस्याएं
- कम मेमोरी होने पर ऐप्लिकेशन बंद करने की सुविधा (एलएमके) की वजह से ऐप्लिकेशन बंद होना: ओएस, सिस्टम में मेमोरी की कमी को दूर करने के लिए, बैकग्राउंड में चल रहे या फ़ोरग्राउंड में चल रहे ऐप्लिकेशन को ज़बरदस्ती बंद कर देता है.
- फ़्रेम ड्रॉप होना और स्टट्रिंग (जैंक): मैनेज किए गए हीप या ओएस-लेवल मेमोरी स्वैपिंग में, बार-बार गार्बेज कलेक्शन (जीसी) स्पाइक होने से, प्रोसेसिंग में रुकावटें (बॉटलनेक) आती हैं.
- थर्मल थ्रॉटलिंग और तेज़ी से बैटरी खर्च होना: लगातार मेमोरी का बंटवारा करने, डीऐलोकेट करने, और मेमोरी पेज कमिट करने से, सीपीयू पर ज़्यादा लोड पड़ता है. इससे डिवाइस ज़्यादा गर्म होता है और बैटरी तेज़ी से खत्म होती है.
Android में मेमोरी मैनेजमेंट से जुड़े अपडेट
- Android 17:
MemoryLimiterकी सुविधा: Android 17 मेंMemoryLimiterकी सुविधा जोड़ी गई है. यह सुविधा, डिवाइस के हिसाब से तय की गई थ्रेशोल्ड के मुकाबले, ऐप्लिकेशन की मेमोरी के इस्तेमाल पर नज़र रखती है. मेमोरी की तय सीमा से ज़्यादा इस्तेमाल करने वाले ऐप्लिकेशन, सिस्टम लेवल पर तुरंत बंद हो जाते हैं. यह मैकेनिज़्म, एलएमके के पारंपरिक तरीके से ज़्यादा सख्त है. इसलिए, मेमोरी के ज़्यादा इस्तेमाल को मैनेज करना पहले से ज़्यादा ज़रूरी हो गया है.
Unity में मेमोरी का इस्तेमाल कम करना
Unity में, इंजन के इंटरनल मेमोरी पूल (नेटिव ब्लॉक ऐलोकेटर और मैनेज किए गए हीप) के बढ़ने के बाद, ज़्यादा लोड को मैनेज करने के लिए, इंजन उन मेमोरी पेजों को ओएस को तुरंत वापस करने के बजाय, सेव रखता है.
इसलिए, ज़्यादा ऐसेट अनलोड होने के बाद भी, रेज़िडेंट मेमोरी बढ़ जाती है. इससे ऐप्लिकेशन, Android ओएस प्रोसेस के बंद होने (जैसे, एलएमके या MemoryLimiter) के लिए ज़्यादा संवेदनशील हो जाता है.
ओएस-लेवल पर लागू होने से रोकने के लिए, मेमोरी ऑप्टिमाइज़ेशन को तीन मुख्य पहलुओं के ज़रिए मैनेज करना होगा:
- कैटगरी A: मेमोरी के ज़्यादा इस्तेमाल को कम करना
- कैटगरी B: ऐसेट और सिस्टम के गैर-ज़रूरी फ़ुटप्रिंट हटाना
- कैटगरी C: जीसी के गैर-ज़रूरी ऐलोकेशन हटाना
कैटगरी A: मेमोरी के ज़्यादा इस्तेमाल को कम करना
Unity के ऐलोकेटर, ओएस को तुरंत वापस करने के बजाय, दोबारा इस्तेमाल करने के लिए खाली किए गए मेमोरी पेजों को सेव रखते हैं. इसलिए, बेसलाइन मेमोरी, मौजूदा इस्तेमाल के बजाय, ज़्यादा से ज़्यादा इस्तेमाल को दिखाती है. इसलिए, स्पाइक को पहले से रोकना, बाद में साफ़-सफ़ाई पर निर्भर रहने से ज़्यादा असरदार है.
1. ज़्यादा बड़े AssetBundle इस्तेमाल करने से बचें
Unity के बंडल लोड करने के मैकेनिज़्म की वजह से, बड़े AssetBundle से मेमोरी का इस्तेमाल बहुत ज़्यादा बढ़ जाता है और ओओएम क्रैश हो जाते हैं. संसाधन को बेहतर तरीके से मैनेज करने के लिए, बंडल को मॉड्यूलर रखें.
मुख्य समस्याएं
- मेमोरी का ज़्यादा इस्तेमाल: किसी एक छोटी ऐसेट का अनुरोध करने पर, Unity को पूरे बंडल की फ़ाइल (जिसमें हेडर, मेटाडेटा, और स्ट्रीमिंग बफ़र शामिल हैं) को रैम में लोड करना पड़ता है.
- अनलोड करने में समस्या: अगर किसी बंडल में मौजूद ऐसेट, इस्तेमाल में है, तो पूरे बंडल को अनलोड नहीं किया जा सकता. इससे, इस्तेमाल न किया गया डेटा रैम में सेव रहता है.
सबसे सही तरीके
- बंडल को मॉड्यूलर रखें: ऐसेट को सीन या लाइफ़साइकल के हिसाब से ग्रुप करें.
- Unity 6.6 या इसके बाद के वर्शन के लिए सलाह: अनचाही क्रॉस-बंडल डिपेंडेंसी से बचने के लिए, कॉन्टेंट डायरेक्ट्री का इस्तेमाल करें.
2. ScriptableObjects में ऐसेट रेफ़रंस ऑप्टिमाइज़ करना
ScriptableObject में, UnityEngine.Object के सीरियललाइज़ किए गए फ़ील्ड, डायरेक्ट हार्ड रेफ़रंस बनाते हैं. इससे, रेफ़रंस की गई सभी ऐसेट, ScriptableObject के लोड या इंस्टैंशिएट होते ही रैम में लोड हो जाती हैं.
// BEFORE: Loading SceneRequiredAssets forces _worldAsset and _spawnSettings into RAM immediately
public class SceneRequiredAssets : ScriptableObject
{
public string sceneName;
public Object _worldAsset;
public Object _spawnSettings;
}
// AFTER: Use AssetReference to enable asynchronous, on-demand loading using Addressables
public class SceneRequiredAssets : ScriptableObject
{
public string sceneName;
public AssetReference _worldAsset;
public AssetReference _spawnSettings;
}
3. ऑडियो क्लिप लोड करने के टाइप कॉन्फ़िगर करना
सभी ऑडियो क्लिप को बिना कंप्रेस किए सीधे मेमोरी में लोड करने से, मेमोरी का इस्तेमाल बहुत ज़्यादा बढ़ जाता है और यह समस्या हमेशा बनी रहती है. ऑडियो लोड करने की सेटिंग, उनके इस्तेमाल के हिसाब से कॉन्फ़िगर की जानी चाहिए:
| ऑडियो कैटगरी | लोड करने का टाइप | कारण |
|---|---|---|
| बीजीएम (बैकग्राउंड म्यूज़िक) | स्ट्रीमिंग | मेमोरी स्पाइक से बचने के लिए, डिस्क से ऑडियो को छोटे बफ़र में स्ट्रीम करता है. |
| लंबे साउंड इफ़ेक्ट | मेमोरी में कंप्रेस किया गया | रैम पर लोड कम रखता है और प्लेबैक के दौरान ऑडियो को तुरंत डीकंप्रेस करता है. |
| छोटे और बार-बार इस्तेमाल होने वाले साउंड इफ़ेक्ट | लोड होने पर डीकंप्रेस करें | प्लेबैक के दौरान रनटाइम सीपीयू ओवरहेड से बचने के लिए, लोड होने पर ऑडियो को रैम में डीकंप्रेस करता है. |
4. ऑब्जेक्ट पूलिंग और रिलीज़ करने की रणनीतियां लागू करना
सीन ट्रांज़िशन के दौरान, ऑब्जेक्ट पूल में सेव किए गए अनरिलीज़ किए गए इंस्टेंस, रिज़र्व की गई मेमोरी को हमेशा के लिए सेव रखते हैं. इससे, बेसलाइन मेमोरी फ़ुटप्रिंट ज़रूरत से ज़्यादा बढ़ जाता है.
- कार्रवाई: सीन ट्रांज़िशन या कम गतिविधि वाली अवधियों के दौरान, समय-समय पर पूल किए गए इस्तेमाल न किए गए ऑब्जेक्ट को साफ़ करें या ट्रिम करें. इससे Unity को उस रिज़र्व की गई जगह को अन्य ऐलोकेशन के लिए वापस करने या दोबारा इस्तेमाल करने की अनुमति मिलती है.
कैटगरी B: मेमोरी के गैर-ज़रूरी इस्तेमाल को कम करना
रिडंडेंट ग्राफ़िक ऐसेट और रेंडरिंग टारगेट बफ़र हटाने से, किसी प्रोग्राम के चलने के दौरान, इस्तेमाल की जाने वाली मेमोरी (मेमोरी फ़ुटप्रिंट) सीधे तौर पर कम हो जाती है.
1. रेंडर टेक्सचर और कैमरे की डेप्थ ऑप्टिमाइज़ करना
- डेप्थ/स्टेंसिल बफ़र हटाना: सिर्फ़ रंग का डेटा दिखाने वाले रेंडर टेक्सचर के लिए, डेप्थ स्टेंसिल फ़ॉर्मैट को कोई नहीं पर सेट करें.
- यूआई कैमरे की डेप्थ टेक्सचर बंद करना: यूआई कैमरे के लिए, जहां डेप्थ डेटा की
ज़रूरत नहीं है, वहां यूआरपी कैमरा सेटिंग में डेप्थ टेक्सचर जनरेशन बंद करें. इससे
CopyDepthपास और उससे जुड़ी जीपीयू टेक्सचर मेमोरी हट जाती है.
2. टेक्सचर और मेश ऑप्टिमाइज़ करना
| कैटगरी | ऑप्टिमाइज़ेशन के लिए दिशा-निर्देश |
|---|---|
| टेक्सचर कंप्रेस करना | टारगेट प्लैटफ़ॉर्म के कंप्रेस करने के फ़ॉर्मैट हमेशा लागू करें. उदाहरण के लिए, Android के लिए ASTC. |
| पढ़ें/लिखें की सुविधा चालू करना | ज़रूरत न होने पर, इसे बंद रखें. इसे चालू करने से, सीपीयू और जीपीयू रैम में टेक्सचर मेमोरी की डुप्लीकेट कॉपी बन जाती है. |
| मिपमैप | यूआई टेक्सचर या कैमरे से तय दूरी पर मौजूद ऑब्जेक्ट के लिए, मिपमैप बंद करें. इससे ~33% टेक्सचर मेमोरी सेव होती है. |
| मेश की जटिलता | जीपीयू और नेटिव मेमोरी फ़ुटप्रिंट को कम करने के लिए, गैर-ज़रूरी पॉलीगॉन की संख्या और वर्टेक्स स्ट्रीम कम करें. |
3. शेडर वैरिएंट हटाना और मेमोरी ऑप्टिमाइज़ करना
यूबर-शेडर (उदाहरण के लिए, यूआरपी लिट शेडर), #multi_compile और shader_feature कीवर्ड का इस्तेमाल करके, कई सुविधाओं को शामिल करते हैं. ऑप्टिमाइज़ेशन के बिना,
कॉम्बिनेटरियल एक्सप्लोज़न से हज़ारों यूनीक
शेडर वैरिएंटबनते हैं. इससे, बिल्ड साइज़ बढ़ जाता है, नेटिव मेमोरी का इस्तेमाल बहुत ज़्यादा बढ़ जाता है, और गेमप्ले के दौरान जीपीयू ड्राइवर कंपाइलेशन में समस्याएं आती हैं.
A. शेडर वैरिएंट मेमोरी ओवरहेड का मैकेनिज़्म
- कॉम्बिनेटरियल एक्सप्लोज़न: हर जोड़े गए कीवर्ड ग्रुप के साथ, कुल संभावित वैरिएंट तेज़ी से बढ़ते हैं.
- चंक ऐलोकेशन आर्किटेक्चर: Unity, कंपाइल किए गए बाइनरी वैरिएंट को कंप्रेस किए गए मेमोरी ब्लॉक में ग्रुप करता है. इन्हें चंक कहा जाता है. डिफ़ॉल्ट तौर पर, इनका साइज़ 4 एमबी होता है.
- नेटिव मेमोरी का ज़्यादा इस्तेमाल: जब रनटाइम कोड, किसी एक वैरिएंट का भी अनुरोध करता है किसी चंक में, तो पूरा 4 एमबी का चंक रैम में डीकंप्रेस हो जाता है. अगर ऑप्टिमाइज़ नहीं किया जाता है, तो उन चंक में पैकेज किए गए हज़ारों इस्तेमाल न किए गए वैरिएंट, नेटिव मेमोरी में हमेशा के लिए सेव रहते हैं.
B. Unity का बिल्ट-इन मल्टी-स्टेज स्ट्रिपिंग पाइपलाइन: Unity, टारगेट प्लैटफ़ॉर्म की ग्राफ़िक्स सेटिंग और इस्तेमाल न की गई इंजन सुविधाओं (उदाहरण के लिए, फ़ॉग, लाइटमैप, और एक्सआर सेटिंग) के आधार पर, बिल्ड के समय गैर-ज़रूरी वैरिएंट को अपने-आप हटा देता है. इसके अलावा, shader_feature वैरिएंट अपने-आप फ़िल्टर हो जाते हैं. ऐसा तब होता है, जब प्रोजेक्ट में मौजूद कोई भी मटीरियल, उनके कीवर्ड का इस्तेमाल नहीं करता. वहीं, #multi_compile वैरिएंट को इस्तेमाल के बावजूद, ज़बरदस्ती शामिल किया जाता है.
C. कस्टम ऑटोमेटेड स्ट्रिपिंग आर्किटेक्चर
(IPreprocessShaders) स्टैटिक विश्लेषण, रनटाइम C# स्क्रिप्ट
(Material.EnableKeyword) का इस्तेमाल करके, डाइनैमिक तरीके से बदले गए कीवर्ड का पता नहीं लगा सकता.
इसलिए, स्टैंडर्ड स्ट्रिपिंग अक्सर काफ़ी नहीं होती. यह पक्का करने के लिए कि सिर्फ़ इस्तेमाल किए गए वैरिएंट शामिल किए जाएं, क्यूए टेस्ट सुइट के दौरान Player.log का इस्तेमाल करके वैरिएंट इकट्ठा किए जा सकते हैं. इसके लिए, Editor की सेटिंग में Log Shader Compilation चालू करें. इसके अलावा, Profiler Traces (Shader.CreateGPUProgram मार्कर) का इस्तेमाल करके भी वैरिएंट इकट्ठा किए जा सकते हैं. इसके बाद, रनटाइम के दौरान कभी भी एक्ज़ीक्यूट नहीं किए गए वैरिएंट को फ़िल्टर करने के लिए, Editor स्क्रिप्ट में
IPreprocessShaders.OnProcessShader लागू करें. इससे, बिल्ड में सिर्फ़ ज़रूरी
वैरिएंट शामिल किए जा सकेंगे.
कैटगरी C: जीसी के गैर-ज़रूरी ऐलोकेशन हटाना
मैनेज किए गए हीप में, गार्बेज कलेक्शन (जीसी) ऐलोकेशन की वजह से, मेमोरी फ़्रैगमेंटेशन, हीप एक्सपैंशन होता है. साथ ही, जीसी पॉज़ के दौरान फ़्रेम ड्रॉप की समस्या भी होती है.
1. लैम्डा क्लोज़र ऐलोकेशन से बचना
जब कोई लैम्डा एक्सप्रेशन, बाहरी लोकल वैरिएबल कैप्चर करता है, तो C#, हीप पर एक इंप्लिसिट डिसप्ले क्लास जनरेट करता है. Update के अंदर इसे एक्ज़ीक्यूट करने से, हर फ़्रेम में क्लोज़र इंस्टेंस ऐलोकेट होते हैं.
// Bad: Capturing local variable 'targetId' allocates a new closure object on the Heap every frame
void Update()
{
int targetId = 100;
Monster target = monsterList.Find(m => m.Id == targetId);
}
// Good 1: Replace with a standard 'for' loop (Recommended: 0 B allocation)
void Update()
{
int targetId = 100;
Monster target = null;
for (int i = 0; i < monsterList.Count; i++)
{
if (monsterList[i].Id == targetId)
{
target = monsterList[i];
break;
}
}
}
// Good 2: Use a static lambda (C# 9.0+) if no outer variables are captured
Monster target = monsterList.Find(static m => m.Id == 100);
2. stackalloc और Span का इस्तेमाल करना
स्टैक मेमोरी का इस्तेमाल करके, कम समय के लिए इस्तेमाल होने वाली अस्थायी ऐरे के लिए, हीप ऐलोकेशन से बचें.
// Before: Allocates an array on the Heap every call (GC Target)
Vector2[] pos = new Vector2[4];
// After: Utilizes Stack memory using System.Span (0 B Heap Allocation)
System.Span<Vector2> pos = stackalloc Vector2[4];
3. कलेक्शन इटरेट करना ऑप्टिमाइज़ करना
LINQ एक्सटेंशन या ReadOnlyCollection ऐक्सेसर की वजह से होने वाले बॉक्सिंग और एन्यूमरेटर हीप ऐलोकेशन से बचें.
// Before: LINQ Count() causes internal GetEnumerator() heap allocations
bool hasData = component != null && component.parameters.Count(parameter => parameter.overrideState) > 0;
// After: Replaced with indexer and direct loop iteration
bool hasData = HasDataOptimized(component);
private bool HasDataOptimized(TestComponent component)
{
if (component == null) return false;
var count = component.parameters.Count;
for (var i = 0; i < count; ++i)
{
if (component.parameters[i].overrideState)
return true;
}
return false;
}
4. जीसी ऐलोकेशन से बचने के लिए अतिरिक्त नियम
Camera.allCamerasका इस्तेमाल न करें. ऐसा इसलिए, क्योंकि हर कॉल पर, यह हीप पर एक नईCamera[]ऐरे जनरेट करता है. इसके बजाय, कैमरा ऐरे को कैश करें और उसेCamera.GetAllCameras(_allCameras)को पास करें.- पर
IReadOnlyList<T>काforeachइस्तेमाल न करें: इंटरफ़ेस पर इटरेट करने से, स्ट्रक्ट एन्यूमरेटर बॉक्सिंग होती है. इससे, जीसी ऐलोकेशन जनरेट होते हैं. इसके बजाय, स्टैंडर्डforलूप का इस्तेमाल करें. - कोरोटीन ऑब्जेक्ट कैश करना:
WaitForSecondsइंस्टेंस को कैश करें बजाय इसके किyield return new WaitForSeconds(time);को बार-बार इंस्टैंशिएट करें. - डिक्शनरी में कस्टम स्ट्रक्ट की: डिक्शनरी
की के तौर पर कस्टम स्ट्रक्ट का इस्तेमाल करने से, डिफ़ॉल्ट
Equalsकॉल होता है. इससे, ऑब्जेक्ट बॉक्सिंग ट्रिगर होती है.IEqualityComparer<T>लागू करें और इसे डिक्शनरी कंस्ट्रक्टर को पास करें.
public struct TypeKey
{
public int v1;
public int v2;
public class TypeKeyComparer : IEqualityComparer<TypeKey>
{
public bool Equals(TypeKey x, TypeKey y) => x.v1 == y.v1 && x.v2 == y.v2;
public int GetHashCode(TypeKey obj) => obj.v1.GetHashCode() ^ obj.v2.GetHashCode();
}
}
// Pass custom comparer during Dictionary initialization to prevent boxing
public readonly Dictionary<TypeKey, int> _typeKeyDictionary = new(new TypeKey.TypeKeyComparer());
5. जेनेरिक और रिफ़्लेक्शन का इस्तेमाल कम करना
जेनेरिक तरीके, कोड को दोबारा इस्तेमाल करने और उसे मैनेज करने के लिए बेहतरीन होते हैं. हालांकि, इनका ज़्यादा इस्तेमाल करने से, Unity IL2CPP (इंटरमीडिएट लैंग्वेज से C++) बैकएंड के संदर्भ में, आपके प्रोजेक्ट पर बुरा असर पड़ सकता है.
- IL2CPP कोड का ज़्यादा इस्तेमाल: हर यूनीक जेनेरिक टाइप कॉम्बिनेशन के लिए, IL2CPP कोड का एक खास वर्शन जनरेट करता है. मुश्किल जेनेरिक का ज़्यादा इस्तेमाल करने से, जनरेट किए गए C++ कोड का "कॉम्बिनेटरियल एक्सप्लोज़न" हो सकता है. इससे, ऐप्लिकेशन का बाइनरी साइज़ और नेटिव मेमोरी फ़ुटप्रिंट काफ़ी बढ़ जाता है.
- रिफ़्लेक्शन ओवरहेड: रिफ़्लेक्शन का इस्तेमाल करने वाले तरीके, जैसे कि
System.Reflectionएपीआई, स्वाभाविक रूप से धीमे होते हैं और अक्सर रनटाइम के दौरान हीप ऐलोकेशन की वजह बनते हैं. - सबसे सही तरीका: जेनेरिक का इस्तेमाल सोच-समझकर करें. इन्हें,
आर्किटेक्चर की साफ़ तौर पर जानकारी देने के लिए प्राथमिकता दें. न कि बिना सोचे-समझे, हर जगह इस्तेमाल करने के लिए. जहां परफ़ॉर्मेंस ज़रूरी है, वहां कॉन्क्रीट टाइप या इंटरफ़ेस-आधारित पॉलीमॉर्फ़िज़्म का इस्तेमाल करें. रिफ़्लेक्शन के लिए, अपडेट लूप में क्वेरी करने के बजाय, शुरू करने के दौरान
MethodInfoयाFieldInfoजैसे नतीजों को कैश करें.
6. मैनेज किए गए शेल के लीक होने से बचना
MonoBehaviour, Texture, या GameObject जैसे हर UnityEngine.Object में, एक C# "मैनेज किया गया शेल" रैपर होता है. यह रैपर, नेटिव C++ इंजन के साथ कम्यूनिकेट करता है.
- समस्या: अगर किसी मैनेज किए गए शेल को स्टैटिक रेफ़रंस, परसिस्टेंट इवेंट सदस्यता या अनक्लीन क्लोज़र की मदद से मेमोरी में सेव रखा जाता है, तो जीसी उस मेमोरी को वापस नहीं ले सकता. नेटिव ऑब्जेक्ट के नष्ट होने के बाद भी, मैनेज किया गया रैपर बना रहता है. इससे "घोस्ट" मेमोरी लीक होती है. इससे, मैनेज किए गए हीप का इस्तेमाल बढ़ जाता है.
- समाधान: हमेशा मज़बूत क्लीनअप पैटर्न लागू करें. ऑब्जेक्ट को नष्ट करते समय या सीन ट्रांज़िशन के दौरान,
-=ऑपरेटर का इस्तेमाल करके, इवेंट से साफ़ तौर पर सदस्यता रद्द करें. साथ ही,UnityEngine.Objectटाइप के स्टैटिक रेफ़रंस को शून्य करें. इस क्लीनअप से, जीसी रैपर को सफलतापूर्वक इकट्ठा कर सकता है. ऐसा तब होता है, जब नेटिव इंजन अपना हैंडल रिलीज़ कर देता है.