5편동안 이어진 Glide의 마지막 글이고, 사실상 이 시리즈를 작성하게 된 계기이다.
BitmapPool 이란 ?
BitmapPool은 Glide에서 Bitmap 객체를 재사용하기 위해 사용하는 메모리 풀이다. 이미지 로더가 이미지를 디코딩하거나 변환할 때 Bitmap 객체를 자주 생성하면, 메모리 소비와 GC(Garbage Collection)로 인해 성능 저하와 OOM(Out of Memory) 문제가 발생할 수 있기 때문에 만들어졌다. Glide는 필요없는 Bitmap을 Pool에 저장하고, 재사용 가능한 Bitmap을 빠르게 반환한다. 이를 통해서 불필요한 객체 생성 및 할당을 줄이고, 비용을 절약해 성능을 높인다. 즉, 속도를 개선할 수 있다.
LruBitmapPool
Glide는 LruBitmapPool을 기본 BitmapPool로 사용한다. 이름 그대로 LRU 알고리즘을 활용해 Bitmap을 관리하는 Pool로, 최근에 사용되지 않는 비트맵부터 제거한다. 그 크기는 Glidedml MemorySizeCalculator를 통해 결정되고, 해당 값은 기기의 화면 사이즈와 밀도, 메모리 클래스(사용가능한 최대 메모리) 및 isLowRamDevice(Low-Ram 장치 여부)의 반환 값을 기반으로한다.
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context)
.setBitmapPoolScreens(3)
.build();
builder.setBitmapPool(new LruBitmapPool(calculator.getBitmapPoolSize()));
}
}
- BitmapPool에 여유 공간이 있을 경우, 새로 생성된 Bitmap을 저장한다
- 공간이 부족하면, 가장 오래된 Bitmap(최근에 사용되지 않은 Bitmap)을 제거하여 공간을 확보한다
- 크기와 구성(ARGB_8888, RGB_565 등)이 동일한 Bitmap 요청 시, Pool에 있는 Bitmap을 반환한다
BitmapPool 사이즈를 AppGlideModule에서 MemorySizeCalculator의 설정을 통해 다음과 같이 커스터마이징 할 수도 있다.
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
int bitmapPoolSizeBytes = 1024 * 1024 * 30; // 30mb
builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes));
}
}
Glide의 리소스 추적 및 재사용 방식
리소스가 안전하게 재사용될 수 있는 경우 Glide는 자동으로 이를 관리하며, 따라서 사용자가 직접 재활용을 강제하지 않아도 된다. 이를 위해 Glide는 참조 횟수(reference counting) 기반의 리소스 추적 및 관리 방식을 사용한다
참조 횟수(reference counting) 기반 리소스 관리
Glide는 리소스 사용 여부를 확인하기 위해 참조 횟수를 유지한다. 리소스가 여전히 사용 중인지, 재활용 가능한 상태인지를 결정한다
- 참조 횟수 증가
- into() 메서드로 리소스를 로드할 때마다 참조 횟수가 1씩 증가한다
- 예를 들어, 동일한 리소스가 두 개의 Target에 로드되면 참조 횟수는 2 됩니다.
- 참조 횟수 감소
- 리소스가 로딩된 View 또는 Target에서 clear()를 호출하는 경우
- 새로운 리소스에 대한 요청으로 View 또는 Target에서 into()가 호출된 경우
리소스 해제 후 안전하지 않은 사용 사례
참조 횟수가 0이 되면 리소스가 해제되는데, 그 이후에도 리소스를 계속 사용하면 정의되지 않은 동작, 그래픽 손상, 또는 앱 충돌이 발생할 수 있다. 따라서 공식문서에는 해당 위험을 일으킬 수 있는 대표적인 사례를 다음과 같이 소개한다.
- 이미지 뷰에서 직접 Drawable/Bitmap 사용
- getImageDrawable()을 호출해 로드된 Bitmap이나 Drawable을 가져와 다른 곳에 사용하지 않아야 한다
- Glide는 리소스를 재사용하거나 파괴할 수 있으므로 기존 리소스를 참조하면 문제가 발생할 수 있다
- SimpleTarget에서 리소스 제거하지 않음
- SimpleTarget의 onLoadCleared() 콜백에서 리소스를 명시적으로 제거하지 않으면 문제가 생길 수 있다
- View가 해당 리소스를 참조한 상태에서 Glide가 리소스를 재사용하면 예기치 못한 동작이 발생할 수 있다
- Bitmap 객체에 직접 recycle() 호출
- Glide로 관리되는 Bitmap에 대해 직접 recycle() 메서드를 호출하면 메모리 관리 충돌이 발생할 수 있다
- Glide가 자체적으로 리소스를 해제하거나 BitmapPool에 반환하기 때문에 사용자가 직접 해제 작업을 수행하면 안 된다
Pooling
대부분의 Glide 재활용 로직은 Bitmap을 대상으로 하지만 모든 리소스 구현은 recycle()을 구현하고 포함 할 수 있어, 재사용 가능한 데이터를 풀링 할 수 있다. ResourceDecoder는 원하는 Resource API 구현을 자유롭게 반환 할 수 있으므로 사용자는 자체 리소스 및 ResourceDecoder를 구현하여 새로운 타입에 대한 추가 풀링을 커스터마이징하거나 제공 할 수 있다.
특히 Bitmap의 경우 Glide는 리소스가 Bitmap 객체를 얻고 재사용 할 수있는 BitmapPool 인터페이스를 제공한다. Glide의 BitmapPool은 Glide 싱글톤을 사용하여 모든 Context로부터 얻을 수 있다.
다음과 같이 Glide는 알아서 BitmapPool을 이용해 리소스를 보관하고 적절히 재활용해 효율적인 이미지 로딩을 수행해주는 것으로 보이지만, 사용자가 코드를 통해 올바르게 리소스를 다루지 않으면 위험은 발생할 수 있다.
언급한 것처럼, Glide는 사용한 Bitmap 또는 기타 리소스를 BitmapPool에 저장하여 재사용한다. 이 과정에서 Glide는 clear() 메서드 호출이나 새로운 요청이 들어오면 이전 Bitmap 사용이 끝났다고 가정하고 이를 재활용한다. 하지만 이때 사용자가 Glide 외부에서 Bitmap이나 리소스를 직접 참조하고 사용하는 경우 문제가 발생할 수 있다.
Glide의 리소스 재사용 에러
Glide의 공식문서는 이문제를 잘 제어하기 위해 대표적인 에러와 그 해결방법을 명시해놓았다. 나도 개발 중, 이문서를 통해 오류를 해결할 수 있었으므로 함께 알아보자.
먼저 대표적인 두가지 에러이다.
Cannot draw a recycled Bitmap
Glide의 BitmapPool은 크기가 고정되어 있고, 따라서 Bitmap이 재사용되지 않고 풀에서 제거되면 Glide는 recycle()을 호출한다. 애플리케이션이 Glide에서 재활용해도 안전하다라는 것을 표시 한 후에도 실수로 Bitmap을 계속 유지하는 경우, 애플리케이션은 Bitmap 그리기를 시도하여 onDraw()에서 충돌이 발생할 수 있다. 아니면 하나의 대상이 두 개의 ImageView에 사용되고 있고 ImageView 중 하나가 BitmapPool에 배치 된 후에도 재활용 된 Bitmap에 액세스하려고 시도하기 때문일 수 있다.
Can’t call reconfigure() on a recycled bitmap
Bitmap이 BitmapPool에 여러 번 반환되거나 풀로 반환되지만 View에 의해 여전히 유지되는 경우 다른 이미지가 Bitmap으로 디코딩 될 수 있다. 이 경우 Bitmap의 내용이 새 이미지로 바뀐다. View는 이 프로세스 중에 여전히 Bitmap을 그리려고 시도 할 수 있으 며, 이로 인해 산출물이 발생하거나 원래 View에 새 이미지가 표시된다.
따라서 View는 여전히 이전 Bitmap이라고 생각하고 이를 그리려고 시도하지만, 실제로는 다른 데이터(새 이미지 데이터)가 들어있는 상황이 만들어지는 것이다. 따라서 이로 인해 화면이 깨진 이미지가 나타나거나, 원하지 않는 이미지가 불러와지는 문제가 나타날 수 있다.
재사용 에러의 대표적인 원인
같은 Target에 두개의 다른 리소스를 로드하려고 할 때
Glide에서 단일 Target에 여러 리소스를 로드하는 안전한 방법은 없다. 한 리소스가 로드 완료되면 기존 리소스는 자동으로 해제되거나 교체되고, 이로 인해 예상치 못한 동작이나 충돌이 발생할 수 있다.
- thumbnail() API 사용
- 일련의 리소스를 Target에 로드 할 수 있지만 다음에 onResourceReady()를 호출 할 때까지 이전 리소스를 참조하는 것이 안전하다. 당연히 일반적으로 더 나은 방법은 실제로 두 번째 View를 사용하고 두 번째 이미지를 두 번째 View에 로드하는 것이다.
- ViewSwitcher
- 두 개의 ImageView 자식을 가진 ViewSwitcher를 사용해 두 리소스를 교대로 로드하고 표시할 수 있다
- 커스텀 ViewTarget 사용
- Glide의 ViewTarget을 하위 클래스로 구현하여, 리소스 요청을 저장하거나 다른 태그를 관리할 수 있다
Target에 리소스를 로딩한 뒤 Target을 정리 또는 재사용하거나 리소스를 지속적으로 참조하는 것
Glide에서 Target에 로드된 리소스는 Target이 clear() 호출되거나 새로운 요청으로 대체될 때 해제됩니다. 그러나 clear() 이후에도 리소스를 참조하거나, View에서 리소스를 가져와 다른 곳에서 계속 사용하면 예상치 못한 동작이 발생할 수 있다. 따라서 이 오류를 방지하는 가장 쉬운 방법은 onLoadCleared()가 호출 될 때 리소스에 대한 모든 참조가 무효화 되도록 하는 것이다.
일반적으로 Bitmap을 로드한 다음 Target을 역 참조하고 Target에서 into() 또는 clear()를 다시 호출하지 않는 것이 안전하다. 그러나 Bitmap을 로드하고 대상을 지운 다음 나중에 Bitmap을 계속 참조하는 것은 안전하지 않다. 마찬가지로 리소스를 View에 로드한 다 음 getImageDrawable() 또는 다른 수단을 통해 View에서 리소스를 얻고 다른 곳에서 계속 참조하는 것은 안전하지 않다.
- onLoadCleared()에서 참조 무효화
- Glide는 onLoadCleared() 메서드에서 리소스가 더 이상 사용되지 않는다고 간주하므로, 이 메서드가 호출되면 리소스에 대한 모든 참조를 제거해야 함을 기억하자
- Target 사용 주의
- Target을 사용할 때는 into() 또는 clear()를 반복적으로 호출하지 않도록 주의하자
- Glide가 리소스를 관리하도록 맡기고, 로드된 Bitmap을 직접 참조하지 않는 것이 안전하다
Transformation에서 원본 Bitmap을 재활용하는 것
JavaDoc의 Transformation을 보면, transform()에 전달된 원본 Bitmap은 Transformation에서 반환된 Bitmap이 transform()에 전달된 인스턴스와 동일한 인스턴스가 아닌 경우 자동으로 재활용된다.
BitmapTransformation은 Glide의 리소스 생성을 처리하기 위한 보일러플레이트를 제공하지만 재활용은 내부적으로 수행 되므로, Transformation과 BitmapTransformation 모두 전달된 Bitmap 또는 Resource를 재활용해서는 안된다. 또한 커스텀 BitmapTransformation을 BitmapPool에서 가져오면 transform()에서 반환하지 않는 중간 Bitmap은 BitmapPool에 다시 넣거나 recycle()을 호출해야 하지만 둘 다 가져서는 안된다. Glide에서 얻은 Bitmap은 절대 recycle()해서는 안된다.
이렇게 마지막으로 Glide가 어떻게 리소스를 재활용하고 효율적으로 로드를 하는지 알아보았다. 이전 글에 보았던 캐싱 전략과 함께 쓰여 Glide는 bitmap을 최대한 알아서 관리해주려고 하고 있다. 하지만 사용자의 기기 메모리는 제한적이고, 개발자가 비트맵의 참조를 적절히 해제해주고, 동시에 접근하지 않도록 코드를 잘 짜주는 것은 여전히 중요하다.
그렇기 위해서 이번글에 언급한 OOM 방지 전략에 대해 잘 살펴보고, Glide를 잘 사용하도록 해보자.
참고자료
https://charlezz.com/ 님의 Glide 공식문서 번역본
Image Loading and Caching Library Part 2 — Principle / Memory & Footprint / Compose
Image Library의 동작 방식
medium.com