메모리 관리에 대한 탐구 (10) - Unity에서 Font 관련 용량 줄이기
Unity로 모바일 게임을 개발하다보면 메모리가 부족해지는 경우가 생긴다.
메모리 관리에 대한 탐구 (5) - 메모리 프로파일러를 통한 메모리 사용량 분석 :: 메피카타츠의 블로그 (tistory.com)
메모리 관리에 대한 탐구 (5) - 메모리 프로파일러를 통한 메모리 사용량 분석
오늘은 유니티에서 Package Manager를 통해서 제공하는 Memory Profiler에 대해서 소개할 예정이다. 유니티 2021에서 제공하는 0.7.1 버전을 기준으로 소개를 할 것인데, 현재는 1.0도 나와있다. UI나 세부적
mepkatatsu.tistory.com
이전에 언급했던 메모리 프로파일러를 사용하면 메모리에서 오브젝트들이 차지하고 있는 용량을 확인할 수 있다.
이전에 올렸던 사진 중에서도 최상위에 있는 것이 바로 폰트의 Atlas 텍스쳐로, 무려 각각 32MB씩이나 차지를 하고 있다.
다국가 서비스를 하는 경우에 빌드에 다양한 나라의 언어에 대한 폰트가 포함될 수 있고, 이에 따라 용량이 큰 폰트의 수가 늘어날 수밖에 없다.
이런 와중에 폰트가 메모리 내에 중복으로 적재되는 경우가 발생하는데, 이 경우에 메모리가 심각하게 부족해져 게임이 튕기게 되는 문제가 발생하곤 한다.
폰트 용량을 절감하면 메모리 뿐만 아니라, apk의 빌드 용량도 감소시킬 수 있다.
아래는 현재 참여 중인 라이브 프로젝트에서 폰트 용량 절감으로 얻은 효과이다.
apk 용량: Mono 빌드 기준 120.5MB -> 84.5MB 로 30% 감소
메모리 내 폰트 용량: 피크 기준 714MB -> 53.2MB로 92.5% 감소 (Objects And Allocations 기준)
전체 메모리 사용량: 피크 기준 1.06GB -> 0.62GB 로 41.5% 감소 (프로파일러 좌측에 나타나는 기기 전체 사용량 기준)
메모리 내 폰트 용량 감소와 전체 메모리 사용량 감소에 다소의 차이가 있는데 아마 줄어든 만큼 완전히 줄어드는 게 아니라 어느정도 메모리를 확보해놓는 것 때문에 그런 것 같기도 하다. (전체 메모리 사용량이 0.62GB인데 이중 200MB는 여유 공간이라든가 등)
그러면 apk 용량 감소 / 메모리 절감을 위해 확인해야 할 요소들을 살펴보자.
=========================================================================
1. 폰트 Atlas 용량 줄이기
Unity로 개발을 하면서 텍스트를 표시할 때는 TextMeshPro를 주로 사용할 것이다.
TextMeshPro를 사용할 때는 위와 같이 ttf라고 하는 폰트 파일을 TMP(Text Mesh Pro)에서 제공하는 기능을 사용해서 SDF 에셋으로 만들어야 한다.
여기서 유심히 봐야 할 것은 아래 2가지이다.
Sampling Point Size: 글자를 Atlas에 저장할 사이즈, 클수록 글자가 선명해지지만 Atlas의 용량이 커진다.
Atlas Width & Atlas Height: 글자를 저장할 Atlas의 사이즈, 클수록 용량이 커진다.
위의 세팅을 말로 풀어쓰자면
DungGeunMo폰트의 글자를 60이라는 크기로 샘플링해서 4096x4096짜리 Atlas에 담아서 저장하겠다. 라는 의미이다.
그 결과가 이거다. 저만큼의 폰트를 쓰려고 쓸데없이 방대한 양의 Atlas를 생성해놓은 셈이다. (...)
이 16MB짜리 텍스쳐는 이유는 잘 모르겠지만 메모리에 올라가면 2배인 32MB로 올라가게 된다.
(2024/10/27 추가 - 아마 Dynamic으로 설정해놓으면 Texture에 읽고 쓰는 기능이 필요할 거라 Read/Write 옵션을 자동으로 사용하고, 때문에 CPU, GPU 양쪽 메모리에 적재되어 2배를 차지하는 것으로 추정된다.)
Atlas Width & Atlas Height를 256x256으로 줄여주면...
위와 같이 64KB짜리 텍스쳐로 줄어들게 된다.
축하한다! 당신은 16MB짜리 텍스쳐를 64KB로 줄였다. 무려 99.6%나 감소시킨 셈이다.
여기서 Sampling Point Size를 문제되지 않는 수준까지 줄이면 더 줄일 수도 있다.
(*추가: 단, 주의해야 할 점은 dynamic 폰트를 사용한다면 플레이 중에 새로운 글자를 보여주는 경우 이 텍스쳐에 글자들이 새로 추가된다는 점이다. 그러면 텍스쳐가 여러 장으로 분리되고 각 글자마다 참조하는 텍스쳐가 다른 경우 드로우콜이 늘어날 수 있기 때문에 해당 폰트에 사용될 글자수를 예측하여 적절한 크기로 할당하여 균형을 잡는 것이 중요하다.)
줄인 이후 asset 용량이 32,831KB -> 146KB로 줄어든 것도 확인할 수 있다.
Atlas Population Mode를 Dynamic으로 설정하고, Multi Atlas Textures에 체크한 다음 Atlas의 사이즈를 줄여서 사이즈를 더 줄일 수도 있지만, 이 경우 드로우콜이 늘어나고 오버헤드가 발생할 수 있어 최대 2개 정도로만 쪼개지도록 해놨다. (용량이 큰 폰트만)
내가 읽은 설명 상으로는 Dynamic으로 해놓으면 글자를 사용하기 전까지는 Atlas에 그려지지 않는 것으로 알고 있는데, 실제로 메모리를 찍어보니 처음부터 전체 텍스쳐가 한꺼번에 메모리에 올라가 있는 것을 확인할 수 있었다.
* Dynamic은 사전에 미리 Character Table에 등록해놓지 않은 경우에도 ttf에서 실시간으로 글자를 불러와서 Atlas에 추가하는 식으로 작동되는 것 같다. (이 때문에 에디터에서 플레이를 하고 저장하면 Font Asset에 계속 변경사항이 생기는 문제가 있었다.) 아예 비워놓는 게 메모리 측면에서는 가장 이득이겠지만, 그러면 모든 글자에 대해 동적인 로딩이 발생할 테니 자주 사용하는 글자 정도는 추가해두는 게 성능과도 타협을 볼 수 있는 방법일 것 같다.
* 드로우콜을 줄이기 위해서라도 지속적으로 UI에 나타나는 글자들은 같은 Atlas에 담는 것이 좋을 것 같다. 이를 위해서는 직접 글자들을 수집해서 저장할 수도 있겠고, 아예 비워놓으면 플레이하면서 알아서 첫 Atlas에 저장될 것 같다. (때문에 Atlas 크기를 너무 작지 않게 하는 것이 좋겠다.) 이전에 한글 XXXX자를 미리 생성해서 넣어놓는 방식을 사용하기도 했었는데, 확인해보니 실제로 사용되지 않는 글자들이 많을 뿐더러 Dynamic으로 글자를 가져오는 게 그렇게 느리지도 않은 것 같다. 효율적인 메모리 관리와 배칭을 위해서 Dynamic을 사용해야 하는 경우, Atlas에 최소한의 ASCII 문자들만 넣어놓는 것도 좋은 선택일 것 같다.
============================================
2. 폰트 중복 제거하기
메모리 프로파일러로 찍어서 확인을 해보면 위와 같이 같은 폰트의 Atlas가 중복으로 메모리에 올라가 있는 모습을 확인하게 될 수도 있다.
이 문제는 몇 가지 경우로 나뉜다.
1. 에셋 번들을 빌드할 때 Addressables를 사용하는 중인가?
-> (아마도 내가 아는 선에서는) 폰트 중복 문제를 완벽하게 해결할 수 있을 것이다. 폰트를 앱 빌드할 때 포함되도록 하면 해결된다고 알고 있다.
2. 에셋 번들을 빌드할 때 Scriptable Build Pipeline이 아닌 기본 AssetBundle의 Pipeline을 사용하는 중인가?
-> 이전에 언급했던, 번들을 다른 옵션으로 빌드하기 위해 분리해서 빌드하는 경우 Dependencies가 제대로 참조되지 않아서 폰트가 중복될 수 있다.
예시)
폰트 데이터가 포함된 Font 번들을 LZ4 형식으로 압축하여 빌드
해당 폰트를 사용하는 A, B, C 번들을 LZMA 형식으로 압축하여 빌드
Scriptable Build Pipeline으로 빌드하는 경우 한꺼번에 빌드가 되어서 A -> Font / B -> Font / C -> Font 각각 번들을 잘 참조하지만, 기본 AssetBundle의 Pipeline을 사용하는 경우 Font 번들과 A, B, C 번들이 따로 빌드되기 때문에 (Font) / A(A + Font) / B(B + Font) / C(C + Font) 로 각각 나뉘어 빌드된다.
이 경우, Font 번들이 로드된 상태에서 A 번들을 로드하면, A 번들을 로드하면 A 번들은 A 번들을 빌드할 때 포함된 Font 데이터를 참조한다. A 번들 내에 들어있는 Font 데이터는 Font 번들에 있는 Font 데이터와는 별개로 인식되기 때문에 메모리에 중복으로 적재된다. B / C 번들을 로드할 때도 동일한 현상이 발생해서 이 경우 최대 4개의 폰트 데이터 중복 적재가 일어나게 된다.
특히나 폰트에 Fallback을 사용해서 32MB짜리 폰트 3개를 묶어서 사용 중이라면 순식간에 300MB에 달하는 메모리 내 중복이 발생하는 것이다.
3. Addressables 외에 Scriptable Build Pipeline 혹은 AssetBundle의 Pipeline을 사용하는 중인가?
-> 이 경우 AssetBundle에 포함되는 폰트와, 앱 빌드 시 포함되는 폰트를 하나로 합칠 수 없기 때문에 강제로 분리하여 사용하게 된다. 게임을 시작할 때 AssetBundle을 받기 전 텍스트를 보여줘야 할 텐데, 이때 보여주는 폰트는 앱 빌드 시 포함되는 폰트이고, 이후 AssetBundle에서 로드하여 보여주는 폰트는 AssetBundle에 포함된 폰트라 서로 다른 폰트이다. 폰트 데이터는 씬을 넘어가야 사라지는 것으로 추정되니 AssetBundle에 포함된 폰트를 보여주는 경우 맨 처음 씬과 분리해야 폰트 중복을 막을 수 있을 것이다.
============================================
[주의해야 할 점]
1. TMP Settings를 사용하는 경우 Fallback으로 등록되는 듯 하니 주의하자.
TMP Settings라는 Asset이 존재한다. Default Font Asset은 기본적으로 어떤 폰트를 사용할 것이냐?에 해당하는 부분인데, 문제는 얘가 모든 폰트에 대해 Fallback으로 지정되는 것으로 추정된다는 점이다.
즉, 얘를 앱 빌드할 때 포함시킨다면...
Default Font Asset에 들어간 폰트가 앱 빌드에 포함되고, 모든 폰트에 대해서 Fallback으로 지정되기 때문에 에셋 번들에 포함된 폰트만 사용하더라도 항상 앱 빌드에 포함된 폰트가 메모리에 같이 적재되는 현상이 발생한다.
이걸 피하기 위해서 Default Font Asset과 Fallback을 지워준 후에 Default Font Asset에 해당하는 폰트들을 각각의 폰트의 Fallback으로 달아주었는데, 지금 생각해보니 TMP Settings를 에셋 번들에 포함시키면 될 것 같기도 하다. (이렇게 해도 앱 빌드에 포함되지 않는다는 전제 하에. 이거는 테스트를 해 봐야 할 것 같다.)
=> 이 방법은 유효하지 않았다. 아마 TMP Settings가 필수적으로 저 위치에 포함되어야 하고, 무조건 앱 빌드에 포함되었던 것으로 기억함. (이것까진 확실하지 않음)
2. AssetBundle과 Resources 폴더의 폰트 배치에 주의하자.
2-1. Resources 폴더에만 폰트를 넣고 AssetBundle을 빌드하는 경우
먼저 Resources 폴더에 있기 때문에 앱을 빌드할 때 포함된다. 그리고 AssetBundle를 빌드할 때도 Resources 폴더의 폰트를 함께 빌드한다. 얘네는 정확히 어떤 식으로 Dependencies가 지정될지 확인은 안 해봤지만, AssetBundle에 명시적으로 포함하는 것이 좋을 것으로 추정된다.
2-2. AssetBundle에만 폰트를 넣지만, 빌드에 포함되는 씬에서 해당 폰트를 사용하는 경우
AssetBundle에 폰트가 정상적으로 포함되고, 앱 빌드에도 폰트가 들어간다.
2-3. AssetBundle과 Resources 폴더에 동일한 폰트를 각각 넣는 경우
특정 경우에 문제가 발생할 수 있다.
예를 들어서...
1. AssetBundle에서 폰트 A, B, C, D를 사용하고 있다.
2. A의 Fallback으로 B, C, D가 등록되어 있다.
3. 빌드에 포함된 씬에서 폰트 A를 사용하고 있으니 Fallback 폰트인 B, C, D를 Resources 폴더에 따로 넣어준다.
위의 경우에 문제가 발생할 수 있다.
이미 에셋 번들에 포함된 A를 앱 빌드할 때 포함시키면서 Fallback 폰트인 B, C, D도 같이 앱 빌드에 포함된다.
그리고 Resources 폴더에 들어있는 B, C, D는 별개의 폰트이기 때문에 앱 빌드에 별도로 포함해서 빌드한다.
즉, 폰트 A를 사용하면서 Fallback으로 사용되는 B, C, D는 AssetBundle 폴더에 들어있는 B, C, D이고, Resources 폴더에 넣어준 B, C, D는 실제로는 사용하지 않으면서 앱 빌드에만 포함되는 셈이다. 이 경우 메모리에는 적재되지 않겠지만, apk 빌드 사이즈가 커질 수 있으니 조심해야 한다.
이외에도 SDF를 만들 때 필요한 글자만 지정해서 만들 수 있는데, AssetBundle을 받기 전에 보여주는 글자는 어느 정도 정해져 있으니까 해당 글자만 담아서 따로 폰트를 만드는 방식으로 apk빌드 파일을 줄이는 방법도 있을 것이다.