Modern mobil cihazların fiziksel RAM kapasitesi artmaya devam etse de yüksek çözünürlüklü öğeler ve karmaşık oluşturma ardışık düzenleri nedeniyle oyunların bellek tüketimi bu artıştan daha hızlı bir şekilde artıyor.
Aşırı bellek kullanımının neden olduğu temel sorunlar
- Düşük bellek sonlandırıcı (LMK) sonlandırmaları: İşletim sistemi, sistem genelindeki bellek yetersizliklerini çözmek için arka plandaki ve hatta ön plandaki uygulamaları zorla kapatır.
- Kare düşmesi ve takılma (jank): Yönetilen yığında veya işletim sistemi düzeyinde bellek değiştirme işleminde sık sık çöp toplama (GC) artışları, işleme darboğazları oluşturur.
- Termal kısıtlama ve pilin hızlı tükenmesi: Sürekli bellek ayırma, ayırmayı kaldırma ve bellek sayfası kaydetme işlemleri, CPU'ya önemli bir yük bindirerek ısının artmasına ve pilin daha hızlı tükenmesine neden olur.
Android'de bellek yönetimiyle ilgili güncellemeler
- Android 17:
MemoryLimiterile tanışın: Android 17, uygulama bellek tüketimini cihaza özel eşiklere göre aktif olarak izleyenMemoryLimiterözelliğini sunuyor. Bellek sınırını aşan uygulamalar sistem düzeyinde anında sonlandırılır. Bu mekanizma, geleneksel LMK'den daha katı olduğundan en yüksek bellek kullanımını yönetmek artık her zamankinden daha önemli.
Unity'de bellek kullanımını azaltma
Unity'de motor, yüksek bir yükü karşılamak için dahili bellek havuzlarını (Yerel Blok Ayırıcılar ve Yönetilen Yığın) genişlettikten sonra bu bellek sayfalarını hemen OS'ye döndürmek yerine saklar.
Bu nedenle, ağır öğeler kaldırıldıktan sonra bile temel yerleşik bellek şişkin kalır ve uygulama, Android OS işlem sonlandırmalarına (ör. LMK veya MemoryLimiter) karşı son derece savunmasız hale gelir.
İşletim sistemi düzeyinde yaptırımları önlemek için bellek optimizasyonu üç temel unsur üzerinden ele alınmalıdır:
- A kategorisi: En yüksek bellek kullanımını azaltma
- B Kategorisi: Gereksiz öğe ve sistem izlerini ortadan kaldırın
- C kategorisi: Gereksiz GC ayırmalarını ortadan kaldırma
A kategorisi: En yüksek bellek kullanımını azaltma
Unity'nin ayırıcıları, serbest bırakılan bellek sayfalarını hemen işletim sistemine döndürmek yerine yeniden kullanım için saklar. Bu nedenle, temel bellek mevcut kullanım yerine ulaşılan en yüksek noktayı yansıtma eğilimindedir. Bu nedenle, ani artışları önlemek, sonradan temizleme yapmaktan daha etkilidir.
1. Çok büyük AssetBundle'lardan kaçının
Unity'nin Bundle Loading mekanizması nedeniyle, büyük AssetBundle'lar ciddi bellek şişmesine ve bellek yetersizliği (OOM) kilitlenmelerine yol açar. Verimli kaynak yönetimi için paketleri modüler tutun.
Önemli sorunlar
- Yüksek bellek ek yükü: Tek bir küçük öğe istenmesi, Unity'nin tüm paket dosyasını (başlıklar, meta veriler ve akış arabellekleri dahil) RAM'e yüklemesine neden olur.
- Kaldırma tuzağı: Bir paketteki herhangi bir öğe etkin olarak kullanılıyorsa paketin tamamı kaldırılamaz ve kullanılmayan veriler RAM'de kalır.
En iyi uygulamalar
- Paketleri modüler tutun: Öğeleri sahneye veya yaşam döngüsüne göre mantıksal olarak gruplandırın.
- Unity 6.6+ ipucu: İstenmeyen paketler arası bağımlılıkları önlemek için Content Directories'i kullanın.
2. ScriptableObjects içinde öğe referanslarını optimize etme
Bir ScriptableObject içindeki doğrudan UnityEngine.Object serileştirilmiş alanlar, doğrudan sabit referanslar oluşturarak ScriptableObject yüklendiğinde veya örneklendirildiğinde tüm referans verilen öğelerin RAM'e yüklenmesini zorunlu kılar.
// 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. Ses kliplerini ve kanallarını yapılandırma
Tüm ses kliplerini sıkıştırılmamış olarak doğrudan belleğe yüklemek, büyük ve kalıcı bellek zirveleri oluşturur. Ses yükleme ayarları, kullanım profillerine göre yapılandırılmalıdır:
| Ses kategorisi | Yükleme kapağı | Neden |
|---|---|---|
| BGM (Background Music) | Akış | Bellek kullanımındaki ani artışları önlemek için sesi diskten küçük arabellekler halinde aktarır. |
| Uzun ses efektleri | Bellekte Sıkıştırılmış | RAM kullanımını düşük tutar ve oynatma sırasında sesi anında açar. |
| Kısa ve Sık Ses Efektleri | Yükleme Sırasında Sıkıştırılmış Dosyayı Aç | Oynatma sırasında çalışma zamanı CPU yükünü önlemek için yükleme sırasında sesi RAM'e açar. |
Bellek ek yükünü daha da azaltmak için yük türlerine ek olarak aşağıdaki ses yapılandırmalarını uygulayın:
- Yönlü olmayan ses için Mono'ya Zorla'yı etkinleştirin: Kullanıcı arayüzü ses efektleri, seslendirmeler ve 3D uzamsallaştırılmış ses efektleri gibi birçok ses öğesi stereo kanal ayrımı gerektirmez. Bu öğelerin
AudioClipiçe aktarma ayarlarında Tek Kanallı Olarak Zorla'yı etkinleştirerek bellek ayak izlerini ve arabellek boyutlarını yarıya (%50 azalma) indirin. - Cihaz katmanına göre etkin ses kanallarını sınırlama: Her etkin sesli ses, yerel DSP karıştırıcı ve sıkıştırmayı açma arabellekleri ayırır. Belleği az olan cihazlarda, eşzamanlı etkin kanal sayısını azaltın. Örneğin, Unity'de
AudioSettings.Resetkullanarak Max Real Voices değerini düşürün (ör. 32'den 16'ya) veya üçüncü taraf ses motorlarında yazılım oynatma kanallarını kısıtlayın.
4. Nesne havuzu oluşturma ve yayınlama stratejilerini uygulama
Sahne geçişlerinde Nesne Havuzları içinde kalan yayınlanmamış örnekler, ayrılmış belleği süresiz olarak tutar ve temel bellekte kaplanan yerin gereksiz yere şişmesine neden olur.
- İşlem: Sahne geçişleri veya düşük etkinlik dönemlerinde kullanılmayan havuzlanmış nesneleri düzenli olarak temizleyin ya da kırpın. Böylece Unity, ayrılmış alanı diğer ayırmalar için döndürebilir veya yeniden kullanabilir.
B kategorisi: Gereksiz bellek kullanımını ortadan kaldırma
Gereksiz grafik öğelerini ortadan kaldırmak ve hedef arabellekleri doğrudan oluşturmak, temel bellekte kaplanan yeri azaltır.
1. Render dokularını ve kamera derinliğini optimize etme
- Derinlik/kalıp arabelleklerini kaldırın: Yalnızca renk verisi gerektiren dokuları oluşturmak için derinlik kalıp biçimini Yok olarak ayarlayın.
- Kullanıcı arayüzü kamerası derinlik dokusunu devre dışı bırakın: Derinlik verilerinin gerekli olmadığı kullanıcı arayüzü kameralarında,
CopyDepthgeçişini ve ilişkili GPU doku belleğini ortadan kaldırmak için URP kamera ayarlarında derinlik dokusu oluşturmayı devre dışı bırakın.
2. Dokuları ve örgüleri optimize etme
| Kategori | Optimizasyon yönergesi |
|---|---|
| Doku sıkıştırma | Her zaman hedef platform sıkıştırma biçimlerini (ör. Android için ASTC) uygulayın. |
| Read/Write etkin | Gerekmedikçe devre dışı bırakın. Bu ayarın etkinleştirilmesi, doku belleğini CPU ve GPU RAM'i arasında kopyalar. |
| Mipmap'ler | Kullanıcı arayüzü dokuları veya sabit bir kamera mesafesine sabitlenmiş nesneler için mipmap'leri devre dışı bırakarak doku belleğinden yaklaşık% 33 tasarruf edin. |
| Mesh karmaşıklığı | GPU ve yerel bellekte kaplanan yeri azaltmak için gereksiz çokgen sayısını ve köşe akışlarını azaltın. |
3. Gölgeleyici varyantlarını kaldırma ve belleği optimize etme
Uber gölgelendiriciler (ör. URP Lit Shader), #multi_compile ve shader_feature anahtar kelimelerini kullanarak çok sayıda özelliği kapsar. Optimizasyon yapılmadığında, kombinatoryal patlama nedeniyle on binlerce benzersiz gölgelendirici varyantı oluşturulur. Bu durum, derleme boyutlarının şişmesine, yerel bellek tüketiminin büyük ölçüde artmasına ve oyun sırasında GPU sürücüsü derlemesinin aksamasına neden olur.
A. Gölgeleyici varyantı bellek ek yükünün mekaniği
- Kombinasyon patlaması: Toplam olası varyant sayısı, eklenen her anahtar kelime grubuyla birlikte katlanarak artar.
- Parça ayırma mimarisi: Unity, derlenmiş ikili varyantları parça adı verilen sıkıştırılmış bellek blokları halinde gruplandırır (varsayılan: 4 MB).
- Yerel bellek şişmesi: Çalışma zamanı kodu, bir parçanın içinde tek bir varyant bile istediğinde 4 MB'lık parçanın tamamı RAM'de açılır. Optimize edilmezse bu parçalarda paketlenen binlerce kullanılmayan varyant, yerel belleği kalıcı olarak işgal eder.
B. Unity'nin yerleşik çok kademeli ayıklama ardışık düzeni: Unity, hedef platform grafik ayarlarına ve kullanılmayan motor özelliklerine (ör. sis, ışık haritaları ve XR ayarları) göre derleme sırasında gereksiz varyantları otomatik olarak ayıklar. Ayrıca, anahtar kelimeleri projedeki herhangi bir materyal tarafından etkin bir şekilde kullanılmıyorsa shader_feature varyantları otomatik olarak filtrelenir. #multi_compile varyantları ise kullanımdan bağımsız olarak zorunlu şekilde dahil edilir. Ancak URP veya ardışık düzen ayarlarınızda oluşturma ya da son işleme özellikleri (ör. ek ışıklar, gerçek zamanlı gölgeler veya kullanılmayan UberPost efektleri) etkin kalırsa Unity, içeriğinizin bunları gerçekten kullanıp kullanmadığını belirleyemez ve ilgili tüm #multi_compile varyantlarını oluşturur. Kullanılmayan işlem hattı ayarlarını denetleyip devre dışı bırakarak gerçek oluşturma gereksinimlerinize uygun hale getirin ve gereksiz varyant oluşturulmasını önleyin.
C. Özel otomatik kaldırma mimarisi
(IPreprocessShaders) Statik analiz, çalışma zamanı C# komut dosyaları (Material.EnableKeyword) kullanılarak dinamik olarak değiştirilen anahtar kelimeleri algılayamadığından standart kaldırma genellikle yeterli değildir. Yalnızca gerçekten kullanılan varyantların dahil edilmesini sağlamak için Player.log (Editor ayarlarında Log Shader Compilation'ı etkinleştirerek) veya Profiler Traces (Shader.CreateGPUProgram işaretleri) kullanarak QA test paketleri sırasında varyantları toplayabilirsiniz. Ardından, çalışma zamanında hiçbir zaman yürütülmeyen varyantları filtrelemek için bir düzenleyici komut dosyasında IPreprocessShaders.OnProcessShader işlevini uygulayın. Böylece, derlemede yalnızca gerekli olan varyantlar kalır.
D. Gereksiz gölgelendiricileri birleştirin: Lit ve Simple Lit gibi uber gölgelendiriciler, çok sayıda #multi_compile yönergesi içerir ve çok sayıda varyant oluştururken önemli miktarda bellek tüketir:
- Eşzamanlı kullanımı profilleme: Her iki gölgelendiricinin de aynı anda önemli miktarda bellek tüketip tüketmediğini kontrol etmek için bellek kullanımınızı profilleme ve projenizin her ikisine de gerçekten ihtiyacı olup olmadığını doğrulama.
- Tek bir gölgelendiriciyi standart hale getirin: Fiziksel tabanlı oluşturma (PBR) gerekli olmadığında öğelerinizi, gereksiz Lit gölgelendirici kullanımını ortadan kaldıracak ve Simple Lit'i standart hale getirecek şekilde yapılandırın (örneğin, dışa aktarılan FBX modellerinde içe aktarma sırasında yanlışlıkla uygulanmış varsayılan Lit malzemeleri olup olmadığını kontrol edin).
C kategorisi: Gereksiz GC ayırmalarını ortadan kaldırma
Yönetilen yığında çöp toplama (GC) ayırmaları, bellek parçalanmasına, hiçbir zaman küçülmeyen yığın genişlemelerine ve GC duraklamaları sırasında ciddi kare düşmelerine neden olur.
1. Lambda kapatma tahsislerini önleme
Bir lambda ifadesi dış yerel değişkenleri yakaladığında C#, yığında örtülü bir görüntüleme sınıfı oluşturur. Bu işlemi Update içinde yürütmek, her karede closure örnekleri ayırır.
// 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 ve Span kullanma
Yığın belleği kullanarak kısa ömürlü geçici diziler için yığın ayırmalarından kaçının.
// 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. Koleksiyon yinelemesini optimize etme
LINQ uzantıları veya ReadOnlyCollection erişimcilerinin neden olduğu boks ve Enumerator yığın ayırmalarından kaçının.
// 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. Ek GC tahsisini önleme kuralları
- Her çağrıda yığında yeni bir
Camera[]dizisi oluşturduğu içinCamera.allCameraskullanmaktan kaçının. Bunun yerine, bir kamera dizisini önbelleğe alın veCamera.GetAllCameras(_allCameras)yöntemine iletin. IReadOnlyList<T>üzerindeforeach'den kaçının: Bir arayüz üzerinde yineleme yapmak, yapı numaralandırıcı kutulamasına neden olarak GC ayırmaları oluşturur. Bunun yerine standart birfordöngüsü kullanın.- Coroutine nesnelerini önbelleğe alma:
yield return new WaitForSeconds(time);öğesini tekrar tekrar oluşturmak yerineWaitForSecondsörneklerini önbelleğe alın. - Sözlüklerde özel yapı anahtarları: Sözlük anahtarları olarak özel yapıların kullanılması, varsayılan
Equalsöğesini çağırarak nesne boks işlemini tetikler.IEqualityComparer<T>öğesini uygulayın ve Dictionary oluşturucusuna iletin.
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. Genel ve yansıma kullanımını en aza indirme
Genel yöntemler, kodun yeniden kullanılması ve sürdürülebilirliği açısından mükemmel olsa da aşırı kullanılması, Unity IL2CPP (Intermediate Language to C++) arka ucu bağlamında projenizi olumsuz etkileyebilir.
- IL2CPP kod şişmesi: IL2CPP, her benzersiz genel tür kombinasyonu için kodun özel bir sürümünü oluşturur. Karmaşık jeneriklerin aşırı kullanımı, oluşturulan C++ kodunda "kombinasyon patlamasına" yol açabilir. Bu durum, uygulamanın ikili boyutunu ve yerel bellek ayak izini önemli ölçüde artırır.
- Yansıtma ek yükü: Yansıtma kullanan yöntemler (ör.
System.ReflectionAPI'leri) doğası gereği yavaştır ve genellikle çalışma zamanında yığın ayırmalarına neden olur. - En iyi uygulama: Jenerikleri dikkatli kullanın. Geniş ve ayrım gözetmeyen uygulamalar yerine mimari netlik için öncelik verin. Performansın kritik olduğu durumlarda somut türleri veya arayüze dayalı polimorfizmi tercih edin. Yansıtma için güncelleme döngüsünde sorgulamak yerine başlatma sırasında
MethodInfoveyaFieldInfogibi önbellek sonuçlarını kullanın.
6. Yönetilen kabukların sızdırılmasını önleme
MonoBehaviour, Texture veya GameObject gibi her UnityEngine.Object, yerel C++ motoruyla iletişim kuran bir C# "Yönetilen Kabuk" sarmalayıcısına sahiptir.
- Sorun: Yönetilen bir kabuk, statik bir referans, kalıcı bir etkinlik aboneliği veya temizlenmemiş bir kapatma tarafından bellekte tutuluyorsa GC, belleği geri alamaz. Doğal nesne yok edilse bile yönetilen sarmalayıcı kalıcı olur. Bu da Yönetilen Yığın'ı şişiren "hayalet" bellek sızıntılarına yol açar.
- Çözüm: Her zaman sağlam temizleme kalıpları uygulayın. Nesneleri yok ederken veya sahneleri dönüştürürken
-=operatörünü kullanarak etkinliklerin aboneliğinden açıkça çıkın veUnityEngine.Objecttürlerine yönelik statik referansları geçersiz kılın. Bu temizleme işlemi, yerel motor tanıtıcısını serbest bıraktıktan sonra GC'nin sarmalayıcıyı başarıyla toplayabilmesini sağlar.
7. Adreslenebilir öğeler için ikili kataloglar kullanma
Unity 2022 ve önceki sürümlerde Addressables, ikili kataloglar yerine varsayılan olarak JSON kataloglarını kullanır (Unity 2023 ve sonraki sürümlerde varsayılan olarak ikili kataloglar kullanılır). Projeniz JSON katalogları kullanıyorsa başlangıçta büyük bir katalog yüklemek, dosyanın tamamını tek bir devasa C# dizesine okur ve JSON ayrıştırma ile Base64 kod çözme sırasında binlerce yönetilen dize ve geçici byte[] dizisi tahsis eder. Bu durum, Yönetilen Yığın'ı kalıcı olarak genişleten büyük bir GC ayırma artışına neden olur. Bu nedenle, kompakt verileri doğrudan yayınlamak için ikili katalog kullanın.