Modern mobil cihazların fiziksel RAM kapasitesi artmaya devam etse de yüksek çözünürlüklü öğeler ve karmaşık oluşturma işlem hatları nedeniyle oyunların bellek tüketimi bu artışın önüne geçiyor.
Aşırı bellek kullanımının neden olduğu temel sorunlar
- Düşük bellek sorunu (LMK) nedeniyle sonlandırmalar: İşletim sistemi, sistem genelindeki bellek yetersizliğini gidermek için arka plandaki veya hatta ön plandaki uygulamaları zorla kapatır.
- Kare düşmesi ve takılma (jank): Yönetilen Yığında veya OS düzeyinde sık sık atık toplama (GC) zirveleri yaşanması, işleme darboğazları oluşturur.
- Termal kısma ve pilin hızlı tükenmesi: Sürekli bellek ayırma, bellekten çıkarma ve bellek sayfası işlemleri önemli CPU ek yüküne neden olur. Bu durum, ısının artmasına ve pilin daha hızlı tükenmesine yol açar.
Android bellek yönetimi güncellemeleri
- Android 17:
MemoryLimiterözelliğinin kullanıma sunulması: Android 17, uygulama belleği tüketimini cihaza özel eşiklere göre etkin bir şekilde izleyenMemoryLimiterözelliğini kullanıma 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 tepe yükünü karşılamak için dahili bellek havuzlarını (Native Block Allocators ve Managed Heap) genişlettikten sonra bu bellek sayfalarını işletim sistemine hemen döndürmek yerine saklar.
Sonuç olarak, ağır öğeler kaldırıldıktan sonra bile temel Resident
Memory şişirilmiş durumda kalır ve bu da uygulamayı Android
OS süreç sonlandırmalarına (ör. LMK veya MemoryLimiter) karşı oldukça savunmasız hale getirir.
İşletim sistemi düzeyinde zorunlu kılmayı önlemek için bellek optimizasyonuna üç temel sütun üzerinden yaklaşılmalı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ırın
A kategorisi: En yüksek bellek kullanımını azaltma
Unity'nin ayırıcıları, boşaltılan bellek sayfalarını hemen işletim sistemine döndürmek yerine yeniden kullanmak üzere saklar. Bu nedenle, temel bellek mevcut kullanımı değil, ulaşılan en yüksek tepe noktasını yansıtma eğilimindedir. Bu nedenle, artışları en baştan önlemek, sonradan temizlemeye güvenmekten daha etkilidir.
1. Aşırı büyük AssetBundle'lardan kaçının
Unity'nin Bundle Loading mekanizması nedeniyle, büyük AssetBundle'lar ciddi bellek şişmesine ve OOM kilitlenmelerine yol açar. Verimli kaynak yönetimi için paketleri modüler tutun.
Temel sorunlar
- Yüksek bellek ek yükü: Tek bir küçük öğe istenmesi, Unity'nin paket dosyasının tamamını (üstbilgiler, meta veriler ve akış arabellekleri dahil) RAM'e yüklemesine neden olur.
- Boşaltma tuzağı: Bir paketteki herhangi bir öğe etkin olarak kullanılıyorsa paketin tamamı boşaltılamaz ve kullanılmayan veriler RAM'de sıkışı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 İçerik Dizinleri'ni kullanın.
2. ScriptableObjects içindeki öğe referanslarını optimize etme
ScriptableObject oluşturma işlemindeki doğrudan UnityEngine.Object serileştirilmiş alanlar, doğrudan sabit referanslar oluşturarak referans verilen tüm öğelerin ScriptableObject yüklendiği veya örneği oluşturulduğu anda RAM'e yüklenmesini zorlar.
// 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 klibi yükleme türlerini yapılandırma
Tüm ses kliplerinin doğrudan belleğe sıkıştırılmadan yüklenmesi 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 türü | Neden |
|---|---|---|
| BGM (Arka Plan Müziği) | Akış | Bellek kullanımındaki ani artışları önlemek için diskteki sesi küçük arabellekler halinde aktarır. |
| Long SFX (Uzun SFX) | Bellekte Sıkıştırılmış | RAM yükünü düşük tutar ve oynatma sırasında sesi anında açar. |
| Kısa ve Sık Özel Efektler | Yüklenirken 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. |
4. Nesne havuzu oluşturma ve serbest bırakma stratejilerini uygulama
Sahne geçişleri sırasında Nesne Havuzları'nda 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ırın
Yedekli 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/şablon arabelleklerini kaldırma: Yalnızca renk verileri gerektiren Render Textures için Derinlik Şablonu Biçimi'ni Yok olarak ayarlayın.
- Disable UI camera depth texture (Kullanıcı arayüzü kamera derinliği dokusunu devre dışı bırak): Derinlik verilerinin gereksiz olduğu 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 (örneğin, Android için ASTC) uygulayın. |
| Okuma/yazma etkin | Gerekmedikçe devre dışı bırakın. Bu ayar etkinleştirildiğinde doku belleği, CPU ve GPU RAM'inde çoğaltılır. |
| Mipmap'ler | Sabit kamera mesafesinde düzeltilen kullanıcı arayüzü dokuları veya nesneler için mipmap'leri devre dışı bırakarak doku belleğinde yaklaşık% 33 tasarruf edin. |
| Ağ karmaşıklığı | GPU ve yerel bellekte kaplanan yeri azaltmak için gereksiz poligon sayıları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ü derlemesiyle ilgili sorunlara yol açar.
A. Gölgeleyici varyantı bellek ek yükünün mekanikleri
- Kombinasyon patlaması: Her eklenen anahtar kelime grubuyla birlikte olası toplam varyant sayısı katlanarak artar.
- Parça ayırma mimarisi: Unity, derlenmiş ikili varyantları Parçalar adı verilen sıkıştırılmış bellek blokları halinde derler (varsayılan: 4 MB).
- Yerel bellek şişmesi: Çalışma zamanı kodu bir parçada tek bir varyant istese bile 4 MB'lık parçanın tamamı RAM'e sıkıştırılmadan açılır. Optimum hale getirilmemişse bu parçalarda paketlenmiş binlerce kullanılmayan varyant, yerel belleği kalıcı olarak kaplar.
B. Unity'nin yerleşik çok kademeli kaldırma 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 kaldırır. Ayrıca, anahtar kelimeleri projedeki herhangi bir materyal tarafından etkin olarak kullanılmıyorsa shader_feature varyantları otomatik olarak filtrelenir. #multi_compile varyantları ise kullanımdan bağımsız olarak zorunlu şekilde dahil edilir.
C. Özel otomatik çıkarma 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 çıkarma genellikle yeterli değildir. Yalnızca gerçekten kullanılan varyantların dahil edilmesini sağlamak için Player.log (Editör ayarlarında Log Shader Compilation'ı etkinleştirme) veya Profiler Traces (Shader.CreateGPUProgram işaretçileri) kullanarak kalite kontrolü 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 Editor komut dosyasına IPreprocessShaders.OnProcessShader uygulayın. Böylece, derlemede yalnızca gerekli olanlar kalır.
C Kategorisi: Gereksiz GC ayırmalarını ortadan kaldırın
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 ayırmalarını ö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. Bunu Update içinde yürütmek, her karede kapatma ö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ının veya ReadOnlyCollection erişimcilerinin neden olduğu kutulama 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 ayırma önleme kuralları
- Her çağrıda yığında yeni bir
Camera[]dizi oluşturduğundanCamera.allCameraskullanmaktan kaçının. Bunun yerine, bir kamera dizisini önbelleğe alın veCamera.GetAllCameras(_allCameras)'ya 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.- Eş yordam nesnelerini önbelleğe alın:
yield return new WaitForSeconds(time);öğesini tekrar tekrar örneklendirmek yerineWaitForSecondsörneklerini önbelleğe alın. - Sözlüklerdeki özel yapı anahtarları: Özel yapıları Dictionary
anahtarları olarak kullanmak varsayılan
Equalsişlevini çağırarak nesne kutulamayı 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 düşünce kullanımını en aza indirme
Genel yöntemler mükemmel kod yeniden kullanımı ve sürdürülebilirlik sağlasa da bunların aşırı kullanılması, Unity IL2CPP (Intermediate Language to C++) arka ucu bağlamında projenizi olumsuz etkileyebilir.
- IL2CPP kod şişmesi: Her benzersiz genel tür kombinasyonu için IL2CPP, kodun özel bir sürümünü oluşturur. Karmaşık jeneriklerin aşırı kullanımı, oluşturulan C++ kodunda "kombinatoryal patlamaya" yol açabilir ve uygulamanın ikili program boyutunu ve yerel bellekte kaplanan yerini önemli ölçüde artırabilir.
- 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 bir şekilde kullanın. Bunları geniş ve ayrım gözetmeyen bir uygulama yerine mimari netlik için önceliklendirin. Performansın kritik olduğu durumlarda somut türleri veya arayüz tabanlı 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. Sızdırılan yönetilen kabukları önleme
UnityEngine.Object, MonoBehaviour, Texture veya GameObject gibi her
yerel C++ motoruyla iletişim kuran bir C# "Managed Shell" 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 güçlü temizleme kalıpları uygulayın. Nesneleri yok ederken veya sahneler arasında geçiş yaparken
-=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 tutacağını serbest bıraktıktan sonra çöp toplama işleminin sarmalayıcıyı başarıyla toplayabilmesini sağlar.