Development/Error

[Error] Can't compress a recycled bitmap

은스타 2022. 10. 18. 11:12
반응형

안드로이드 'Can't compress a recycled bitmap' 오류 원인과 해결방법 총정리

안녕하세요.
이번 포스팅은 Android 개발하면서 발생한 'Can't compress a recycled bitmap' 에 관하여 오류의 원인과 해결 방법 및 자주 묻는 질문에 대하여 정리해보도록 하겠습니다.


목차

 

#1. 오류의 정체: 'Can't compress a recycled bitmap'이란?

안드로이드 개발을 하다 보면 자주 마주치게 되는 오류 중 하나가 바로 'Can't compress a recycled bitmap' 메시지입니다. 이 오류는 말 그대로 '이미 재활용(recycle)된 비트맵을 압축할 수 없다'는 의미로, 비트맵 객체가 이미 recycle() 메서드에 의해 메모리에서 해제된 상태에서 해당 객체에 접근하려 할 때 발생합니다.

일반적으로 이 오류는 다음과 같은 형태로 로그캣(Logcat)에 표시됩니다:

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)
    ...

이 오류는 앱 크래시(crash)를 유발하므로, 반드시 해결해야 하는 중요한 문제입니다.

 

#2. 오류 발생 원인 분석

'Can't compress a recycled bitmap' 오류가 발생하는 주요 원인들을 살펴보겠습니다:

1. 비트맵 조기 재활용

가장 흔한 원인은 비트맵 객체를 너무 일찍 재활용하는 경우입니다. 예를 들어:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
// 비트맵 작업 수행
bitmap.recycle(); // 메모리 해제

// 이후에 비트맵 사용 시도 - 오류 발생!
saveBitmap(bitmap); // Can't compress a recycled bitmap 오류 발생

2. 비동기 작업에서의 타이밍 문제

비동기 작업에서 비트맵을 사용할 때 발생하는 문제도 흔합니다:

final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);

new Thread(new Runnable() {
    @Override
    public void run() {
        // 다른 스레드에서 시간이 걸리는 작업 수행
        processLargeImage(bitmap);
    }
}).start();

// 메인 스레드에서는 이미 비트맵 사용 완료로 판단하고 재활용
bitmap.recycle(); // 다른 스레드에서 아직 작업 중일 수 있음!

3. 공유 비트맵 객체의 잘못된 관리

여러 곳에서 동일한 비트맵 객체를 참조하는 경우 잘못된 관리로 인해 문제가 발생할 수 있습니다:

// 클래스 A에서
Bitmap sharedBitmap = loadBitmap();
passToClassB(sharedBitmap);
sharedBitmap.recycle(); // 클래스 B에서도 사용 중일 수 있음

// 클래스 B에서
void useBitmap(Bitmap bitmap) {
    // 이미 재활용된 비트맵에 접근하려고 시도
}

 

#3. 해결방법 1: Bitmap 재활용 관리하기

비트맵 재활용을 올바르게 관리하는 방법을 알아보겠습니다:

사용 완료 확인 후 재활용하기

비트맵 사용이 완전히 끝난 후에만 재활용하세요:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
// 비트맵 작업 수행
saveBitmap(bitmap); // 먼저 저장 작업 완료

// 모든 작업이 끝난 후 재활용
if (bitmap != null && !bitmap.isRecycled()) {
    bitmap.recycle();
    bitmap = null; // GC를 위해 참조 제거
}

재활용 전 상태 확인하기

항상 비트맵을 사용하기 전에 재활용 상태를 확인하는 습관을 들이세요:

void processBitmap(Bitmap bitmap) {
    if (bitmap != null && !bitmap.isRecycled()) {
        // 안전하게 비트맵 사용
    } else {
        Log.e(TAG, "Bitmap is recycled or null!");
        // 적절한 오류 처리
    }
}

 

#4. 해결방법 2: 비트맵 복제 활용하기

공유 비트맵 객체를 사용할 때는 복제본 활용을 고려하세요:

// 원본 비트맵
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);

// 복제본 생성 (비트맵이 변경 가능할 때만 사용)
Bitmap copiedBitmap = originalBitmap.copy(originalBitmap.getConfig(), true);

// 각각 다른 목적으로 사용
processImageA(originalBitmap);
processImageB(copiedBitmap);

// 각각 별도로 재활용
if (originalBitmap != null && !originalBitmap.isRecycled()) {
    originalBitmap.recycle();
}

// 복제본은 별도로 관리되므로 원본 재활용에 영향 받지 않음

 

#5. 해결방법 3: 비트맵 리소스 관리 최적화

메모리 관리를 위한 효과적인 기법들을 소개합니다:

BitmapOptions을 활용한 효율적 로딩

메모리 사용을 최소화하기 위해 BitmapFactory.Options를 활용하세요:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 원본 크기의 1/2로 축소
options.inJustDecodeBounds = false;

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image, options);
// 이제 비트맵이 더 적은 메모리를 사용

비트맵 풀(Pool) 활용하기

안드로이드 3.0(API 레벨 11) 이상에서는 Bitmap.recycle()의 필요성이 감소했습니다. 대신 비트맵 풀을 활용할 수 있습니다:

// API 레벨 19 이상에서 사용 가능
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true; // 변경 가능한 비트맵으로 설정
options.inBitmap = reuseableBitmap; // 재사용할 비트맵 지정

Bitmap newBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.another_image, options);

 

#6. 해결방법 4: Glide 또는 Picasso 라이브러리 활용

이미지 로딩 라이브러리를 사용하면 메모리 관리 문제를 크게 줄일 수 있습니다:

Glide 사용 예시

Glide는 자동으로 비트맵 리소스를 관리해 줍니다:

// build.gradle에 추가: implementation 'com.github.bumptech.glide:glide:4.12.0'

// 사용 예
Glide.with(context)
    .load(R.drawable.my_image)
    .into(imageView);

Picasso 사용 예시

Picasso도 효과적인 대안입니다:

// build.gradle에 추가: implementation 'com.squareup.picasso:picasso:2.71828'

// 사용 예
Picasso.get()
    .load(R.drawable.my_image)
    .into(imageView);

 

#7. 개발자들이 자주 저지르는 실수

비트맵 사용 시 흔히 발생하는 실수들을 알아봅시다:

1. AsyncTask 완료 전 비트맵 재활용

// 잘못된 방식
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);

new AsyncTask<Bitmap, Void, Void>() {
    @Override
    protected Void doInBackground(Bitmap... bitmaps) {
        processBitmap(bitmaps[0]); // 시간이 오래 걸리는 작업
        return null;
    }
}.execute(bitmap);

bitmap.recycle(); // 오류! AsyncTask가 아직 실행 중일 수 있음

2. 캐시된 비트맵에 무분별한 recycle() 호출

// 잘못된 방식
public class ImageCache {
    private static Map<String, Bitmap> cache = new HashMap<>();

    public static Bitmap get(String key) {
        return cache.get(key);
    }

    public static void add(String key, Bitmap bitmap) {
        cache.put(key, bitmap);
    }

    public static void clearOne(String key) {
        Bitmap bitmap = cache.get(key);
        if (bitmap != null) {
            bitmap.recycle(); // 위험! 다른 곳에서 참조 중일 수 있음
            cache.remove(key);
        }
    }
}

 

#8. 안드로이드 버전별 대응 방법

안드로이드 버전에 따라 비트맵 처리 방식이 다릅니다:

Android 2.3.3 (API 10) 이하

이전 버전에서는 직접적인 메모리 관리가 필요했습니다:

bitmap.recycle(); // 메모리 제약이 심한 기기에서 필요

Android 3.0 (API 11) 이상

비트맵이 네이티브 힙 대신 자바 힙에 저장되면서 GC로 자동 관리됩니다:

// recycle() 호출 필요성 감소
// 단, 매우 큰 비트맵의 경우 recycle() 호출 고려

Android 8.0 (API 26) 이상

하드웨어 가속 비트맵이 도입되어 메모리 효율성이 더욱 향상되었습니다:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.HARDWARE; // 메모리 효율적
Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.my_image, options);

 

#9. 마치며: 메모리 관리의 중요성

비트맵은 안드로이드 앱에서 가장 메모리를 많이 사용하는 요소 중 하나입니다. 'Can't compress a recycled bitmap' 오류를 방지하는 것은 단순히 오류 해결을 넘어 효율적인 메모리 관리의 시작점이 됩니다.

모범 사례:

  1. 필요한 크기로만 비트맵 로드하기
  2. 사용하지 않는 비트맵 참조 제거하기
  3. 명시적 재활용은 신중하게 사용하기
  4. 가능하면 이미지 로딩 라이브러리 활용하기
  5. 비트맵 상태를 항상 확인한 후 작업하기

이러한 원칙을 따르면 'Can't compress a recycled bitmap' 오류뿐만 아니라 OOM(Out of Memory) 오류도 함께 예방할 수 있습니다.

 

#10. 자주 묻는 질문 (FAQ)

Q: 비트맵 재활용은 항상 필요한가요?

A: 안드로이드 3.0(API 11) 이상에서는 GC가 비트맵 메모리를 자동으로 관리하므로 recycle() 호출의 필요성이 줄어들었습니다. 하지만 매우 큰 비트맵을 다루거나 메모리가 제한된 기기에서는 여전히 유용할 수 있습니다.

Q: 이미 재활용된 비트맵인지 어떻게 확인할 수 있나요?

A: Bitmap 클래스의 isRecycled() 메서드를 사용하여 확인할 수 있습니다. 이 메서드는 비트맵이 재활용되었으면 true를 반환합니다.

if (bitmap != null && !bitmap.isRecycled()) {
    // 안전하게 비트맵 사용
}

Q: 비트맵 메모리 사용량을 줄이는 가장 좋은 방법은 무엇인가요?

A: 필요한 크기로만 비트맵을 로드하는 것이 가장 효과적입니다. BitmapFactory.Options의 inSampleSize 옵션을 사용하여 원본보다 작은 크기로 로드하거나, 최신 안드로이드 버전에서는 하드웨어 가속 비트맵(Bitmap.Config.HARDWARE)을 활용할 수 있습니다.

Q: Glide나 Picasso 같은 라이브러리를 사용하면 비트맵 재활용을 신경 쓰지 않아도 되나요?

A: 네, 이러한 라이브러리들은 내부적으로 효율적인 비트맵 풀링과 캐싱 메커니즘을 구현하고 있어 메모리 관리를 자동화합니다. 대부분의 경우 직접 recycle()을 호출할 필요가 없습니다.

Q: LruCache를 사용할 때도 비트맵 recycle()을 호출해야 하나요?

A: LruCache에서 제거되는 비트맵에 대해 recycle()을 호출하는 것은 위험할 수 있습니다. 캐시에서 제거된 비트맵이 다른 곳에서 여전히 참조되고 있을 수 있기 때문입니다. 대신 참조 카운팅을 구현하거나, 약한 참조(WeakReference)를 사용하는 것이 더 안전합니다.


마무리

이 글이 'Can't compress a recycled bitmap' 오류를 이해하고 해결하는 데 도움이 되었기를 바랍니다. 비트맵 처리는 안드로이드 앱 개발에서 중요한 부분이며, 올바른 메모리 관리는 안정적이고 효율적인 앱을 만드는 핵심 요소입니다.

긴 글 읽어주셔서 감사합니다.

끝.

반응형