Meskipun kapasitas RAM fisik perangkat seluler modern terus bertambah, konsumsi memori game melampaui pertumbuhan ini karena aset beresolusi tinggi dan pipeline rendering yang kompleks.
Masalah utama yang disebabkan oleh penggunaan memori yang berlebihan
- Penghentian low memory killer (LMK): OS menutup paksa aplikasi latar belakang atau bahkan latar depan untuk mengatasi kekurangan memori di seluruh sistem.
- Frame drop dan ketersendatan (jank): Peningkatan pembersihan sampah memori (GC) yang sering di Managed Heap atau pertukaran memori tingkat OS menciptakan bottleneck pemrosesan.
- Pembatasan termal dan baterai cepat habis: Alokasi memori, dealokasi, dan penerapan halaman memori yang berkelanjutan menimbulkan overhead CPU yang signifikan, sehingga menyebabkan peningkatan panas dan baterai cepat habis.
Update pengelolaan memori Android
- Android 17: Pengenalan
MemoryLimiter: Android 17 memperkenalkanMemoryLimiteryang secara aktif memantau penggunaan memori aplikasi terhadap nilai minimum khusus perangkat. Aplikasi yang melebihi batas memori akan segera dihentikan di tingkat sistem. Karena mekanisme ini lebih ketat daripada LMK tradisional, pengelolaan penggunaan memori puncak kini lebih penting daripada sebelumnya.
Mengurangi penggunaan memori di Unity
Di Unity, setelah mesin memperluas kumpulan memori internalnya (Native Block Allocators dan Managed Heap) untuk mengakomodasi beban puncak yang tinggi, mesin akan mempertahankan halaman memori tersebut, bukan langsung mengembalikannya ke OS.
Akibatnya, meskipun setelah aset berat dibongkar, Memori
Residen dasar tetap meningkat, sehingga membuat aplikasi sangat rentan terhadap penghentian proses Android
OS (seperti LMK atau MemoryLimiter).
Untuk mencegah penerapan tingkat OS, pengoptimalan memori harus dilakukan melalui tiga pilar utama:
- Kategori A: Mengurangi penggunaan memori puncak
- Kategori B: Menghilangkan jejak aset dan sistem yang tidak perlu
- Kategori C: Menghilangkan alokasi GC yang tidak perlu
Kategori A: Mengurangi penggunaan memori puncak
Pengalokasi Unity mempertahankan halaman memori yang dibebaskan untuk digunakan kembali, bukan langsung mengembalikannya ke OS, sehingga memori dasar cenderung mencerminkan puncak tertinggi yang tercapai, bukan penggunaan saat ini. Oleh karena itu, mencegah lonjakan sejak awal lebih efektif daripada mengandalkan pembersihan setelahnya.
1. Menghindari AssetBundle yang terlalu besar
Karena mekanisme Pemuatan Bundle Unity, AssetBundle berukuran besar menyebabkan kebocoran memori yang parah dan error OOM. Pertahankan agar paket tetap modular untuk memastikan pengelolaan resource yang efisien.
Masalah utama
- Overhead memori tinggi: Meminta satu aset kecil memaksa Unity untuk memuat seluruh file paket (termasuk header, metadata, dan buffer streaming) ke dalam RAM.
- Perangkap pelepasan: Jika aset apa pun dalam bundle sedang digunakan secara aktif, seluruh bundle tidak dapat dilepaskan, sehingga data yang tidak digunakan terperangkap dalam RAM.
Praktik terbaik
- Pertahankan modularitas paket: Kelompokkan aset secara logis berdasarkan adegan atau siklus proses.
- Tips Unity 6.6+: Gunakan Direktori Konten untuk mencegah dependensi lintas paket yang tidak diinginkan.
2. Mengoptimalkan referensi aset di ScriptableObjects
Kolom berserial UnityEngine.Object langsung dalam ScriptableObject membuat
referensi tetap langsung, sehingga memaksa semua aset yang direferensikan dimuat ke dalam RAM segera
setelah ScriptableObject itu sendiri dimuat atau di-instansiasi.
// 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. Mengonfigurasi jenis pemuatan klip audio
Memuat semua klip audio tanpa dikompresi langsung ke dalam memori akan menciptakan puncak memori yang besar dan permanen. Setelan beban audio harus dikonfigurasi berdasarkan profil penggunaannya:
| Kategori audio | Jenis pemuatan | Alasan |
|---|---|---|
| BGM (Musik Latar) | Streaming | Mengalirkan audio dari disk dalam buffer kecil untuk menghilangkan lonjakan memori. |
| SFX Panjang | Dikompresi Dalam Memori | Mempertahankan overhead RAM yang rendah dan mendekompresi audio secara langsung selama pemutaran. |
| SFX Pendek & Sering | Decompress On Load | Mendekompresi audio ke dalam RAM saat memuat untuk menghindari overhead CPU runtime selama pemutaran. |
4. Menerapkan strategi penggabungan dan pelepasan objek
Instance yang belum dirilis yang tetap berada di dalam Object Pool selama transisi adegan akan mempertahankan Memori yang Dicadangkan tanpa batas waktu, sehingga membuat jejak memori dasar meningkat tanpa perlu.
- Tindakan: Hapus atau pangkas objek gabungan yang tidak digunakan secara berkala selama transisi adegan atau periode aktivitas rendah, sehingga Unity dapat mengembalikan atau menggunakan kembali ruang yang dicadangkan tersebut untuk alokasi lain.
Kategori B: Menghilangkan penggunaan memori yang tidak perlu
Menghilangkan aset visual yang berlebihan dan merender buffer target secara langsung akan menurunkan jejak memori dasar.
1. Mengoptimalkan tekstur render dan kedalaman kamera
- Menghapus buffer depth/stencil: Tetapkan Format Stensil Kedalaman ke Tidak Ada untuk Tekstur Render yang hanya memerlukan data warna.
- Menonaktifkan tekstur kedalaman kamera UI: Untuk kamera UI yang tidak memerlukan data kedalaman, nonaktifkan pembuatan tekstur kedalaman di setelan kamera URP untuk menghilangkan
CopyDepthPass dan memori tekstur GPU terkait.
2. Mengoptimalkan tekstur dan mesh
| Kategori | Pedoman pengoptimalan |
|---|---|
| Kompresi tekstur | Selalu terapkan format kompresi platform target (misalnya, ASTC untuk Android). |
| Baca/Tulis diaktifkan | Tetap nonaktifkan kecuali diperlukan. Mengaktifkan ini akan menduplikasi memori tekstur di seluruh RAM CPU dan GPU. |
| Mipmap | Nonaktifkan Mipmap untuk tekstur atau objek UI yang tetap pada jarak kamera konstan, sehingga menghemat memori tekstur sekitar 33%. |
| Kompleksitas mesh | Kurangi jumlah poligon dan aliran verteks yang tidak perlu untuk menurunkan jejak memori GPU dan native. |
3. Menghapus varian shader dan mengoptimalkan memori
Uber-shader (misalnya, URP Lit Shader) merangkum banyak fitur menggunakan kata kunci
#multi_compile dan shader_feature. Tanpa pengoptimalan, ledakan kombinatorial akan membuat puluhan ribu varian shader unik, sehingga menyebabkan ukuran build yang besar, konsumsi memori native yang besar, dan gangguan kompilasi driver GPU selama gameplay.
A. Mekanisme overhead memori varian shader
- Ledakan kombinatorial: Total kemungkinan varian bertambah secara eksponensial dengan setiap grup kata kunci yang ditambahkan.
- Arsitektur alokasi chunk: Unity mengelompokkan varian biner yang dikompilasi ke dalam blok memori terkompresi yang disebut Chunk (default: 4 MB).
- Pembengkakan memori native: Saat kode runtime meminta satu varian di dalam chunk, seluruh chunk 4 MB akan didekompresi ke dalam RAM. Jika tidak dioptimalkan, ribuan varian yang tidak digunakan yang dikemas dalam potongan tersebut akan menempati memori native secara permanen.
B. Pipeline penghapusan multi-tahap bawaan Unity: Unity secara otomatis menghapus
varian yang tidak diperlukan pada waktu build berdasarkan setelan grafis platform target
dan fitur mesin yang tidak digunakan (misalnya, setelan Kabut, Peta Cahaya, dan XR). Selain itu, varian shader_feature akan otomatis
dikecualikan jika kata kuncinya tidak digunakan secara aktif oleh materi apa pun dalam
project, sedangkan varian #multi_compile akan disertakan secara paksa terlepas
dari penggunaannya.
C. Arsitektur penghapusan otomatis kustom
(IPreprocessShaders) Karena analisis statis tidak dapat
mendeteksi kata kunci yang diubah secara dinamis menggunakan skrip C# runtime
(Material.EnableKeyword), penghapusan standar sering kali tidak memadai. Untuk
memastikan hanya varian yang benar-benar digunakan yang disertakan, Anda dapat mengumpulkan varian
selama rangkaian pengujian QA menggunakan Player.log (dengan mengaktifkan
Log Shader Compilation di setelan Editor) atau Profiler Traces
(penanda Shader.CreateGPUProgram). Kemudian, terapkan
IPreprocessShaders.OnProcessShader dalam skrip Editor untuk memfilter
varian yang tidak pernah dieksekusi selama runtime, dan hanya menyimpan varian
yang diperlukan dalam build.
Kategori C: Menghilangkan alokasi GC yang tidak perlu
Alokasi Pengumpulan Sampah (GC) di Managed Heap menyebabkan fragmentasi memori, perluasan heap yang tidak pernah menyusut, dan penurunan frame yang parah selama jeda GC.
1. Mencegah alokasi penutupan lambda
Saat ekspresi lambda mengambil variabel lokal luar, C# akan membuat class tampilan implisit di Heap. Mengeksekusi ini di dalam Update akan mengalokasikan
instance penutupan setiap frame.
// 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. Menggunakan stackalloc dan Span
Hindari alokasi Heap untuk array sementara berumur pendek dengan menggunakan memori Stack.
// 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. Mengoptimalkan iterasi pengumpulan
Hindari alokasi heap boxing dan Enumerator yang disebabkan oleh ekstensi LINQ atau pengakses 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. Aturan pencegahan alokasi GC tambahan
- Hindari penggunaan
Camera.allCameraskarena akan menghasilkan arrayCamera[]baru di heap pada setiap panggilan. Sebagai gantinya, simpan array kamera dalam cache dan teruskan keCamera.GetAllCameras(_allCameras). - Hindari
foreachdiIReadOnlyList<T>: Melakukan iterasi pada antarmuka menyebabkan boxing enumerator struct, yang menghasilkan alokasi GC. Gunakan loopforstandar sebagai gantinya. - Meng-cache objek coroutine: Meng-cache instance
WaitForSeconds, bukan meng-instanceyield return new WaitForSeconds(time);berulang kali. - Kunci struct kustom dalam kamus: Menggunakan struct kustom sebagai kunci Dictionary
memanggil
Equalsdefault, yang memicu boxing objek. TerapkanIEqualityComparer<T>dan teruskan ke konstruktor Dictionary.
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. Meminimalkan penggunaan generik dan refleksi
Meskipun metode generik memberikan penggunaan ulang dan pemeliharaan kode yang sangat baik, penggunaan berlebihan dapat berdampak negatif pada project Anda dalam konteks backend Unity IL2CPP (Intermediate Language to C++).
- Pembengkakan kode IL2CPP: Untuk setiap kombinasi jenis generik yang unik, IL2CPP membuat versi kode khusus. Penggunaan generik yang kompleks secara berlebihan dapat menyebabkan "ledakan kombinatorial" kode C++ yang dihasilkan, sehingga meningkatkan ukuran biner aplikasi dan jejak memori native secara signifikan.
- Overhead refleksi: Metode yang menggunakan refleksi, seperti
API
System.Reflection, pada dasarnya lambat dan sering menyebabkan alokasi heap selama runtime. - Praktik terbaik: Gunakan generik dengan bijak—prioritaskan untuk
kejelasan arsitektur, bukan untuk penerapan yang luas dan tidak pandang bulu. Jika performa sangat penting, gunakan jenis konkret atau polimorfisme berbasis antarmuka. Untuk refleksi, simpan hasil cache seperti
MethodInfoatauFieldInfoselama inisialisasi, bukan mengkuerinya dalam loop pembaruan.
6. Menghindari kebocoran shell terkelola
Setiap UnityEngine.Object, seperti MonoBehaviour, Texture, atau GameObject,
memiliki wrapper "Managed Shell" C# yang berkomunikasi dengan mesin C++ native.
- Masalahnya: Jika Managed Shell disimpan dalam memori oleh referensi statis, langganan peristiwa persisten, atau penutupan yang tidak dibersihkan, GC tidak dapat mengklaim kembali memori. Meskipun objek native dihancurkan, wrapper terkelola tetap ada, sehingga menyebabkan kebocoran memori "hantu" yang memperbesar Heap Terkelola.
- Resolusi: Selalu terapkan pola pembersihan yang kuat. Saat menghancurkan
objek atau melakukan transisi adegan, batalkan langganan peristiwa secara eksplisit menggunakan
operator
-=dan batalkan referensi statis ke jenisUnityEngine.Object. Pembersihan ini memastikan GC dapat berhasil mengumpulkan wrapper setelah mesin native melepaskan handle-nya.