비트맵 메모리 관리는 시각적으로 풍부한 Android 앱에서 메모리 부족 (OOM) 위험을 완화하는 데 매우 중요합니다. 개발자는 이미지 크기를 줄여야 한다는 것을 알고 있지만, 장기적인 앱 안정성을 달성하려면 재사용과 재활용이라는 두 가지 핵심 요소를 숙달해야 합니다. 이 세션에서는 최신 도구, Glide 및 Coil과 같은 오픈소스 라이브러리, 시스템 수준 API를 사용하여 메모리 성능을 최적화하는 실용적인 플레이북을 제공합니다.
이 세션에서는 Android vitals를 사용하여 실제 메모리 회귀를 진단하고, Glide 및 Coil을 사용하여 고급 캐싱 (재사용) 패턴을 구현하고, onTrimMemory 콜백을 사용하여 메모리를 사전에 해제 (재활용)하는 방법을 알아봅니다.
BitmapRegionDecoder와 같은 API를 사용합니다.onTrimMemory를 처리하여 앱이 백그라운드로 이동할 때 이미지 메모리를 해제합니다. onStop에서 백그라운드 폴링 및 애니메이션을 중지하여 백그라운드 메모리 증가를 방지합니다.스크립트
풍람
네, 점심시간까지 약 1시간 정도 남았으니 계속 함께해 주세요.
저는 런던에서 Android 개발자 관계 엔지니어로 일하고 있는 펑 람입니다.
네, 감사합니다.
시작하기 전에 질문을 드려도 될까요?
올해 새 휴대전화를 구매한 분은 몇 명인가요?
와,
와,
솔직하게 말씀해 주세요. Pixel을 구매하지 않으셔도 괜찮습니다.
새 휴대전화가 더 비싸니까요.
맞습니다. 그래서 일부는 새 휴대전화를 구매하지 않았습니다.
가격이 더 비싼 이유는 무엇인가요?
알고 계시나요?
아니요.
RAM
RAM, 네, 네.
예, RAM은 Android와 개발자에게 정말 어려운 과제입니다.
그래서 오늘은 메모리, 즉 RAM의 문제를 어떻게 극복할 수 있는지 알려드리려고 합니다.
앞서 Google I/O와 Google I/O Connect에서 메모리에 관한 세션이 있었습니다. 운영체제가 메모리를 할당하는 방식, 애플리케이션을 프로파일링하는 방법, R8을 사용하여 코드를 최적화하는 방법, 그리고 물론 당시 비트맵에 대해서도 약간 다루었습니다.
스크린샷에서 내 사진을 볼 수 있습니다.
메모리 최적화는 Android 런타임, 가비지 컬렉션, 성능 도구, 객체 메모리 할당, 컴파일러 등 다양한 영역에 걸쳐 있는 대규모 도메인 중 하나입니다. 이 세션에서는 30분 동안 비트맵 메모리를 최적화하는 방법에 관한 정보를 제공해 드리겠습니다.
왜 비트맵을 사용하는지 궁금하실 수 있습니다.
비트맵 메모리에 집중해야 하는 이유
이미지 한 장이 천 마디 말보다 더 가치 있기 때문입니다.
맞습니다. 그 이유를 설명해 드리겠습니다.
Pixel 7, Pixel 9, Pixel 11 Pro 버전과 같은 휴대전화 화면의 화면 해상도는 약 2,500, 1,200, 1,400입니다.
따라서 각 픽셀에 대해 32비트의 색상 심도를 전달합니다.
전체 화면 이미지의 경우, 손가락이 부족하지만, 약 15MB의 메모리, 50MB가 필요합니다.
OS 시스템의 단어는 64비트, 즉 8바이트입니다.
1, 000단어는 8MB에 불과합니다.
8K입니다.
따라서 이미지 한 장이 천 마디 말보다 더 가치 있는 것은 분명합니다.
네, 중요한 부분입니다.
이미지 하나가 이미 15MB입니다.
앱에 이미지가 10개 또는 100개 있다면 어떻게 될까요?
큰 숫자입니다.
따라서 실제로 비트맵은 항상, 일반적으로 애플리케이션에서 가장 큰 메모리 소비자입니다.
앞서 언급했듯이 이미지가 메모리로 디코딩되면 크기와 색상 심도로 확장됩니다.
파일 크기가 JPEG인지 PNG 형식인지가 아닙니다.
아니요. 디코딩되어 메모리를 사용합니다.
따라서 최대 해상도 이미지가 몇 개만 있어도 기기 메모리에서 수백 MB를 쉽게 차지할 수 있습니다.
둘째, 메모리가 매우 부족하면 운영체제에서 메모리 소비를 처리하는 데 더 많은 전력을 소비해야 합니다.
이로 인해 UI가 버벅거리는 것처럼 보일 수 있습니다.
또는 OS가 메모리를 확보하기 위해 앱을 종료할 수도 있습니다.
그렇지 않으면 앱에서 메모리 부족 오류, 메모리 부족 예외가 발생합니다.
다행히도 이 얼굴은 적은 노력으로도 큰 효과를 볼 수 있습니다.
대부분의 메모리 급증은 일반적인 실수로 인해 발생하며, 이는 피할 수 있고 이미지 품질을 저하시키지 않고도 해결할 수 있습니다.
이제 비트맵 메모리가 애플리케이션의 원활한 작동뿐만 아니라 안정성과 사용자 유지에도 중요하다는 것을 알게 되었습니다.
따라서 오늘은 앱을 원활하게 운영하기 위한 5가지 간단한 단계를 살펴보겠습니다.
먼저 Google Play 기술 품질, 즉 새로운 기술 품질 요구사항을 살펴보겠습니다. 여기에는 향후 적용될 메모리 제한과 이 제한을 쉽게 확인할 수 있는 위치가 포함됩니다.
둘째, 500개의 Android 애플리케이션에 관한 우수사례를 공유하겠습니다. 메모리에서 어떻게 사용되는지, 올바른 방식으로 사용되고 있는지, 배울 점이 있는지 살펴보겠습니다.
그런 다음 비트맵 메모리 사용량을 최적화하는 원리를 보여드리겠습니다.
마지막으로, 자체 앱을 확인하는 데 도움이 되는 몇 가지 도구를 공유해 드리겠습니다.
풍람
첫 번째는 Google Play의 새로운 품질 요구사항입니다.
Google Play Console을 얼마나 자주 방문하시나요?
매월?
매주?
전혀 없나요?
웃는 건 동의한다는 뜻이겠죠?
아니요?
아니요.
Android vitals 부분을 방문하셨나요?
네, 네.
네, 제가 이 영상을 만들기 전에 이미 알고 계셨군요.
따라서 Google Play에는 성능 기준에 관한 새로운 품질 요구사항이 있습니다.
Android 및 Play는 사용자가 양질의 앱을 사용하기를 원합니다.
따라서 이 기준점도 최종 사용자에게 최상의 환경을 제공하기 위한 것입니다.
간단히 말해, '앱이 백그라운드에서 실행되거나 관찰 가능한 포그라운드 노출 영역에서 실행될 때 메모리 사용량의 90번째 백분위수가 이 기준점을 초과해서는 안 됩니다.
그렇지 않으면 Play 스토어에서 앱의 노출이 줄어들 수 있습니다."
이 정책은 2027년 2월부터 앱과 게임에 적용됩니다.
이 기준점을 초과하면 앱 상태 평가에 직접적인 영향을 미칩니다.
따라서 Google Play Console로 이동하여 '오늘의 상위 90번째 백분위수는 얼마인가?'를 확인하세요.
기한 전에 개선하세요.
이것이 애니메이션입니다.
"Google Play Console로 이동하면 Android vitals가 표시됩니다.
메모리 부분이 있습니다.
90번째 백분위수를 확인할 수 있습니다.
클릭하면 시간 경과에 따른 평균 메모리 소비량을 확인할 수 있습니다.
또한 RAM, 폼 팩터, 화면 밀도, OS 버전별로 다양한 등급이 있어 특정 보급형 기기 또는 특정 앱 버전에서 메모리 병목 현상을 격리하는 데 도움이 됩니다.
'앱이 기준점을 초과했다고 알려줘서 고마워.
하지만 정말로 무엇이 문제인지 모르겠습니다.
도와주시겠어요?"
네, 그래서 제가 도와드리려고 합니다.
쉽게 설명해 드리겠습니다. TV를 시청하고 있는데 TV에 동영상이나 오디오가 나오지 않으면 어떻게 하시겠어요?
TV 설명서를 꺼내서 마지막 몇 페이지를 보면 보통 문제 해결 가이드가 있습니다. "TV에서 소리가 나지 않으면 이 버튼을 누르거나 저 버튼을 누르세요."라고 나와 있죠.
네, 마찬가지로 오늘 몇 가지 일반적인 문제를 확인했습니다.
이 가이드가 문제 해결에 도움이 되기를 바랍니다.
일반적으로 TV 설명서를 읽을 때는 1페이지부터 시작해야 합니다. 1페이지는 보통 진단 도구 키트로, TV가 어떻게 작동하는지 이해하는 데 도움이 됩니다.
하지만 이제는 이 부분이 덜 흥미롭다는 것을 알게 되었으니, 오늘은 문제 해결부터 먼저 진행하고 나중에 다시 돌아오도록 하겠습니다.
솔직히 말해서, 저는 TV 설명서를 이렇게 읽습니다.
Android 생태계에서 가장 빈번하게 발생하는 비트맵 할당 비트맵 메모리 문제를 파악하기 위해 Android에서 인기 있는 앱 약 500개를 조사했습니다.
앱을 실제로 실행하고, 시작하고, 로그인하고, 다른 페이지로 이동하고, 깊이 들어가고, 돌아옵니다.
그리고 다양한 RAM, 다양한 기기에서 앱을 테스트합니다. 예를 들어 2GB 메모리가 탑재된 Android Go 기기, 4GB의 엔트리 레벨 기기, 4GB, 6GB~8GB의 미드레벨 기기, 화면이 매우 크고 화면 품질이 우수하며 16GB 메모리가 탑재된 프리미엄 폴더블 기기에서 테스트합니다.
다양한 앱 상태에서 이 기기의 메모리 사용량을 측정하고, 힙 덤프를 캡처하고, Perfetto AI Skill을 비롯한 다양한 도구를 사용하여 할당을 분석하고, 일반적인 안티패턴, 즉 우리가 대규모로 저지르는 일반적인 실수를 식별했습니다.
이것이 다양한 활동 상태에서 메모리가 변경되는 방식입니다.
이 분석을 통해 크기가 너무 큰 이미지, 백그라운드 캐시 증가 등 15가지가 넘는 일반적인 문제를 식별합니다.
이것이 오늘 공유하고 살펴볼 핵심 우수사례입니다.
풍람
예, 지금 공유하는 모든 우수사례는 500개 앱 연구에 관한 것입니다.
여기 계신 분들은 모두 Android를 좋아하시죠?
우리 모두 Android를 좋아합니다.
Android를 좋아하지만 Android 개발 전문가는 아닌 사람이 있다고 가정해 보겠습니다.
그래서 Android 피규어를 주문하는 Android 앱을 만듭니다.
하지만 몇 가지 실수가 있습니다.
이야기해 봅시다. 이것이 첫 번째 실수일 수도 있습니다.
"제품 페이지에 다양한 Android 피규어를 보여주는 2열 레이아웃이 있습니다.
각 열의 이미지는 약 400x400픽셀 크기입니다.
하지만 실제로 앱은 화면 크기와 관계없이 서버에서 하나의 이미지를 다운로드합니다.
3000x3000픽셀 이미지를 다운로드하고 있습니다.
예, 개발자가 "Android는 정말 아름답고 놀랍습니다.
사용자에게 최고의 콘텐츠를 보여주고 싶습니다."
하지만 실제로는 그렇지 않습니다. 이미지 뷰의 크기에 따라 제한되기 때문입니다.
따라서 큰 이미지를 다운로드하더라도 사용자에게 최상의 상태로 표시되지 않습니다.
또한 이 큰 이미지로 인해 추가 메모리가 발생했습니다.
다운샘플링을 하더라도 다운샘플링 과정에서 추가 메모리가 사용됩니다.
이 개발자가 문제를 해결하도록 어떻게 도와야 하나요?
CDN에 대해 알고 있다면 CDN이 실제로 자르기, 크기 조절, 회전, 필터 적용, 정사각형에서 원형 이미지로 변환과 같은 일부 이미지 작업을 처리할 수 있다는 것을 알 수 있습니다.
HTTP 요청을 통해 CDN이 무언가를 처리할 수 있습니다.
따라서 서버 측에서 하나의 이미지만 호스팅하는 경우에도 CDN을 사용하여 크기가 조절된 이미지를 기기로 전송할 수 있습니다.
첫 번째로, 이미지를 요청할 때 CDN을 사용해 보세요. 그러면 ImageView 크기로 이미지를 요청하게 됩니다.
하지만 불가능한 경우(예: ImageView보다 약간 더 클 수 있는 경우) 코드에서 다운샘플링하여 크기를 조절해야 합니다.
따라서 메모리에 로드될 때 전체 해상도의 이미지가 아닌 더 작은 이미지가 로드됩니다.
다음은 Coil의 코드 샘플입니다.
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(sizedUrl)
.size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
.precision(Precision.EXACT) // Downsample to exact card size
.bitmapConfig(Bitmap.Config.HARDWARE)
.build(),
contentDescription = product.title
)노란색 문구로 크기를 전달할 수 있습니다.
예, HTTP 요청에서 크기를 HTTP로 전달할 수 있습니다.
강조 표시된 코드에서 Size.ORIGINAL를 사용하지 않는 것을 볼 수 있습니다.
너비와 높이를 사용하면 매개변수로 전달됩니다.
정밀도를 사용하여 이미지를 정확한 크기로 다운샘플링합니다.
가능하면 Bitmap.Config.HARDWARE를 사용하세요. 이미지가 VRAM에 직접 저장되므로 메모리도 적게 사용합니다.
마찬가지로 이 앱에서 Android 수집품의 새 버전이 출시되었다고 가정해 보겠습니다. 사용자가 왼쪽과 오른쪽으로 스와이프하여 새 Android 피규어의 다양한 디자인을 볼 수 있는 긴 이미지가 있습니다.
하지만 여기에는 또 다른 문제가 있습니다.
긴 이미지의 경우 실제로 많은 부분이 화면을 벗어나므로 사용자가 볼 수 없지만 이미지가 메모리에 로드됩니다.
더 잘할 수 있을까요?
예.
이미지를 더 작게 잘라서 화면에 표시되는 이미지만 로드되도록 할 수 있습니다.
화면을 벗어난 항목은 로드되지 않거나 언로드됩니다.
따라서 메모리를 적게 사용합니다.
마찬가지로 이 경우 메모리의 약 60% 가 절약되었습니다.
물론 더 작은 직사각형으로 자르면 더 많이 절약할 수 있습니다.
하지만 마찬가지로 사용자가 Android 피규어의 모든 세부정보를 확대하여 볼 수 있는 기능을 제공하려는 경우, 흔히 저지르는 실수는 매우 큰 이미지를 다시 로드하여 확대하고 또 확대할 수 있도록 하는 것입니다.
하지만 이 작업을 수행할 필요는 없습니다.
더 나은 방법은 사용자가 이미 로드된 저해상도 이미지를 확대하고 손가락을 모으는 동작을 중지하면 좌표를 계산한 다음 고해상도 이미지에서 계산된 영역만 로드하는 것입니다.
따라서 보이는 영역만 메모리에 로드됩니다.
어떻게 해야 하나요?
이미지의 특정 영역을 로드하는 데 도움이 되는 BitmapRegionDecoder라는 API가 있습니다.
물론 도움이 될 수 있는 도구와 라이브러리도 있습니다.
GitHub에서 검색할 수 있습니다.
일부 만화 앱이나 지도에서 이미 이 기능을 사용하고 있습니다.
참고용으로 사용할 수 있습니다.
또한 우리는 인간이므로 실수를 합니다.
AI는 실수를 할 수 있습니다.
따라서 실수를 방지하기 위해 이미지를 로드할 때 제한을 추가할 수도 있습니다.
예를 들어 이 경우 "창 크기나 화면 크기를 한도로 사용하고 이를 min 함수에 넣어 요청 너비 또는 경계 너비의 최솟값을 구하자"라고 말할 수 있습니다.
// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(activity).bounds
// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth = minOf(requestedWidthPx, bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())
val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)이 함수를 사용하면 이미지를 로드할 때마다 경계가 존재하지 않게 됩니다.
따라서 화면 해상도보다 큰 이미지는 로드되지 않습니다.
이미지의 보이는 영역만 디코딩해야 메모리를 절약할 수 있습니다.
풍람
이 앱은 한 개발자가 만든 것이 아닙니다.
사실 여러 기여자가 참여하고 있으며 서로 다른 도구를 사용하고 있을 수 있습니다.
예를 들어, 일부는 죄송합니다.
다시 시작해 보겠습니다.
이 앱도 마찬가지입니다.
제품 페이지에 제품 목록이 있고 이 이미지가 표시되어 장바구니에 추가했습니다.
몇 분 후 결제하러 가면 동일한 이미지가 다시 표시됩니다.
하지만 이 이미지가 정말로 동일한 이미지일까요? 아니면 메모리에 있는 두 개의 서로 다른 객체일까요?
예, 흔히 발견되는 실수 중 하나는 앱에서 이미지를 많이 로드하는 것입니다.
하지만 메모리에 중복 이미지가 많이 있습니다.
이미지 로더를 사용하는 경우 실제로 캐싱을 수행하기 위한 키를 전달할 수 있습니다.
따라서 동일한 이미지를 사용할 때 새 객체를 만드는 대신 이미 할당된 비트맵 객체를 사용할 수 있습니다.
Android 스튜디오에서 메모리 프로파일러를 사용하여 앱에 중복 비트맵이 있는지 확인할 수 있습니다.
Google I/O 세션에서 다루었으니 YouTube에서 동영상을 다시 시청하실 수 있습니다.
Android 스튜디오를 사용하지 않는 경우 adb 명령어를 사용하여 힙 덤프를 캡처한 다음 Perfetto 힙 그래프 SQL 언어를 사용하여 앱에 중복 비트맵이 있는지 쿼리할 수 있습니다.
이것은 Coil의 코드 예시로, 비트맵 캐싱을 위한 키를 추가하는 방법을 보여줍니다.
// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
model = ImageRequest.Builder(context)
.data(productUrl)
.placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
.memoryCacheKey(canonicalKey)
.diskCacheKey(productUrl.substringBefore("?"))
.build(),
contentDescription = product.title
)이제는 가능합니다.
이전 예시로 돌아갑니다.
안녕하세요. 제 앱의 개발자가 다릅니다.
일부는 뷰를 사용하므로 Glide를 이미지 로더로 사용합니다.
일부는 Compose를 사용하기 때문에 Coil을 사용하고 있습니다.
방금 말씀하신 이미지 로더가 캐싱을 도와준다고 하셨는데, 이미지 로더가 두 개 이상 있으면 문제가 되나요?
문제가 있는 것이 분명합니다.
따라서 이미지 로더의 경우 기본적으로 힙 메모리 크기의 X% 를 기본 캐시 크기로 사용합니다.
따라서 이미지 로더가 하나 있으면 메모리의 X% 가 사용됩니다.
이미지 로더가 두 개 있으면 메모리가 2배가 됩니다.
이미지 로더가 3개인 경우 3배가 됩니다.
이미지 로더가 동일한 풀을 공유하지 않는 경우에도 마찬가지입니다.
제품 페이지에서와 마찬가지로 Glide를 사용하여 이미지를 로드한 다음 결제 페이지에서 Coil을 사용하여 동일한 이미지를 로드합니다.
하지만 추억에는 여전히 두 개의 이미지가 있습니다.
좋지 않네요.
네, 이 문제를 해결하려면 이상적으로는 하나의 이미지 로더 또는 하나의 공유 풀을 캐싱에 사용하면 됩니다.
하지만 엔지니어링 작업이 너무 많아서 할 수 없다면 어떻게 해야 할까요?
더 쉬운 해결 방법은 이 이미지 로더가 처리할 이미지의 비율과 다른 이미지 로더가 처리할 이미지의 비율을 측정하고 이 코드와 같이 다른 이미지 로더의 가중치를 설정하는 것입니다.
val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
MemoryCache.Builder()
.maxSizeBytes(simpleSplitBudget)
.build()loaderABudget를 70%, loaderBBudget를 30%로 설정할 수도 있습니다.
각 로더가 처리할 이미지의 양을 정말로 알 수 없는 경우에도 분할하는 것이 좋습니다.
이미지 로더가 2개 있으면 2로 나누고, 이미지 로더가 3개 있으면 3으로 나눕니다. 기본값을 사용하면 메모리가 과도하게 사용되기 때문입니다.
제품 페이지에서 사용자가 제품을 더 보기 위해 스크롤할 것으로 예상한다고 가정해 보겠습니다.
따라서 일반적으로 약 50개의 이미지 또는 50MB의 이미지를 캐시할 수 있습니다.
하지만 앱이 백그라운드로 전환되면 어떻게 될까요?
앱이 백그라운드에 있으면 사용자가 탐색하지 않을 수 있습니다.
사용자가 탐색하지 않는 경우에도 이 정도의 캐시 이미지를 메모리에 유지해야 할까요?
아니요, 출시해야 합니다.
예, onTrimMemory 콜백을 준수해야 합니다.
예, 실제로 현재 많은 앱이 onTrimMemory 콜백을 구현하지 않거나 준수하지 않습니다.
onTrimMemory 콜백의 경우 처리해야 하는 신호가 두 가지 있습니다.
하나는 UI_HIDDEN입니다. 사용자가 홈 버튼을 누른 것과 같으며, 홈 버튼을 누른 직후가 아니라 홈 버튼을 누른 후 일정 시간이 지난 후입니다. 즉, '앱이 실제로 사용자에게 표시되는 UI가 없는 상태'라는 의미입니다.
그러면 '사용자가 앱을 사용하지 않고 있습니다.
리소스를 OS에 해제해야 합니다."
따라서 캐시 크기를 줄여야 합니다.
다른 하나는 TRIM_MEMORY_BACKGROUND입니다. 이는 앱이 이미 오랫동안 백그라운드에 있었고 이제 OS에 리소스가 필요하다는 의미입니다.
이 단계에서는 메모리를 최대한 많이, 거의 100% 해제해야 합니다.
이렇게 하면 OS에서 앱이 종료될 가능성, 즉 위험도 줄일 수 있습니다.
'메모리를 원하면 제공하고 OS가 처리하도록 하면 된다'고 생각했기 때문입니다.
그렇지 않으면 OS는 어쩔 수 없이 무언가를 해야 합니다.
풍람
또 다른 주요 실수는 "앱이 백그라운드로 전환되면 메모리를 적게 사용해야 한다고 말씀드렸습니다."라고 말하는 것입니다.
하지만 백그라운드에서 더 많은 메모리를 사용하는 앱이 있었습니다.
이유가 무엇인가요?
그래서 제가 조사를 하는 겁니다.
이유를 알아보려고 노력했습니다.
그 이유 중 하나는 '앱에 애니메이션이나 캐러셀이 있는데,
예를 들어 15초마다 새 이미지를 다운로드하고 사용자에게 새 이미지를 표시합니다."
하지만 개발자가 앱이 백그라운드로 전환될 때 애니메이션을 중지하지 않았습니다.
따라서 앱이 백그라운드에 있어도 코드가 계속 실행되고, 새 이미지를 가져오고, 새 이미지를 디코딩하고, 더 많은 메모리를 사용합니다.
따라서 이는 앱에서 해서는 안 되는 작업입니다.
네
앱이 백그라운드로 전환될 때 모든 애니메이션, 캐싱 또는 관련 없는 작업을 중지해야 합니다.
따라서 Activity 수명 주기를 알고 있다면 onPause, onStop 이벤트가 있으므로 onStop, onPause, onDestroyView를 구현하여 애니메이션을 중지하고, 이미지 pull 요청을 중지하거나, 사용자의 주의가 필요하지 않은 작업과 관련된 기타 작업을 중지해야 합니다.
override fun onStop() {
super.onStop()
// 1. Stop animations so they don't decode next frames in background!
bannerAnimationView.cancelAnimation()
animatedImageDrawable?.stop()
// 2. Cancel next-image buffering / prefetch queues!
nextImagesBufferingJob?.cancel()
Glide.with(this).pauseRequests()
}
override fun onDestroyView() {
super.onDestroyView()
bannerAnimationView.clearAnimation() // Release resources!
}풍람
이미 많은 케이스를 논의했지만, 제가 '15개 이상의 케이스가 있다'고 말씀드렸습니다.
모든 케이스를 공유하면 다 잊어버리실 수도 있으니까요.
집에 돌아가서 Google에서 '메모리 문제를 해결하려면 어떻게 해야 하나요?'라고 검색하면 됩니다.
그래서 기억하기 쉽게 만들어 드립니다.
네
기억하기 쉽도록 잘 알려진 3R 프레임워크에 연결해 보겠습니다.
이 3R 프레임워크는 누구나 들어봤을 것입니다.
첫 번째는 이미지를 로드할 때 메모리를 줄이는 방법입니다.
물론 앞서 언급했듯이 서버에서 이미지를 가져올 때 다운샘플링하여 올바른 크기의 이미지를 가져올 수 있습니다.
색상 심도를 최적화하거나 가능하면 하드웨어 비트맵을 사용합니다.
CDN을 사용하지 않는 경우 CDN을 사용해 보세요.
따라서 서버에서 이미지를 가져올 때 크기에 가장 적합한 이미지를 가져옵니다.
재사용: 캐시를 사용하여 메모리에 로드된 이미지를 재사용하고 메모리에 중복된 비트맵 이미지가 없도록 합니다.
물론 코드에 이미지 로더가 여러 개 있으면 효율성이 떨어지므로 여러 개를 사용하지 않도록 합니다.
Reduce, reuse, and what is the remaining one?
알고 계시나요?
재활용하세요.
다시 시도하시겠습니까?
재활용, 그래, 재활용.
맞습니다. 초등학교에서 코딩을 배우지는 않지만, 줄이기, 재사용, 재활용은 배웁니다.
재활용, 맞습니다.
네, 다른 애플리케이션이나 OS에서 메모리를 사용할 수 있도록 메모리를 해제하는 방법에 관한 것입니다.
재활용에 관한 내용도 있습니다.
먼저 onTrimMemory 이벤트가 있다고 말씀드렸습니다. UI가 숨겨지거나 OS에 리소스가 정말로 필요할 때 OS가 전송하는 신호가 두 개 있습니다.
예, 이 요청을 처리해 보세요.
물론 애니메이션, 이미지 로더 또는 포그라운드에서 실행되는 다른 작업이 있는 경우 onPause, onStop 이벤트가 발생하면 중지해야 합니다.
그러니 줄이고, 재사용하고, 재활용하세요.
기억하기 쉽죠?
풍람
이제 TV 설명서의 덜 흥미로운 부분으로 돌아가겠습니다.
이 메모리 문제를 효과적으로 측정하기 위해 다양한 도구 키트를 어떻게 사용하나요?
첫 번째는 Google Play Console, Android vitals, Reporting API입니다.
예, Google Play Console에서 상위 10%를 확인하세요.
현재 위치는 어디인가요?
웹페이지를 방문하지 않는 경우 Reporting API를 사용합니다.
API를 사용하여 값을 가져올 수 있습니다.
네, 기준점이 200MB와 400MB인지 확인하세요.
기준점을 초과하지 않는지 확인하세요.
힙 덤프를 캡처하거나 이벤트를 전송하여 앱이 현재 UI_HIDDEN 단계에 있음을 시뮬레이션하는 데 도움이 되는 ADB 명령어도 있습니다.
adb shell dumpsys meminfo <pkg> # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDENAndroid 스튜디오를 사용하고 있다면 좋습니다.
하지만 이 친구는 언제나 여러분의 좋은 친구입니다.
Android 스튜디오 프로파일러를 사용하여 힙 덤프를 캡처하고 UI를 통해 실제 메모리 사용량과 메모리 할당을 확인할 수 있습니다.
물론 Android 스튜디오의 Gemini를 사용한 AI 또는 기타 사용 가능한 Android 기술에 대해서도 이야기합니다.
힙 덤프를 분석하고 Perfetto SQL 트레이스를 사용하여 비트맵을 찾고 일반적으로 캐시 소유자를 파악하는 데 도움이 됩니다.
Glide로 로드하는 이미지인가요, 아니면 Coil로 로드하는 이미지인가요?
애플리케이션에 다른 메모리 누수가 있나요?
"저는 더 이상 IDE를 사용하지 않아요.
저는 프롬프트 엔지니어입니다.
AI 에이전트에게 프롬프트를 보내 코드를 작성합니다."
네, 알고 있습니다.
여가 시간에는 바이브 코딩도 합니다.
따라서 샘플로 공유해 드립니다.
네, 이건 제가 500개 앱을 조사할 때 사용하는 프롬프트의 일부예요.
네, 일부만요.
화면이 크지 않다는 것을 알고 있습니다.
더 많이 넣으면 잘 보이지 않을 거예요.
하지만 AI를 사용하여 힙 덤프를 포함한 메모리 사용량을 캡처하고 분석하고 결과를 제공하는 방법을 자동화하는 방법을 알 수 있습니다.
그리고 한 문장 더 추가해서 '내 코드에 대한 코드 수정도 제공해 줘'라고 말할 수 있습니다.
이로 인해 두려움이 줄어들기를 바랍니다. "메모리는 큰 영역입니다.
대학에서 컴퓨터 공학을 전공했지만 운영체제나 컴퓨터 시스템 아키텍처는 공부하지 않았습니다.
하지만 실제로는 그렇게 무섭지 않고 그렇게 어렵지 않습니다."
예, 앱을 쉽게 분석할 수 있습니다.
사용해 보세요.
하지만 물론 AI는 실수를 저지를 수 있으므로 메모리 사용량과 OS의 메모리 할당 방식에 관한 기본 지식은 여전히 중요합니다.
제 강연을 듣고 YouTube에서 동영상을 시청하세요.
풍람
요약하자면 몇 가지 핵심 사항이 있습니다.
첫째, 줄이기입니다.
이미지 크기가 적절한지 확인합니다.
이미지를 더 작은 이미지로 분할하여 모든 것을 메모리에 로드하지 않고 사용자에게 표시되는 이미지만 메모리에 로드합니다.
가능하면 하드웨어 bitmapConfig 또는 RGB_565를 사용합니다.
재사용: 캐시 전략을 마련하고 이미지 로더를 사용하여 메모리의 중복 비트맵 수를 줄입니다.
물론 이미지 로더의 기본 캐시 크기를 덮어쓰려고 하더라도 여러 비트맵 이미지를 사용하지 마세요.
재사용, 재활용하지 않습니다.
네
앱의 메모리 관리는 앱 상태에 맞게 조정되어야 합니다.
UI가 숨겨져 있으면 메모리를 적게 사용해야 합니다.
OS에서 메모리 해제를 요청하는 경우 최대한 많이 해제합니다.
비트맵, 전원, 컴퓨팅 리소스 등 리소스 소모를 중지합니다.
예, 배너 타이머를 중지하고 애니메이션을 중지하고 이미지 풀링을 중지합니다.
마지막으로 모니터링을 해야 합니다.
Google Play Console을 정기적으로 확인합니다.
이것이 연습인지 확인하세요.
이를 통해 전체 사용자 모집단에서 내 앱의 메모리 사용량의 상위 90번째 백분위수의 현재 상태를 파악할 수 있습니다.
이렇게 하면 문제를 조기에 파악하는 데도 도움이 됩니다.
네
로컬에서 프로파일링하려면 Android 스튜디오 메모리 프로파일러를 사용하면 됩니다.
ADB 명령어도 있습니다.
Android 기술도 있습니다.
하지만 실제 사용자의 통계를 확인하려면 Google Play Console을 사용해야 합니다.
또한 Play Console에 로그인하지 않고 API를 사용하여 데이터를 가져올 수 있는 Google Play Reporting API도 있습니다.
언제나 그렇듯이 자세한 내용은 developer.android.com/memory를 참고하세요.
이 기능을 사용해 줄이고, 재사용하고, 재활용하세요.
우리가 직면한 문제를 극복하기 위해 더 나은 Android 생태계를 함께 구축할 수 있기를 바랍니다.
감사합니다.
곧 외부에서 오피스 아워를 진행할 예정이니 궁금한 점이 있으면 언제든지 오세요.
다음 발표자까지 시간이 좀 있으니 다음 발표자가 무대에 오를 때까지 기다려 주세요. 감사합니다.
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(sizedUrl)
.size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
.precision(Precision.EXACT) // Downsample to exact card size
.bitmapConfig(Bitmap.Config.HARDWARE)
.build(),
contentDescription = product.title
)// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(activity).bounds
// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth = minOf(requestedWidthPx, bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())
val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
model = ImageRequest.Builder(context)
.data(productUrl)
.placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
.memoryCacheKey(canonicalKey)
.diskCacheKey(productUrl.substringBefore("?"))
.build(),
contentDescription = product.title
)val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
MemoryCache.Builder()
.maxSizeBytes(simpleSplitBudget)
.build()override fun onStop() {
super.onStop()
// 1. Stop animations so they don't decode next frames in background!
bannerAnimationView.cancelAnimation()
animatedImageDrawable?.stop()
// 2. Cancel next-image buffering / prefetch queues!
nextImagesBufferingJob?.cancel()
Glide.with(this).pauseRequests()
}
override fun onDestroyView() {
super.onDestroyView()
bannerAnimationView.clearAnimation() // Release resources!
}adb shell dumpsys meminfo <pkg> # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN