Addressables 는 번들 단위로 참조를 센다
LoadAssetAsync 와 Release 짝을 꼼꼼히 맞췄는데 메모리 프로파일러에 놓은 텍스처가 그대로 있습니다. 반대로 핸들을 놓자 아직 화면에 있던 오브젝트가 사라지기도 합니다.
Addressables 는 핸들마다 참조를 세지만, 실제로 메모리에서 내리는 단위는 번들입니다. 그리고 셈에 들어가는 것은 Addressables 를 거친 로드와 인스턴스뿐입니다.
Release 한 에셋도 번들이 열려 있으면 남는다
Addressables 매뉴얼은 이렇게 적습니다. 에셋을 놓아도 바로 메모리에서 빠지지 않고, “AssetBundle 의 일부만 내릴 수는 없다”. 번들의 참조가 0 이 되어 번들이 내려갈 때까지 그 번들의 에셋은 남습니다.
Pack Together 그룹 하나에 큰 텍스처 A(2048², GPU 에서 16 MiB)와 작은 텍스처 B 를 넣고, 둘을 로드한 뒤 A 만 놓았습니다.
| 단계 | A | GPU 증가 | 열린 번들 |
|---|---|---|---|
| A, B 로드 | 있음 | +16.02 MiB | 1 |
A Release |
있음 | +16.02 MiB | 1 |
UnloadUnusedAssets |
없음 | +0.02 MiB | 1 |
B Release |
없음 | 0 | 0 |
A 를 놓아도 B 를 놓기 전까지 16 MiB 가 남았습니다. 이때 Resources.UnloadUnusedAssets() 를 부르면 번들은 열린 채 A 만 내려갔습니다. 메모리 페이지에는 없는 동작입니다. 번들이 열려 있으니 A 를 다시 로드하면 번들에서 다시 읽어 왔습니다(8.9 ms).
그래서 놓은 에셋을 바로 빼려면 같은 번들의 다른 에셋까지 놓거나, UnloadUnusedAssets 를 불러야 합니다. 뒤쪽은 씬 전체를 훑는 비용이 있어 아무 때나 부를 수는 없습니다. 근본적인 답은 수명이 다른 에셋을 같은 번들에 넣지 않는 것입니다. 매뉴얼은 이것과 반대 방향의 함정도 적어 둡니다. 번들의 마지막 에셋을 놓자마자 같은 번들의 에셋을 다시 로드하면, 번들을 내렸다가 다시 여는 일이 반복됩니다. Addressables 의 Profiler 모듈에서 번들이 언제 열리고 닫히는지 볼 수 있습니다.
Instantiate 를 누가 했느냐에 따라 셈이 다르다
프리팹을 로드해 인스턴스를 만드는 길이 둘입니다. LoadAssetAsync 로 프리팹을 받아 직접 Object.Instantiate 하거나, Addressables.InstantiateAsync 로 인스턴스를 받습니다. 두 길에서 핸들을 놓거나 인스턴스를 지우는 방식을 바꿔 가며, 직후, 2초 뒤, UnloadUnusedAssets 뒤, 씬을 Single 로 다시 올린 뒤에 열린 번들 수를 셌습니다.
| 만든 방식 | 정리 | 인스턴스 | 열린 번들 (직후, 2초, UUA, 씬 재로드) |
|---|---|---|---|
Object.Instantiate |
로드 핸들 Release |
살아 있음, 머티리얼 null | 0, 0, 0, 0 |
InstantiateAsync |
핸들 Release |
파괴됨 | 0, 0, 0, 0 |
InstantiateAsync |
Destroy 만 |
파괴됨 | 3, 3, 3, 0 |
InstantiateAsync |
ReleaseInstance |
파괴됨 | 0, 0, 0, 0 |
Object.Instantiate 한 인스턴스는 Addressables 가 모릅니다. 프리팹 핸들을 놓으면 셈이 0 이 되어 번들이 내려가고, 인스턴스가 쓰던 머티리얼이 파괴됩니다. 인스턴스는 남아 있는데 GPU Resident Drawer 가 켜져 있으면 화면에서 사라지고, 끄면 마젠타로 그려집니다. 경고나 에러 로그는 없었습니다.
InstantiateAsync 는 인스턴스 수명까지 셉니다. 핸들을 놓거나 ReleaseInstance 하면 인스턴스를 지우고 셈을 줄입니다. 그런데 Destroy 로 지우기만 하면 핸들은 유효한 채로 남아 셈이 줄지 않습니다. 2초 뒤에도, UnloadUnusedAssets 뒤에도 번들 셋이 열려 있었고, 씬을 다시 올리자 0 이 됐습니다. 매뉴얼의 “추적되는 InstantiateAsync 인스턴스는 씬이 내려갈 때 해제된다” 는 설명과 맞습니다. 영원히 새는 것은 아니지만, 그 씬에 머무는 동안은 안 내려갑니다.
직접 확인하는 테스트 코드
같은 번들의 A 만 놓는 경우와, InstantiateAsync 인스턴스를 Destroy 만 하는 경우를 봅니다. 열린 번들은 AssetBundle.GetAllLoadedAssetBundles() 로 셉니다.
IEnumerator ReleaseOneOfTwo()
{
var hA = Addressables.LoadAssetAsync<Texture2D>("AD4/A");
var hB = Addressables.LoadAssetAsync<Texture2D>("AD4/B");
yield return hA;
yield return hB;
Addressables.Release(hA);
yield return null;
bool aAlive = Resources.FindObjectsOfTypeAll<Texture2D>().Any(t => t.name == "AD4_A");
Debug.Log($"A 를 놓은 뒤 A 가 메모리에 있나: {aAlive}"); // True
yield return Resources.UnloadUnusedAssets();
aAlive = Resources.FindObjectsOfTypeAll<Texture2D>().Any(t => t.name == "AD4_A");
Debug.Log($"UnloadUnusedAssets 뒤: {aAlive}, 열린 번들 {AssetBundle.GetAllLoadedAssetBundles().Count()}"); // False, 1
Addressables.Release(hB);
}
IEnumerator DestroyOnly()
{
var handle = Addressables.InstantiateAsync("AD6/Cube");
yield return handle;
Destroy(handle.Result);
yield return new WaitForSecondsRealtime(2);
Debug.Log($"Destroy 2초 뒤 핸들 유효: {handle.IsValid()}, 열린 번들 {AssetBundle.GetAllLoadedAssetBundles().Count()}");
}
첫 함수에서 True 다음 False, 1, 둘째 함수에서 핸들이 유효하고 번들이 열려 있으면 같은 결과입니다.
주의할 점
1. 놓은 에셋이 빠졌는지는 번들 단위로 본다
안 지키면 생기는 문제. 에셋 핸들만 보고 “다 놓았다” 고 판단하면, 같은 번들의 다른 에셋 때문에 남아 있는 메모리를 놓칩니다. 반대로 번들의 마지막 에셋을 놓자마자 같은 번들을 다시 로드하면 번들을 닫았다 여는 일이 반복됩니다.
이렇게 쓴다. Addressables Profiler 모듈에서 번들이 언제 열리고 닫히는지 같이 봅니다. 곧 다시 쓸 번들은 에셋 하나를 쥐고 있어 닫히지 않게 둡니다.
2. Object.Instantiate 로 만들면 핸들을 인스턴스보다 오래 쥔다
안 지키면 생기는 문제. 인스턴스는 셈에 안 들어가서, 프리팹 핸들을 놓는 순간 번들이 내려가고 인스턴스의 머티리얼이 경고 없이 사라집니다.
이렇게 쓴다. 프리팹 핸들을 쥔 쪽이 인스턴스를 전부 지운 뒤에 Release 합니다. 풀이나 스포너가 프리팹을 가지면 핸들도 그 풀이 가집니다.
3. InstantiateAsync 로 만든 것은 ReleaseInstance 로 지운다
안 지키면 생기는 문제. Destroy 로 지우면 핸들이 남아 그 씬이 내려갈 때까지 번들이 열려 있습니다. 한 씬에서 오래 도는 게임에서는 그동안 계속 쌓입니다.
이렇게 쓴다. InstantiateAsync 로 만든 인스턴스는 Addressables.ReleaseInstance(go) 로 지웁니다. 이 인스턴스를 받는 쪽 코드가 Destroy 를 부르지 않게 한 곳으로 모읍니다.
정리
Addressables 는 핸들마다 세지만, 메모리에서 내리는 단위는 번들입니다.
- 같은 번들의 다른 에셋이 살아 있으면
Release한 에셋도 남습니다.UnloadUnusedAssets를 부르면 번들은 열린 채 그 에셋만 내려갑니다. Object.Instantiate한 인스턴스는 셈에 안 들어가서, 핸들을 놓으면 머티리얼이 경고 없이 사라집니다.InstantiateAsync는 인스턴스까지 세지만,Destroy만 하면 씬이 내려갈 때까지 번들이 열려 있습니다.ReleaseInstance로 지웁니다.