(Blinking Issue 발생)
360도 차량 이미지를 보여주는 기능 요구사항으로 인해,
이미지 60장을 한번에 불러와서 보여주는 프리로딩 기능을 구현하던 중 이미지 몇장이 깜박 거리는 현상이 발생했다.
Coil의 Image Loader를 활용해 프리로딩 기능을 구현하던 중 이미지가 깜박 거리는 문제가 발생했다.
문제의 원인을 분석하기 위해 Log를 찍어 봤지만 서버에서 이미지 60장을 다 불러오고 프로그레스 바가 끝나기 때문에
문제가 없어 보였지만 여전히 이미지가 깜박거리는 현상이 발생했다.
(문제 공유 및 페어프로그래밍을 통한 원인 분석)
데일리 스크럼때 해당 이슈를 공유하고 나서 혼자 문제 원인을 분석하던 중
프론트엔드 팀원분이 오셔서 함께 페어 프로그래밍 해보자고 제안을 하셨고 절박했던 나는 바로 넙죽 감사하다고
전해드리고 함께 문제 원인을 찾기 시작했다.
우선 팀원과 함께 고민해 본 결과 Log에 찍히는 이미지 url들은 요청만 한 상태이고 비동기성으로 인해 실제 이미지는
다 받아오지 않은 상태에서 프로그레스 바가 끝나는 것 같다고 문제의 원인을 추려보았다.
그래서 더 정확한 분석을 하기위해 페어 프로그래밍을 통해 Coil의 ImageLoader의 내부 코드를 분석해보니
아래 코드처럼 ImageLoader
의 excute
함수가 ImageResult
객체를 반환하고,
요청 성공시 SuccessResult
타입으로 반환 되는 것을 확인했다.
interface ImageLoader {
...
suspend fun execute(request: ImageRequest): ImageResult
...
}
class ImageRequest() {
...
@JvmDefaultWithCompatibility
interface Listener {
...
@MainThread
fun onSuccess(request: ImageRequest, result: SuccessResult) {}
...
}
...
}
하지만 이미지 60장 모두 SuccessResult 타입으로 반한되는 것을 확인했고
다른쪽에 문제가 있을것 같다고 하고 페어 프로그래밍을 끝냈다.
(팀원분 께서도 잠깐 시간을 내주신 거기 때문에 ,, 감사합니다 ㅜㅜ)
(여전히 발생하는 문제)
혼자서 문제의 원인을 고민해보던 중 이미지 캐싱이 잘 안되었을 수 있겠다고 판단하여
캐싱이 잘 되는지 확인해보기로 했다.
Coil의 내부 코드를 보면 onSuccess 고차함수의 람다 표현식 매개변수로 metadata를 받을 수 있는데
해당 변수를 통해 metadata.datasource를 찍으면 캐싱 여부를 확인할 수 있다.
아래는 Coil 내부 코드에 있는 캐싱 상태를 확인할 수 있는 enum class이다.
/**
* Represents the source that an image was loaded from.
*
* @see SourceResult.dataSource
* @see DrawableResult.dataSource
*/
enum class DataSource {
/**
* Represents an [ImageLoader]'s memory cache.
*
* This is a special data source as it means the request was
* short circuited and skipped the full image pipeline.
*/
MEMORY_CACHE,
/**
* Represents an in-memory data source (e.g. [Bitmap], [ByteBuffer]).
*/
MEMORY,
/**
* Represents a disk-based data source (e.g. [DrawableRes], [File]).
*/
DISK,
/**
* Represents a network-based data source (e.g. [HttpUrl]).
*/
NETWORK
}
아래와 같이 로그를 찍어 봤다.
@BindingAdapter("url", "circleCrop", "radius", "blurRadius", "blurSampling", requireAll = false)
fun ImageView.setImageSrcWithUrl(
url: String?,
isCircleCrop: Boolean = false,
radius: Float? = null,
blurRadius: Int? = null,
blurSampling: Float? = null
) {
load(url, ImageUtils.imageLoader) {
...
listener(
onSuccess = { request, metadata ->
Log.d("ImageLoading", "Loaded from: ${request.data}")
when (metadata.dataSource) {
DataSource.MEMORY -> Log.d("ImageLoading", "Loaded from Memory Cache")
DataSource.DISK -> Log.d("ImageLoading", "Loaded from Disk Cache")
DataSource.NETWORK -> Log.d("ImageLoading", "Loaded from Network")
DataSource.MEMORY_CACHE -> Log.d("ImageLoading", "Loaded from MEMORY_CACHE")
else -> Log.d("ImageLoading", "Loaded from other")
}
}
)
...
}
그런데 아래 그림과 같이 이미지 4장 정도가 디스크 캐싱으로 이미지를 불러오는 문제를 확인할 수 있었다.
(문제 해결 및 필요한 메모리 용량 측정)
코일의 기본 메모리 캐시 크기는 전체 메모리의 25%로 설정되어 있다.
그래서 아래 코드와 같이 memoryCache
의 maxSizePercent
를 통해 캐싱 용량을 전체 메모리의 30%로 설정해 봤지만
여전히 이미지 깜박 거리는 현상이 발생했고 아예 50%로 올려서 테스트를 해보니 문제가 해결되었다.
val imageLoader: ImageLoader by lazy {
ImageLoader.Builder(CaArtApplication.getApplicationContext())
.memoryCache {
MemoryCache.Builder(CaArtApplication.getApplicationContext())
.maxSizePercent(0.5)
.build()
}
.build()
}
하지만 문제는 해결 되었지만 max값이 과한 감이 있고 실제로 50%나 필요한지
의문이 들어 실제 캐싱에 필요한 총 메모리 용량을 측정해보기로 결정했다.
아래 코드와 같이 측정해 본 결과
SuceessResult
의 bitmap
용량을 측정해 본 결과 이미지 한장 당 1.86MB가 필요한 것을 확인했다.
fun preloadImages(urls: List<String>, onImageLoaded: (Int) -> Unit) {
imageLoader.memoryCache?.clear()
val currentMemoryUsage = imageLoader.memoryCache?.size?.div((1024 * 1024))
val maxMemoryCacheSize = imageLoader.memoryCache?.maxSize?.div((1024 * 1024))
Log.d("test", "currentMemoryUsage: ${currentMemoryUsage}")
Log.d("test", "maxMemoryCacheSize: ${maxMemoryCacheSize}")
CoroutineScope(Dispatchers.IO).launch {
val successfulLoads: AtomicInteger = AtomicInteger(0)
urls.forEachIndexed { index, url ->
val request = ImageRequest.Builder(CaArtApplication.getApplicationContext())
.data(url)
.build()
val result = imageLoader.execute(request)
if (result is SuccessResult) {
successfulLoads.incrementAndGet()
if (result.drawable is BitmapDrawable) {
val bitmapSizeInBytes = (result.drawable as BitmapDrawable).bitmap.byteCount
val bitmapSizeInMB = bitmapSizeInBytes.toFloat() / (1024 * 1024)
Log.d("test", "Size of the cached image in bytes: $bitmapSizeInMB")
}
}
val loadProgress = (successfulLoads.get().toFloat() / urls.size * 100).toInt()
onImageLoaded(loadProgress)
}
}
}
로그 결과
maxSize를 40%로 설정하면 캐싱해서 담을 수 있는 용량은 98MB
인데서버에서 받아온 이미지 60장의 총 크기는 약 111MB
이다.
어라..? 허용하는 캐싱용량을 넘어서는데 어떻게 전부 캐싱을 할 수있는거지??
여기서 이상함을 느끼고 실제 다운 받는 이미지 용량과 캐싱되는 이미지 용량이 다른가라는 의문을 품게 되었고
ImageLoader
의 내부 코드를 더 파보기로 했다.
interface ImageLoader {
...
/**
* An in-memory cache of previously loaded images.
*/
val memoryCache: MemoryCache?
}
MemoryCahce
클래스의 내부를 확인해보면 캐싱하는 이미지들을 Value
클래스에서 map을 통해 Bitmap
으로 관리하는 것을 확인할 수 있었고, 실제 캐싱해서 저장되어 있는 BitMap을 가져오려면 MemoryChace클래스에서 get(key) 메소드를 이용해서 가져올 수 있는 것을 확인했다.
이 후 아래 코드를 통해 실제 캐싱해서 저장되어 있는 BitMap 크기를 측정해보니 실제로 총 78MB가 필요한 것을 확인할 수 있었다.
fun preloadImages(urls: List<String>, onImageLoaded: (Int) -> Unit) {
imageLoader.memoryCache?.clear()
val currentMemoryUsage = imageLoader.memoryCache?.size?.div((1024 * 1024))
val maxMemoryCacheSize = imageLoader.memoryCache?.maxSize?.div((1024 * 1024))
Log.d("test", "currentMemoryUsage: ${currentMemoryUsage}")
Log.d("test", "maxMemoryCacheSize: ${maxMemoryCacheSize}")
CoroutineScope(Dispatchers.IO).launch {
val successfulLoads: AtomicInteger = AtomicInteger(0)
urls.forEachIndexed { index, url ->
val request = ImageRequest.Builder(CaArtApplication.getApplicationContext())
.data(url)
.build()
val result = imageLoader.execute(request)
if (result is SuccessResult) {
successfulLoads.incrementAndGet()
}
val loadProgress = (successfulLoads.get().toFloat() / urls.size * 100).toInt()
if(loadProgress >= 100) {
imageLoader.memoryCache?.keys?.forEach { key ->
Log.d("test", "realSize: ${imageLoader.memoryCache?.get(key)?.bitmap?.byteCount?.div((1024 * 1024))}")
}
}
onImageLoaded(loadProgress)
}
}
}
코일 내부에서 이미지 용량 최적화 (다운 샘플링)을 통해 실제 다운 받는 이미지 보다 캐싱되는 이미지 용량이 더 적다는 것을 확인할 수 있었다.
새롭게 알게된 점
- 코일의 캐시 정책
- 코일의 내부 코드
- 서버에서 다운 받는 이미지 용량 보다 캐싱 되는 이미지 용량이 더 적다.
'Android' 카테고리의 다른 글
Android - UseCase 추상화 (0) | 2024.03.15 |
---|---|
Android - CustomView (1) | 2024.01.03 |
안드로이드 - Jetpack Compose Navigation Test Code (0) | 2023.12.30 |
Android - JUnit 단위 테스트 (0) | 2023.07.04 |
Android - GitHub에 API Key, Hash 값 숨기기 (0) | 2023.01.05 |