반응형
안드로이드 'Can't compress a recycled bitmap' 오류 해결 방법
안드로이드 개발을 하다 보면 자주 마주치게 되는 오류 중 하나가 바로 'Can't compress a recycled bitmap' 메시지입니다. 이 오류는 이미 재활용된 비트맵을 압축하려 할 때 발생하는 IllegalStateException으로, 앱 크래시를 유발하는 심각한 문제입니다. 비트맵 객체가 recycle() 메서드에 의해 메모리에서 해제된 상태에서 해당 객체에 접근하려 할 때 발생하며, 특히 비동기 작업이나 공유 비트맵 객체를 다룰 때 흔히 나타납니다. 이 글에서는 오류의 원인부터 실전 해결방법, 안드로이드 버전별 대응 전략, 메모리 최적화 기법까지 빠짐없이 다루고 있습니다. 실무에서 바로 적용할 수 있는 코드 예제와 함께 Glide, Picasso 같은 이미지 로딩 라이브러리 활용법도 소개하니 끝까지 읽어보시기 바랍니다.
목차
1. 오류의 정체와 발생 메커니즘
2. 오류 발생 원인 완벽 분석
3. 비트맵 재활용 관리 해결방법
4. 메모리 최적화 및 라이브러리 활용
5. 자주 묻는 질문 (FAQ)
#1. 오류의 정체와 발생 메커니즘
'Can't compress a recycled bitmap' 오류는 안드로이드 앱 개발 시 비트맵 처리 과정에서 발생하는 대표적인 메모리 관리 오류입니다. 이 오류 메시지는 말 그대로 이미 재활용(recycle)된 비트맵을 압축할 수 없다는 의미로, 비트맵 메모리 해제 후 접근 시도 시 발생합니다.
1) 오류의 기본 정의
이 오류는 IllegalStateException 유형에 속하며, 일반적으로 다음과 같은 형태로 로그캣에 표시됩니다.
java.lang.IllegalStateException: Can't compress a recycled bitmap
at android.graphics.Bitmap.compress(Bitmap.java:1546)
at com.example.myapp.ImageProcessor.saveImage(ImageProcessor.java:125)
at com.example.myapp.MainActivity.onSaveClicked(MainActivity.java:89)
...
at android.graphics.Bitmap.compress(Bitmap.java:1546)
at com.example.myapp.ImageProcessor.saveImage(ImageProcessor.java:125)
at com.example.myapp.MainActivity.onSaveClicked(MainActivity.java:89)
...
이 오류는 앱 크래시를 유발하므로 반드시 해결해야 하는 중요한 문제입니다. 특히 이미지를 다루는 기능이 많은 앱에서는 사용자 경험을 크게 저해할 수 있습니다.
. . . . .
2) 비트맵 재활용의 개념
안드로이드에서 비트맵은 메모리를 많이 차지하는 객체입니다. recycle() 메서드는 비트맵이 차지하던 메모리를 즉시 해제하여 다른 용도로 사용할 수 있도록 합니다.
(1) recycle() 메서드의 역할
recycle() 메서드를 호출하면 비트맵의 픽셀 데이터가 저장된 네이티브 메모리가 해제됩니다. 이후 해당 비트맵 객체에 대한 어떠한 작업도 수행할 수 없게 됩니다.
① 비트맵이 차지하던 메모리 즉시 반환
② 가비지 컬렉터 대기 시간 단축
③ OutOfMemory 오류 예방 효과
④ 재활용 후 비트맵 사용 불가
(2) 오류 발생 시점
재활용된 비트맵에 대해 다음과 같은 작업을 시도할 때 오류가 발생합니다.
① compress() - 이미지 압축 시도
② getPixel() - 픽셀 정보 읽기 시도
③ setPixel() - 픽셀 정보 수정 시도
④ copy() - 비트맵 복제 시도
#2. 오류 발생 원인 완벽 분석
'Can't compress a recycled bitmap' 오류가 발생하는 주요 원인들을 실제 코드 예제와 함께 상세히 살펴보겠습니다. 각 원인별로 문제 상황과 발생 메커니즘을 정확히 이해하는 것이 올바른 해결의 첫 걸음입니다.
1) 비트맵 조기 재활용 문제
가장 흔한 원인은 비트맵 객체를 너무 일찍 재활용하는 경우입니다. 비트맵을 아직 사용해야 하는데 먼저 재활용해버리면 이후 접근 시 오류가 발생합니다.
(1) 문제가 되는 코드 패턴
// 비트맵 로드
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
// 일부 작업 수행
imageView.setImageBitmap(bitmap);
// 너무 일찍 재활용
bitmap.recycle();
// 이후에 비트맵 사용 시도 - 오류 발생!
saveBitmapToFile(bitmap); // Can't compress a recycled bitmap 오류
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
// 일부 작업 수행
imageView.setImageBitmap(bitmap);
// 너무 일찍 재활용
bitmap.recycle();
// 이후에 비트맵 사용 시도 - 오류 발생!
saveBitmapToFile(bitmap); // Can't compress a recycled bitmap 오류
(2) 발생 메커니즘
위 코드에서 비트맵을 ImageView에 설정한 후 즉시 재활용했습니다. 이후 파일로 저장하려고 시도할 때 이미 재활용된 상태이므로 오류가 발생합니다.
. . . . .
2) 비동기 작업에서의 타이밍 문제
비동기 작업에서 비트맵을 사용할 때 발생하는 문제도 매우 흔합니다. 여러 스레드에서 동일한 비트맵을 참조할 때 타이밍 이슈가 발생할 수 있습니다.
(1) AsyncTask 사용 시 문제
// 메인 스레드에서 비트맵 로드
final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
// 백그라운드 작업 시작
new Thread(new Runnable() {
@Override
public void run() {
// 시간이 오래 걸리는 이미지 처리 작업
processLargeImage(bitmap);
}
}).start();
// 메인 스레드에서는 작업 완료로 판단하고 재활용
bitmap.recycle(); // 다른 스레드에서 아직 작업 중일 수 있음!
final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
// 백그라운드 작업 시작
new Thread(new Runnable() {
@Override
public void run() {
// 시간이 오래 걸리는 이미지 처리 작업
processLargeImage(bitmap);
}
}).start();
// 메인 스레드에서는 작업 완료로 판단하고 재활용
bitmap.recycle(); // 다른 스레드에서 아직 작업 중일 수 있음!
(2) 동시성 문제
백그라운드 스레드에서 비트맵을 처리하는 동안 메인 스레드에서 재활용하면, 백그라운드 작업 중 예기치 않은 크래시가 발생합니다.
. . . . .
3) 공유 비트맵 객체의 잘못된 관리
여러 곳에서 동일한 비트맵 객체를 참조하는 경우, 한 곳에서 재활용하면 다른 곳에서 접근할 때 문제가 발생합니다.
(1) 잘못된 공유 패턴
// 클래스 A에서
public class ImageProcessorA {
public void process() {
Bitmap sharedBitmap = loadBitmap();
passToClassB(sharedBitmap);
// 클래스 B에서도 사용 중일 수 있는데 재활용
sharedBitmap.recycle();
}
}
// 클래스 B에서
public class ImageProcessorB {
void useBitmap(Bitmap bitmap) {
// 이미 재활용된 비트맵에 접근하려고 시도
bitmap.compress(CompressFormat.JPEG, 90, stream); // 오류 발생!
}
}
public class ImageProcessorA {
public void process() {
Bitmap sharedBitmap = loadBitmap();
passToClassB(sharedBitmap);
// 클래스 B에서도 사용 중일 수 있는데 재활용
sharedBitmap.recycle();
}
}
// 클래스 B에서
public class ImageProcessorB {
void useBitmap(Bitmap bitmap) {
// 이미 재활용된 비트맵에 접근하려고 시도
bitmap.compress(CompressFormat.JPEG, 90, stream); // 오류 발생!
}
}
(2) 참조 카운팅 부재
여러 컴포넌트에서 동일한 비트맵을 참조할 때, 참조 카운팅 메커니즘 없이 관리하면 어느 한 곳에서 재활용한 후 다른 곳에서 사용하려 할 때 오류가 발생합니다.
. . . . .
4) 캐시 관리 실수
LruCache나 커스텀 캐시를 사용할 때 잘못된 메모리 관리로 인해 오류가 발생할 수 있습니다.
(1) 캐시 제거 시 문제
// 잘못된 캐시 관리 방식
public class ImageCache {
private static Map<String, Bitmap> cache = new HashMap<>();
public static void clearOne(String key) {
Bitmap bitmap = cache.get(key);
if (bitmap != null) {
bitmap.recycle(); // 위험! 다른 곳에서 참조 중일 수 있음
cache.remove(key);
}
}
}
public class ImageCache {
private static Map<String, Bitmap> cache = new HashMap<>();
public static void clearOne(String key) {
Bitmap bitmap = cache.get(key);
if (bitmap != null) {
bitmap.recycle(); // 위험! 다른 곳에서 참조 중일 수 있음
cache.remove(key);
}
}
}
캐시에서 제거된 비트맵이 다른 곳에서 여전히 참조되고 있을 수 있으므로, 무분별한 recycle() 호출은 매우 위험합니다.
#3. 비트맵 재활용 관리 해결방법
비트맵 재활용을 올바르게 관리하는 구체적인 방법들을 실전 코드와 함께 알아보겠습니다. 각 해결방법은 실무에서 바로 적용할 수 있도록 상세한 예제를 포함하고 있습니다.
1) 사용 완료 확인 후 재활용하기
가장 기본적이면서도 중요한 원칙은 비트맵 사용이 완전히 끝난 후에만 재활용하는 것입니다.
(1) 올바른 재활용 패턴
// 비트맵 로드
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
try {
// 모든 작업 수행
imageView.setImageBitmap(bitmap);
saveBitmapToFile(bitmap);
uploadBitmapToServer(bitmap);
} finally {
// 모든 작업이 끝난 후 재활용
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null; // GC를 위해 참조 제거
}
}
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
try {
// 모든 작업 수행
imageView.setImageBitmap(bitmap);
saveBitmapToFile(bitmap);
uploadBitmapToServer(bitmap);
} finally {
// 모든 작업이 끝난 후 재활용
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null; // GC를 위해 참조 제거
}
}
(2) 재활용 전 상태 확인
isRecycled() 메서드를 활용하여 항상 비트맵을 사용하기 전에 재활용 상태를 확인하는 습관을 들이세요.
void processBitmap(Bitmap bitmap) {
// 안전성 체크
if (bitmap == null || bitmap.isRecycled()) {
Log.e(TAG, "Bitmap is recycled or null!");
return;
}
// 안전하게 비트맵 사용
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
}
// 안전성 체크
if (bitmap == null || bitmap.isRecycled()) {
Log.e(TAG, "Bitmap is recycled or null!");
return;
}
// 안전하게 비트맵 사용
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
}
. . . . .
2) 비트맵 복제 활용하기
공유 비트맵 객체를 사용할 때는 복제본을 생성하여 각 컴포넌트가 독립적으로 관리할 수 있도록 합니다.
(1) 복제본 생성 방법
// 원본 비트맵 로드
Bitmap originalBitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.my_image
);
// 복제본 생성 (변경 가능한 비트맵으로)
Bitmap copiedBitmap = originalBitmap.copy(
originalBitmap.getConfig(),
true // mutable = true
);
// 각각 다른 목적으로 사용
processImageA(originalBitmap);
processImageB(copiedBitmap);
// 각각 별도로 재활용
if (originalBitmap != null && !originalBitmap.isRecycled()) {
originalBitmap.recycle();
}
if (copiedBitmap != null && !copiedBitmap.isRecycled()) {
copiedBitmap.recycle();
}
Bitmap originalBitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.my_image
);
// 복제본 생성 (변경 가능한 비트맵으로)
Bitmap copiedBitmap = originalBitmap.copy(
originalBitmap.getConfig(),
true // mutable = true
);
// 각각 다른 목적으로 사용
processImageA(originalBitmap);
processImageB(copiedBitmap);
// 각각 별도로 재활용
if (originalBitmap != null && !originalBitmap.isRecycled()) {
originalBitmap.recycle();
}
if (copiedBitmap != null && !copiedBitmap.isRecycled()) {
copiedBitmap.recycle();
}
(2) 복제 시 주의사항
복제는 메모리를 추가로 사용하므로, 필요한 경우에만 사용하고 원본과 복제본을 모두 적절히 관리해야 합니다.
. . . . .
3) 비동기 작업 안전하게 처리하기
비동기 작업에서 비트맵을 사용할 때는 작업 완료 시점을 명확히 파악하고 재활용해야 합니다.
(1) AsyncTask 안전한 패턴
new AsyncTask<Void, Void, Void>() {
private Bitmap bitmap;
@Override
protected void onPreExecute() {
// 메인 스레드에서 비트맵 로드
bitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.my_image
);
}
@Override
protected Void doInBackground(Void... voids) {
// 백그라운드에서 안전하게 사용
if (bitmap != null && !bitmap.isRecycled()) {
processImage(bitmap);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
// 작업 완료 후 재활용
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
}
}.execute();
private Bitmap bitmap;
@Override
protected void onPreExecute() {
// 메인 스레드에서 비트맵 로드
bitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.my_image
);
}
@Override
protected Void doInBackground(Void... voids) {
// 백그라운드에서 안전하게 사용
if (bitmap != null && !bitmap.isRecycled()) {
processImage(bitmap);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
// 작업 완료 후 재활용
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
}
}.execute();
(2) Coroutine 사용 시 패턴
// Kotlin Coroutine 사용 예제
lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) {
BitmapFactory.decodeResource(resources, R.drawable.my_image)
}
try {
// 메인 스레드에서 UI 업데이트
imageView.setImageBitmap(bitmap)
withContext(Dispatchers.IO) {
// IO 작업
saveBitmapToFile(bitmap)
}
} finally {
// 작업 완료 후 재활용
bitmap?.let {
if (!it.isRecycled) {
it.recycle()
}
}
}
}
lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) {
BitmapFactory.decodeResource(resources, R.drawable.my_image)
}
try {
// 메인 스레드에서 UI 업데이트
imageView.setImageBitmap(bitmap)
withContext(Dispatchers.IO) {
// IO 작업
saveBitmapToFile(bitmap)
}
} finally {
// 작업 완료 후 재활용
bitmap?.let {
if (!it.isRecycled) {
it.recycle()
}
}
}
}
#4. 메모리 최적화 및 라이브러리 활용
비트맵 메모리 관리를 효율적으로 수행하는 고급 기법과 검증된 이미지 로딩 라이브러리 활용 방법을 알아보겠습니다. 이 방법들을 사용하면 메모리 관련 오류를 근본적으로 예방할 수 있습니다.
1) BitmapOptions 활용한 효율적 로딩
메모리 사용을 최소화하기 위해 BitmapFactory.Options를 활용하여 필요한 크기로만 비트맵을 로드할 수 있습니다.
(1) inSampleSize로 크기 조절
// 먼저 이미지 크기만 확인
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.my_image, options);
// 원본 크기 확인
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
// 필요한 크기 계산
int targetWidth = 800;
int targetHeight = 600;
int inSampleSize = calculateInSampleSize(options, targetWidth, targetHeight);
// 실제 로드
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image, options);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.my_image, options);
// 원본 크기 확인
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
// 필요한 크기 계산
int targetWidth = 800;
int targetHeight = 600;
int inSampleSize = calculateInSampleSize(options, targetWidth, targetHeight);
// 실제 로드
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image, options);
(2) inSampleSize 계산 메서드
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// inSampleSize는 2의 제곱수로 설정
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// inSampleSize는 2의 제곱수로 설정
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
. . . . .
2) 안드로이드 버전별 대응 방법
안드로이드 버전에 따라 비트맵 처리 방식이 다르므로, 버전별 최적화 전략이 필요합니다.
(1) 버전별 메모리 관리 특징
| 안드로이드 버전 | API 레벨 | 메모리 관리 특징 |
|---|---|---|
| Android 2.3.3 이하 | API 10 이하 | 비트맵이 네이티브 힙에 저장, recycle() 필수 |
| Android 3.0 ~ 7.1 | API 11 ~ 25 | 자바 힙에 저장, GC 자동 관리 가능 |
| Android 8.0 이상 | API 26 이상 | 하드웨어 가속 비트맵 지원, 메모리 효율 향상 |
(2) Android 8.0 이상 최적화
// Android 8.0 이상에서 하드웨어 가속 비트맵 사용
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.HARDWARE;
Bitmap bitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.my_image, options
);
// 하드웨어 비트맵은 recycle() 불필요
} else {
// 이전 버전용 처리
Bitmap bitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.my_image
);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.HARDWARE;
Bitmap bitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.my_image, options
);
// 하드웨어 비트맵은 recycle() 불필요
} else {
// 이전 버전용 처리
Bitmap bitmap = BitmapFactory.decodeResource(
getResources(), R.drawable.my_image
);
}
. . . . .
3) Glide 라이브러리 활용
Glide는 자동으로 비트맵 리소스를 관리해주는 강력한 이미지 로딩 라이브러리입니다. 메모리 캐시, 디스크 캐시, 비트맵 풀링을 자동으로 처리합니다.
(1) Glide 기본 사용법
// build.gradle에 추가
// implementation 'com.github.bumptech.glide:glide:4.16.0'
// annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
// 기본 사용
Glide.with(context)
.load(R.drawable.my_image)
.into(imageView);
// URL에서 로드
Glide.with(context)
.load("https://example.com/image.jpg")
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.into(imageView);
// implementation 'com.github.bumptech.glide:glide:4.16.0'
// annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
// 기본 사용
Glide.with(context)
.load(R.drawable.my_image)
.into(imageView);
// URL에서 로드
Glide.with(context)
.load("https://example.com/image.jpg")
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.into(imageView);
(2) Glide 고급 옵션
Glide.with(context)
.load(imageUrl)
.override(800, 600) // 크기 조절
.centerCrop() // 중앙 크롭
.diskCacheStrategy(DiskCacheStrategy.ALL) // 디스크 캐시
.skipMemoryCache(false) // 메모리 캐시 사용
.thumbnail(0.1f) // 썸네일 먼저 표시
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> target,
boolean isFirstResource) {
Log.e(TAG, "Image load failed", e);
return false;
}
@Override
public boolean onResourceReady(Drawable resource,
Object model, Target<Drawable> target,
DataSource dataSource, boolean isFirstResource) {
return false;
}
})
.into(imageView);
.load(imageUrl)
.override(800, 600) // 크기 조절
.centerCrop() // 중앙 크롭
.diskCacheStrategy(DiskCacheStrategy.ALL) // 디스크 캐시
.skipMemoryCache(false) // 메모리 캐시 사용
.thumbnail(0.1f) // 썸네일 먼저 표시
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> target,
boolean isFirstResource) {
Log.e(TAG, "Image load failed", e);
return false;
}
@Override
public boolean onResourceReady(Drawable resource,
Object model, Target<Drawable> target,
DataSource dataSource, boolean isFirstResource) {
return false;
}
})
.into(imageView);
. . . . .
4) Picasso 라이브러리 활용
Picasso는 Square에서 개발한 또 다른 인기 있는 이미지 로딩 라이브러리로, 간결한 API와 자동 메모리 관리를 제공합니다.
(1) Picasso 기본 사용법
// build.gradle에 추가
// implementation 'com.squareup.picasso:picasso:2.8'
// 기본 사용
Picasso.get()
.load(R.drawable.my_image)
.into(imageView);
// 변환 적용
Picasso.get()
.load(imageUrl)
.resize(800, 600)
.centerCrop()
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.into(imageView);
// implementation 'com.squareup.picasso:picasso:2.8'
// 기본 사용
Picasso.get()
.load(R.drawable.my_image)
.into(imageView);
// 변환 적용
Picasso.get()
.load(imageUrl)
.resize(800, 600)
.centerCrop()
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.into(imageView);
(2) 메모리 캐시 전략
효율적인 메모리 캐시 관리를 위해 LruCache를 활용할 수 있습니다. 다만 recycle() 호출에는 주의가 필요합니다.
public class BitmapLruCache extends LruCache<String, Bitmap> {
public BitmapLruCache(int maxSize) {
super(maxSize);
}
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 비트맵 크기를 KB 단위로 반환
return bitmap.getByteCount() / 1024;
}
@Override
protected void entryRemoved(boolean evicted, String key,
Bitmap oldValue, Bitmap newValue) {
// 주의: 다른 곳에서 참조 중일 수 있으므로
// recycle() 호출하지 않음
// GC가 자동으로 처리하도록 함
}
}
// 캐시 크기 계산 (최대 메모리의 1/8 사용)
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
BitmapLruCache memoryCache = new BitmapLruCache(cacheSize);
public BitmapLruCache(int maxSize) {
super(maxSize);
}
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 비트맵 크기를 KB 단위로 반환
return bitmap.getByteCount() / 1024;
}
@Override
protected void entryRemoved(boolean evicted, String key,
Bitmap oldValue, Bitmap newValue) {
// 주의: 다른 곳에서 참조 중일 수 있으므로
// recycle() 호출하지 않음
// GC가 자동으로 처리하도록 함
}
}
// 캐시 크기 계산 (최대 메모리의 1/8 사용)
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
BitmapLruCache memoryCache = new BitmapLruCache(cacheSize);
#5. 자주 묻는 질문 (FAQ)
1) Q: 비트맵 재활용은 항상 필요한가요?
A: 안드로이드 3.0(API 11) 이상에서는 GC가 비트맵 메모리를 자동으로 관리하므로 recycle() 호출의 필요성이 크게 줄어들었습니다. 하지만 다음과 같은 경우에는 여전히 유용할 수 있습니다.
① 매우 큰 비트맵을 다루는 경우
② 메모리가 제한된 기기를 지원하는 경우
③ API 10 이하를 지원하는 경우
④ 짧은 시간에 많은 비트맵을 처리하는 경우
Android 8.0(API 26) 이상에서는 하드웨어 가속 비트맵을 사용하면 메모리 효율이 더욱 향상되므로, 최신 버전을 타겟팅하는 앱이라면 recycle() 호출을 최소화하는 것이 권장됩니다.
. . . . .
2) Q: 이미 재활용된 비트맵인지 어떻게 확인할 수 있나요?
A: Bitmap 클래스의 isRecycled() 메서드를 사용하여 확인할 수 있습니다. 이 메서드는 비트맵이 재활용되었으면 true를 반환합니다.
if (bitmap != null && !bitmap.isRecycled()) {
// 안전하게 비트맵 사용
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
} else {
Log.e(TAG, "Bitmap is null or recycled");
}
// 안전하게 비트맵 사용
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
} else {
Log.e(TAG, "Bitmap is null or recycled");
}
비트맵을 사용하는 모든 메서드에서 이러한 체크를 수행하면 오류를 사전에 방지할 수 있습니다.
. . . . .
3) Q: 비트맵 메모리 사용량을 줄이는 가장 좋은 방법은 무엇인가요?
A: 비트맵 메모리 사용량을 줄이는 여러 방법이 있습니다.
① inSampleSize 사용: BitmapFactory.Options의 inSampleSize를 설정하여 필요한 크기로만 로드
② 적절한 Bitmap.Config 선택: ARGB_8888 대신 RGB_565 사용 시 메모리 절반 사용
③ 하드웨어 가속 비트맵: API 26 이상에서 Bitmap.Config.HARDWARE 사용
④ 이미지 로딩 라이브러리: Glide나 Picasso 활용으로 자동 최적화
가장 효과적인 방법은 필요한 크기로만 비트맵을 로드하는 것입니다. 예를 들어 200×200 픽셀 ImageView에 표시할 이미지를 3000×3000 픽셀로 로드하는 것은 메모리 낭비입니다.
. . . . .
4) Q: Glide나 Picasso 같은 라이브러리를 사용하면 비트맵 재활용을 신경 쓰지 않아도 되나요?
A: 네, 이미지 로딩 라이브러리들은 내부적으로 효율적인 메모리 관리를 자동화합니다. Glide와 Picasso는 다음과 같은 기능을 제공합니다.
① 비트맵 풀링(Bitmap Pooling) - 비트맵 재사용으로 메모리 할당 최소화
② 메모리 캐시 - LruCache 기반 자동 관리
③ 디스크 캐시 - 네트워크 요청 최소화
④ 자동 크기 조절 - ImageView 크기에 맞춰 자동 리사이징
⑤ 라이프사이클 관리 - Activity/Fragment 생명주기에 따라 자동 정리
따라서 이러한 라이브러리를 사용하면 직접 recycle()을 호출할 필요가 없으며, 오히려 호출하면 문제가 발생할 수 있습니다.
. . . . .
5) Q: LruCache를 사용할 때도 비트맵 recycle()을 호출해야 하나요?
A: LruCache에서 제거되는 비트맵에 대해 recycle()을 호출하는 것은 위험할 수 있습니다. 캐시에서 제거된 비트맵이 다른 곳에서 여전히 참조되고 있을 수 있기 때문입니다.
더 안전한 방법은 다음과 같습니다.
① 참조 카운팅 구현: 비트맵이 사용되는 횟수를 추적하여 0이 될 때만 재활용
② WeakReference 사용: 캐시에 약한 참조로 저장하여 GC가 자동 처리하도록 함
③ recycle() 호출 안 함: GC에 맡기는 것이 가장 안전 (API 11 이상)
최신 안드로이드 버전에서는 GC가 충분히 효율적이므로, 명시적 재활용보다는 자동 관리에 맡기는 것이 더 안전합니다.
. . . . .
6) Q: 비동기 작업에서 비트맵을 안전하게 사용하려면 어떻게 해야 하나요?
A: 비동기 작업에서 비트맵을 사용할 때는 다음 원칙을 따르세요.
① 작업 완료 시점 명확화: 비동기 작업이 완료된 후에만 재활용
② 스레드 안전성 확보: 여러 스레드에서 동시 접근 시 동기화 처리
③ 생명주기 고려: Activity가 종료되면 작업도 취소
④ 상태 체크: 사용 전 항상 isRecycled() 확인
Kotlin Coroutine을 사용하는 경우 try-finally 블록 또는 use 함수를 활용하여 작업 완료 후 자동으로 정리되도록 할 수 있습니다.
. . . . .
7) Q: OutOfMemoryError와 'Can't compress a recycled bitmap' 오류의 차이는 무엇인가요?
A: 두 오류는 모두 비트맵 메모리 관리와 관련이 있지만 발생 원인과 해결 방법이 다릅니다.
| 구분 | OutOfMemoryError | Can't compress a recycled bitmap |
|---|---|---|
| 발생 원인 | 사용 가능한 메모리 부족 | 재활용된 비트맵 접근 |
| 오류 유형 | Error | IllegalStateException |
| 주요 해결책 | 메모리 사용량 줄이기, 캐시 관리 | 재활용 타이밍 조정, 상태 체크 |
| 예방 방법 | inSampleSize 사용, 적절한 해제 | isRecycled() 체크, 복제 활용 |
OutOfMemoryError는 메모리 용량 자체가 부족할 때 발생하고, 'Can't compress a recycled bitmap'은 이미 해제된 메모리에 접근하려 할 때 발생합니다.
마무리
'Can't compress a recycled bitmap' 오류는 안드로이드 앱 개발에서 자주 발생하는 비트맵 메모리 관리 오류입니다. 이 오류를 해결하는 핵심은 비트맵의 생명주기를 정확히 이해하고 적절한 시점에 재활용하는 것입니다.
이 글에서 다룬 주요 내용을 정리하면 다음과 같습니다.
① 오류의 본질: 재활용된 비트맵에 접근할 때 발생하는 IllegalStateException
② 주요 원인: 조기 재활용, 비동기 타이밍 문제, 공유 객체 관리 실수
③ 기본 해결책: 사용 완료 확인, isRecycled() 체크, 비트맵 복제 활용
④ 고급 기법: BitmapOptions 활용, 버전별 최적화, 라이브러리 사용
⑤ 모범 사례: Glide/Picasso 활용, 자동 메모리 관리 선호
특히 최신 안드로이드 버전(API 11 이상)에서는 명시적인 recycle() 호출보다 GC에 의한 자동 관리가 더 안전합니다. Glide나 Picasso 같은 검증된 이미지 로딩 라이브러리를 사용하면 메모리 관리를 자동화하여 이러한 오류를 근본적으로 예방할 수 있습니다.
비트맵은 안드로이드 앱에서 가장 많은 메모리를 사용하는 리소스 중 하나입니다. 올바른 메모리 관리는 단순히 오류를 방지하는 것을 넘어, 앱의 안정성과 성능을 크게 향상시킵니다. 이 글에서 소개한 기법들을 실무에 적용하여 안정적인 앱을 개발하시기 바랍니다.
긴 글 읽어주셔서 감사합니다.
끝.
끝.
반응형
'Development > Android' 카테고리의 다른 글
| [Android] Android aapt2 process unexpectedly exit 오류 해결 방법 (0) | 2024.07.09 |
|---|---|
| [Android] Android Studio 필수 단축키 70개 - 개발 생산성 2배 높이는 방법 (1) | 2022.10.07 |
| [Android] Android exported 속성 설정 방법과 보안 취약점 해결 (0) | 2022.10.04 |
| [Android] 텍스트 크기에 dp 대신 sp를 사용해야 하는 이유 (0) | 2022.09.30 |
| [Android] Play Asset Delivery / Play Feature Delivery 완벽 가이드 (0) | 2022.09.29 |