비트맵 및 메모리

비트맵 객체는 애플리케이션의 메모리 사용 공간에 가장 큰 영향을 미치는 단일 요소인 경우가 많습니다. 앱 아이콘, 알림 이미지, 미디어 콘텐츠 등 비효율적인 비트맵 처리는 메모리 부족 (OOM) 오류와 시스템 전체의 메모리 압박으로 이어질 수 있습니다.

비트맵 구성 및 픽셀 데이터

비트맵이 사용하는 메모리 양은 주로 크기 (가로 × 세로)와 구성 (Bitmap.Config)에 따라 결정됩니다.

구성은 각 픽셀을 나타내는 데 사용되는 바이트 수를 정의합니다.

구성 픽셀당 바이트 설명
ALPHA_8 1 알파 (투명도) 채널만 해당합니다. 마스크에 유용합니다.
RGB_565 2 빨간색 (5비트), 녹색 (6비트), 파란색 (5비트)입니다. 알파가 없습니다. 높은 색상 충실도가 중요하지 않은 불투명 이미지에 적합합니다.
ARGB_8888 4 알파, 빨간색, 녹색, 파란색 (각각 8비트)입니다. 기본값이며 가장 일반적입니다.
RGBA_F16 8 반정밀도 부동 소수점입니다. 광색역 및 HDR 콘텐츠에 사용됩니다.
HARDWARE 해당 사항 없음 그래픽 메모리 (gralloc/DMABuf)에 저장됩니다. 하드웨어 비트맵을 참고하세요.

메모리 공식: Memory (Bytes) = Width × Height × Bytes Per Pixel

예를 들어 ARGB_8888의 1080p 기기 (1920x1080)에서 전체 화면 이미지는 1920 × 1080 × 4바이트 ≈ 8.3MB를 사용합니다.

힙 비트맵과 공유 비트맵

힙 비트맵 (네이티브 힙)

최신 Android (8.0 이상)에서는 비트맵 픽셀 데이터가 네이티브 힙에 저장되고 작은 래퍼 객체만 Java 힙에 상주합니다.

앱이 이미지를 표시해야 하는 경우 일반적으로 압축된 이미지 파일에서 비트맵으로 디코딩되어 힙에 저장됩니다.

공유 비트맵 (ashmem/memfd)

비트맵이 프로세스 간에 전송될 때 (예: 알림을 위해 Binder를 통해 SystemUI로) Android는 공유 메모리 (ashmem 또는 memfd)를 사용하여 픽셀 데이터 복사를 방지합니다.

Bitmap 인스턴스는 Bitmap.asShared()을 호출하여 공유 메모리에 명시적으로 복사하거나 BitmapParcel 내부에 배치되고 (일반적으로 비트맵을 Bundle과 같은 Parcelable에 추가하여) Binder IPC를 통해 전송되는 경우 암시적으로 복사할 수 있습니다.

공유 비트맵이 Binder IPC를 통해 전송되면 픽셀 데이터 자체는 복사되지 않고 공유 메모리 영역을 참조하는 파일 설명자가 수신자 프로세스에 복제됩니다. 기본 메모리 영역은 여러 프로세스 간에 공유될 수 있으며 이를 참조하는 모든 파일 설명자가 닫힐 때까지 해제되지 않습니다.

변경 가능한 비트맵과 변경 불가능한 비트맵

  • 변경 가능한 비트맵: 생성 후 수정할 수 있습니다 (예: Canvas를 통해). 항상 자체 비공개 메모리 할당이 필요합니다. 변경 가능한 비트맵이 복사되면 딥 복사 (모든 픽셀 데이터의 두 번째 복사본)를 만들어야 합니다.
  • 변경 불가능한 비트맵: 변경할 수 없습니다. 이를 통해 서로 다른 Bitmap 인스턴스 간에 동일한 기본 메모리 버퍼를 공유하는 것과 같은 최적화가 가능합니다. APK 리소스에서 로드된 비트맵 (BitmapFactory)은 일반적으로 변경 불가능합니다.

효율적인 비트맵 처리

비트맵 풀링 및 재사용

비트맵을 자주 할당하고 할당 해제하면 할당 변동이 발생하여 GC가 지속적으로 실행됩니다. 일반적인 이미지 로드 라이브러리는 비트맵 풀 을 사용합니다.

Google은 Java 기반 애플리케이션의 솔루션으로 Glide를, Kotlin 기반 애플리케이션의 솔루션으로 Coil을 권장합니다 (특히 Jetpack Compose를 사용하는 경우).

비트맵이 더 이상 필요하지 않으면 앱은 GC를 실행하는 대신 bitmap.recycle()을 호출하거나 풀로 반환합니다. 다음에 동일한 크기와 구성의 비트맵이 필요할 때 풀은 기존 버퍼를 제공하여 새 할당을 방지합니다.

하드웨어 비트맵

Bitmap.Config.HARDWARE 를 사용하면 그래픽 메모리 (DMABuf)에 픽셀 데이터를 직접 저장할 수 있습니다.

  • 장점:
    • 메모리 절약: 애플리케이션 또는 네이티브 힙을 사용하지 않고 GPU 메모리를 사용합니다. 앱의 UI에 표시되는 비트맵은 어쨌든 GPU 메모리에 복사해야 하는 경우가 많으므로 이 복사 작업과 추가 메모리 비용을 절약할 수 있습니다.
    • 성능: 데이터가 이미 GPU에 있으므로 매우 빠르게 그릴 수 있습니다.
  • 단점:
    • 변경 불가능: 하드웨어 비트맵은 수정할 수 없습니다.
    • 읽기 속도가 느림: CPU에서 픽셀에 액세스하는 것은 매우 비쌉니다 (예: getPixel()).
    • 기여: AHAT와 같은 표준 도구에서 추적하기가 더 어렵습니다 (아래 참고).

실습: 비트맵 탐색

BitmapLab 샘플 앱을 사용하여 이러한 개념을 살펴보겠습니다.

1. dumpsys meminfo로 측정

BitmapLab 을 실행하고 ALLOCATE 10MB ARGB_8888 을 탭합니다. 그런 후 다음 명령어를 실행합니다.

adb shell dumpsys meminfo -s com.android.bitmaplab

최신 Android 버전에서는 Native Allocations 섹션을 찾습니다. 이러한 섹션은 일반적인 App Summary 보다 비트맵에 훨씬 더 나은 기여를 제공합니다:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • Bitmap (malloced): 프로세스의 네이티브 힙에 할당된 비트맵입니다. Android 8.0 이상에서 대부분의 표준 비트맵이 상주하는 위치입니다.
  • Bitmap (nonmalloced): 하드웨어 비트맵 또는 공유 비트맵과 같은 특수 메모리를 사용하는 비트맵입니다 (ashmem 또는 memfd를 통해).

BitmapLab 에서 공유 비트맵 을 할당하면 Bitmap (nonmalloced)에 반영됩니다.

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240
Bitmap (nonmalloced):        1                          10240  # <--- Shared Bitmap!

공유 비트맵 추적

일부 Android 버전 및 커널 구성에서 dumpsys meminfo는 파일 설명자를 통해 프로세스의 주소 공간에 매핑되는 비트맵에 대한 고해상도 추적도 제공합니다.

기본적으로 공유 비트맵은 일반 이름('bitmap')을 사용합니다. 세부 기여 및 고유 비트맵 추적 (서로 다른 프로세스에서 공유 비트맵 식별)을 사용 설정하려면 다음 시스템 속성을 사용 설정해야 합니다.

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

사용 설정하면 /proc/<pid>/smaps의 ashmem 영역에 더 설명적인 이름이 지정됩니다. meminfo 는 이를 활용하며 결과는 다음과 같습니다.

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • 매핑됨: 모든 비트맵 관련 메모리 매핑의 총 크기입니다.
  • 고유: 고유한 비트맵의 크기입니다 (즉, 동일한 기본 공유 비트맵 픽셀 데이터의 두 개 이상의 매핑은 한 번만 계산됨).

2. AHAT의 비트맵

AHAT는 비트맵에 대한 뛰어난 시각화를 제공합니다.

  1. BitmapLab 에서 비트맵을 몇 개 할당합니다.
  2. -b 플래그를 사용하여 힙 덤프를 캡처합니다 (네이티브 비트맵 데이터 포함).

    adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof
    adb pull /data/local/tmp/bitmaps.hprof .
    ahat bitmaps.hprof
    
  3. localhost:7100을 열고 사이드바에서 Bitmaps 링크를 찾거나 Bitmap 클래스를 검색합니다.

  4. AHAT는 실제로 브라우저에서 비트맵을 렌더링하므로 메모리를 많이 사용하는 이미지를 쉽게 식별할 수 있습니다.

렌더링된 비트맵을 표시하는 AHAT

3. Perfetto의 비트맵 트랙

Perfetto는 시간이 지남에 따라 비트맵 할당 및 개수를 추적할 수 있습니다. 이러한 카운터는 특정 애플리케이션에 대해 gfx atrace 카테고리가 사용 설정될 때 Android 프레임워크에서 내보냅니다.

  1. 트레이스를 시작합니다. gfx 카테고리를 포함하고 -a 플래그를 사용하여 특정 앱 패키스를 타겟팅해야 합니다.

    external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \
        -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplab
    
  2. BitmapLab 에서 AllocateClear 버튼을 반복해서 탭합니다.

  3. Parcel/Unparcel Bitmap 도 탭합니다.

  4. ui.perfetto.dev에서 트레이스를 분석합니다.

com.android.bitmaplab의 프로세스 섹션에는 다음이 표시됩니다. * Bitmap Count: 활성 비트맵 수를 보여주는 카운터입니다. * Bitmap Memory: 비트맵에서 사용한 총 바이트 수를 보여주는 카운터입니다.

상위 수준 슬라이스 (Perfetto SDK)

BitmapLabPerfetto SDK 를 사용하여 비트맵 작업의 상위 수준 슬라이스를 내보내기도 합니다. 트레이스에서 BitmapLab_을 검색하여 다음을 찾습니다. * BitmapLab_parcelUnparcel: 파셀링 및 언파셀링 로직을 포함하는 슬라이스입니다. * BitmapLab_postNotification: 알림 게시 흐름을 포함하는 슬라이스입니다.

알림 흐름 추적

Post Notification을 탭하면 앱은 현재 비트맵이 포함된 알림을 만들어 시스템에 전송합니다. 이를 담당하는 프레임워크 코드는 파셀링(Binder IPC를 통해 전송될 파셀에 비트맵 쓰기)과 언파셀링(수신 측의 파셀에서 비트맵 읽기)을 연결하는 흐름 이벤트가 포함된 Perfetto 슬라이스를 내보냅니다.

아래 스크린샷에서 앱이 Binder 트랜잭션에서 사용할 대용량 비트맵을 파셀링하여 알림을 게시하고 system_server 프로세스에서 해당하는 언파셀링을 확인할 수 있습니다.

알림을 통해 BitmapLab에서 system_server로의 흐름을 보여주는 Perfetto

Perfetto를 사용하면 스레드와 프로세스 전반에 걸쳐 전파되는 동일한 알림 비트맵을 추적할 수도 있습니다. 예를 들어 INotificationManager Binder 서버를 구현하는 system_server의 바인더 스레드에서 동일한 비트맵을 알림 셰이드에 표시하기 위해 com.android.systemui로 전달할 수 있는 system_server 작업자 스레드로 전달할 수 있습니다.

시스템 앱 문제

SystemUI (알림) 및 Launcher 와 같은 시스템 앱은 다음과 같은 고유한 문제에 직면합니다.

  1. 무제한 콘텐츠: 알림과 위젯이 많을 수 있습니다. 각각에 대용량 비트맵이 포함되어 있으면 시스템에서 메모리가 빠르게 부족해질 수 있습니다.
  2. 중복: 동일한 앱 아이콘이 런처의 캐시, SystemUI의 알림 영역, 설정 앱에 포함될 수 있습니다.
  3. 하드웨어 버퍼를 통한 공유: 이 문제를 완화하기 위해 시스템 구성요소는 프로세스 간에 인스턴스를 공유하는 중앙 집중식 '이미지 오프로드' 서비스로 HardwareBuffer 이동하고 있습니다.
  4. DMABuf 기여: 하드웨어 비트맵은 힙 공간을 절약하지만 DMABuf 메모리를 사용합니다. DMABuf 메모리는 표준 메모리 도구에서 특정 프로세스에 기여하기가 더 어렵습니다.

    adb shell dmabuf_dump를 사용하여 시스템 전체 DMABuf 할당을 확인합니다. 이 도구는 프로세스별 버퍼 분석을 제공합니다.

     droid.bitmaplab:19562
                      Name              Rss              Pss         nr_procs            Inode               Exporter
                 <unknown>          3840 kB          1280 kB                3             3397              virtio_gpu
                    system            12 kB             4 kB                3             3398                  system
                 <unknown>          3840 kB          1920 kB                2             3399              virtio_gpu
                    system            12 kB             6 kB                2             3400                  system
             PROCESS TOTAL         11556 kB          5136 kB
    
    • RSS: 프로세스에 매핑된 경우 버퍼의 총 크기입니다.
    • Pss: 비례 크기 (버퍼를 공유하는 프로세스 수로 나눈 RSS)입니다. 이는 계산에 가장 적합한 측정항목입니다.
    • nr_procs: 현재 이 버퍼에 대한 참조를 보유하고 있는 프로세스 수입니다.
    • Exporter: 버퍼를 할당한 드라이버입니다 (예: virtio_gpu Cuttlefish의 경우 또는 하드웨어의 벤더별 Ion/DMA-BUF 힙).

    adb shell dmabuf_dump -b를 사용하여 모든 버퍼와 시스템 전체 DMA-BUF 사용량의 요약을 확인할 수도 있습니다.


← Java | ↑ 위로 | 네이티브 →