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 aplikasi latar depan untuk mengatasi kekurangan memori di seluruh sistem.
- Penurunan frame dan stuttering (jank): Lonjakan garbage collection (GC) yang sering terjadi di Managed Heap atau pertukaran memori tingkat OS akan membuat bottleneck pemrosesan.
- Thermal throttling dan pengurasan baterai: Alokasi memori yang berkelanjutan, dealokasi, dan commit halaman memori akan menimbulkan overhead CPU yang signifikan, sehingga menyebabkan peningkatan panas dan pengurasan baterai yang lebih cepat.
Update manajemen memori Android
- Android 17: Pengenalan
MemoryLimiter: Android 17 memperkenalkanMemoryLimiteryang secara aktif memantau konsumsi memori aplikasi terhadap batas 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 yang berat di-unloading, Resident Memory dasar tetap meningkat, sehingga aplikasi sangat rentan terhadap penghentian proses OS Android (seperti LMK atau MemoryLimiter).
Untuk mencegah penerapan tingkat OS, pengoptimalan memori harus didekati melalui tiga pilar utama:
- Kategori A: Mengurangi penggunaan memori puncak
- Kategori B: Menghilangkan jejak aset dan sistem yang tidak diperlukan
- Kategori C: Menghilangkan alokasi GC yang tidak diperlukan
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 dicapai, 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 Bundle Loading Unity, AssetBundle yang sangat besar menyebabkan bloat memori yang parah dan error OOM. Buat bundle tetap modular untuk memastikan pengelolaan resource yang efisien.
Masalah utama
- Overhead memori yang tinggi: Meminta satu aset kecil akan memaksa Unity untuk memuat seluruh file bundle (termasuk header, metadata, dan buffer streaming) ke dalam RAM.
- Unload trap: Jika ada aset dalam bundle yang sedang digunakan secara aktif, seluruh bundle tidak dapat di-unloading, sehingga data yang tidak digunakan akan terperangkap di RAM.
Praktik terbaik
- Buat bundle tetap modular: Kelompokkan aset secara logis berdasarkan adegan atau siklus proses.
- Tips Unity 6.6+: Gunakan Content Directories untuk mencegah dependensi lintas-bundle yang tidak diinginkan.
2. Mengoptimalkan referensi aset di ScriptableObjects
Kolom serial UnityEngine.Object langsung di ScriptableObject membuat referensi hard langsung, sehingga memaksa semua aset yang direferensikan untuk dimuat ke dalam RAM segera setelah ScriptableObject itu sendiri dimuat atau dibuat instance-nya.
// 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 klip dan channel audio
Memuat semua klip audio yang tidak dikompresi langsung ke dalam memori akan membuat puncak memori permanen yang besar. Setelan pemuatan audio harus dikonfigurasi berdasarkan profil penggunaannya:
| Kategori audio | Jenis pemuatan | Alasan |
|---|---|---|
| BGM (Musik Latar Belakang) | Streaming | Mengalirkan audio dari disk dalam buffer kecil untuk menghilangkan lonjakan memori. |
| SFX Panjang | Dikirim dalam Memori | Mempertahankan overhead RAM yang rendah dan mendekompresi audio secara langsung selama pemutaran. |
| SFX Singkat &Sering | Decompress On Load | Mendekompresi audio ke dalam RAM saat dimuat untuk menghindari overhead CPU runtime selama pemutaran. |
Selain jenis pemuatan, terapkan konfigurasi audio berikut untuk lebih mengurangi overhead memori:
- Aktifkan Force To Mono untuk audio non-directional: Banyak aset audio—seperti
efek suara UI, voice-over, dan efek suara spasial 3D—tidak
memerlukan pemisahan channel stereo. Aktifkan Force To Mono di
AudioClipsetelan impor untuk aset ini guna mengurangi jejak memori dan ukuran buffer menjadi setengahnya (pengurangan 50%). - Batasi channel audio aktif berdasarkan tingkat perangkat: Setiap suara audio aktif
mengalokasikan mixer DSP native dan buffer dekompresi. Pada perangkat dengan memori rendah,
kurangi jumlah maksimum channel aktif simultan—misalnya, dengan
menurunkan Max Real Voices (seperti dari 32 menjadi 16) menggunakan
AudioSettings.Resetdi Unity, atau dengan membatasi channel pemutaran software di mesin audio pihak ketiga.
4. Menerapkan strategi pengumpulan dan rilis objek
Instance yang tidak dirilis dan tetap berada di dalam Object Pools di seluruh transisi adegan akan mempertahankan Reserved Memory tanpa batas waktu, sehingga jejak memori dasar meningkat secara tidak 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 untuk alokasi lain.
Kategori B: Menghilangkan penggunaan memori yang tidak diperlukan
Menghilangkan aset grafis yang berlebihan dan merender buffer target secara langsung akan menurunkan jejak memori dasar.
1. Mengoptimalkan tekstur render dan kedalaman kamera
- Menghapus buffer kedalaman/stensil: Tetapkan Depth Stencil Format ke None untuk Render Textures 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 | Panduan pengoptimalan |
|---|---|
| Kompresi tekstur | Selalu terapkan format kompresi platform target (misalnya, ASTC untuk Android). |
| Read/Write diaktifkan | Tetap nonaktif kecuali jika diperlukan. Mengaktifkan ini akan menduplikasi memori tekstur di seluruh RAM CPU dan GPU. |
| Mipmap | Nonaktifkan Mipmap untuk tekstur atau objek UI yang ditetapkan pada jarak kamera konstan, sehingga menghemat memori tekstur ~33%. |
| Kompleksitas mesh | Kurangi jumlah poligon dan aliran vertex yang tidak diperlukan 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 shaderunik, sehingga menyebabkan ukuran build meningkat, konsumsi memori native yang besar, dan gangguan kompilasi driver GPU selama gameplay.
A. Mekanisme overhead memori varian shader
- Ledakan kombinatorial: Total varian yang mungkin tumbuh secara eksponensial dengan setiap grup kata kunci yang ditambahkan.
- Arsitektur alokasi potongan: Unity mengelompokkan varian biner yang dikompilasi ke dalam blok memori terkompresi yang disebut Potongan (default: 4 MB).
- Bloat memori native: Saat kode runtime meminta satu varian di dalam potongan, seluruh potongan 4 MB akan didekompresi ke dalam RAM. Jika tidak dioptimalkan, ribuan varian yang tidak digunakan dan 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, Fog, Lightmaps, dan setelan XR
). Selain itu, varian shader_feature akan otomatis
difilter jika kata kuncinya tidak digunakan secara aktif oleh materi apa pun dalam
project, sedangkan varian #multi_compile akan disertakan secara paksa, terlepas
dari penggunaannya. Namun, jika fitur rendering atau pasca-pemrosesan (seperti
Additional Lights, bayangan real-time, atau efek UberPost yang tidak digunakan) tetap
diaktifkan di setelan URP atau pipeline, Unity tidak dapat menentukan apakah konten Anda benar-benar menggunakannya dan menghasilkan semua varian #multi_compile
yang sesuai. Audit dan nonaktifkan setelan pipeline yang tidak digunakan sehingga sesuai dengan
persyaratan rendering sebenarnya dan mencegah pembuatan varian yang tidak diperlukan.
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 suite pengujian QA menggunakan Player.log (mengaktifkan
Log Shader Compilation di setelan Editor) atau Profiler Traces
(Shader.CreateGPUProgram penanda). Kemudian, terapkan
IPreprocessShaders.OnProcessShader dalam skrip Editor untuk memfilter
varian yang tidak pernah dieksekusi selama runtime, sehingga hanya varian yang diperlukan
yang disimpan dalam build.
D. Menggabungkan shader yang berlebihan: Uber-shader seperti Lit dan Simple
Lit berisi banyak direktif #multi_compile dan menggunakan memori yang besar
saat membuat banyak varian:
- Membuat profil penggunaan simultan: Buat profil penggunaan memori Anda untuk memeriksa apakah kedua shader menggunakan memori yang signifikan pada saat yang sama, dan verifikasi apakah project Anda benar-benar memerlukan keduanya.
- Menstandarkan satu shader: Jika rendering berbasis fisik (PBR) tidak diperlukan, konfigurasi aset Anda untuk menghilangkan penggunaan shader Lit yang berlebihan dan menstandarkan Simple Lit (misalnya, periksa apakah model FBX yang diekspor memiliki materi Lit default yang tidak sengaja diterapkan selama impor).
Kategori C: Menghilangkan alokasi GC yang tidak diperlukan
Alokasi Garbage Collection (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. Menjalankan 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 yang 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 koleksi
Hindari boxing dan alokasi heap 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, cache array kamera dan teruskan keCamera.GetAllCameras(_allCameras). - Hindari
foreachdiIReadOnlyList<T>: Melakukan iterasi pada antarmuka akan menyebabkan boxing enumerator struct, sehingga menghasilkan alokasi GC. Gunakan loopforstandar sebagai gantinya. - Objek coroutine cache: Cache
WaitForSecondsinstance, bukan membuat instanceyield return new WaitForSeconds(time);berulang kali. - Kunci struct kustom dalam kamus: Menggunakan struct kustom sebagai kunci Dictionary
akan memanggil
Equalsdefault, sehingga 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 kembali dan pemeliharaan kode yang sangat baik, penggunaannya yang berlebihan dapat memengaruhi project Anda secara negatif dalam konteks backend Unity IL2CPP (Intermediate Language to C++).
- Bloat kode IL2CPP: Untuk setiap kombinasi jenis generik yang unik, IL2CPP akan menghasilkan versi kode khusus. Penggunaan generik kompleks yang 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
System.ReflectionAPI, secara inheren lambat dan sering menyebabkan alokasi heap selama runtime. - Praktik terbaik: Gunakan generik dengan bijak—prioritaskan untuk
kejelasan arsitektur, bukan aplikasi yang luas dan tidak diskriminatif. Jika performa sangat penting, pilih jenis konkret atau polimorfisme berbasis antarmuka. Untuk refleksi, cache hasil seperti
MethodInfoatauFieldInfoselama inisialisasi, bukan mengkuerinya dalam loop update.
6. Menghindari shell terkelola yang bocor
Setiap UnityEngine.Object, seperti MonoBehaviour, Texture, atau GameObject, memiliki wrapper "Managed Shell" C# yang berkomunikasi dengan mesin C++ native.
- Masalah: 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 akan tetap ada, sehingga menyebabkan kebocoran memori "hantu" yang meningkatkan Managed Heap.
- 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 merilis handle-nya.
7. Menggunakan katalog biner untuk Addressables
Di Unity 2022 dan versi sebelumnya, Addressables menggunakan katalog JSON, bukan katalog biner (yang merupakan default di Unity 2023 dan versi yang lebih baru). Jika project Anda menggunakan katalog JSON, memuat katalog besar saat startup akan membaca seluruh file ke dalam satu string C# besar dan mengalokasikan ribuan string terkelola dan array byte[] sementara selama penguraian JSON dan decoding Base64. Hal ini menyebabkan lonjakan alokasi GC yang besar dan memperluas Managed Heap secara permanen, jadi beralihlah ke katalog biner untuk melakukan streaming data ringkas secara langsung.