-
Notifications
You must be signed in to change notification settings - Fork 0
⚙ [기술 분석] Coil의 디스크 캐싱
이미지 로더 라이브러리를 사용하지 않은 상태에서 많은 이미지를 사용하거나 고해상도 이미지를 로드해야 하는 경우 메모리 부족으로 Out Of Memory가 발생한다. 안드로이드는 앱 내에서 사용할 수 있는 힙 메모리가 정해져 있기 때문에 많은 이미지를 로드한다면, 메모리 부족 상태가 나타날 수 있다.
이러한 문제점을 해결하기 위해 비트맵 캐싱 기법 중 하나인 디스크 캐싱을 사용하는 것이 좋다
이미지가 화면에서 사라지고 다시 구성할 때 이미지를 매번 로드하는 것이 아니라, 메모리와 디스크 캐시를 이용하여 어딘가에서 저장되어있던 비트맵을 다시 가져온다면 시간 단축 및 성능 개선이 가능하다. 비트맵 캐싱에는 Memory Cache와 Disk Cache가 존재한다
- Memory Cache
- 메모리 캐시는 중요한 애플리케이션 메모리를 사용하는 대신 비트맵에 빠르게 액세스할 수 있다. 안드로이드에서는 메모리 캐시의 사용을 위해 LruCache class를 지원한다.
- LruCache 클래스는 비트맵을 캐싱하는 작업, 캐시가 지정된 크기를 초과하기 전에 가장 오래 전에 사용된 항목을 제거하는 작업에 특히 적합하다.
- 저장해야할 용량이 크다면, Disk Cache를 고려해보는 것이 좋다.
- Disk Cache
- 많은 캐시가 요구되거나, 앱이 백그라운드로 전환되어도 적재한 캐시가 삭제되지 않기를 바란다면, Disk Cache를 이용하는 것이 좋다.
- 하지만, Disk로부터 캐싱된 비트맵을 가져올 때는 Memory에서 로드하는 것보다 시간이 오래 걸린다.
공식 문서를 확인해보면, 직접 UI 스레드에서 메모리 캐시를 확인하고 백그라운드 스레드에서 디스크 캐시를 확인하는 작업과, 이미지 처리 완료 시에 나중에 사용할 수 있도록 최종 비트맵을 메모리와 디스크 캐시에 모두 추가하는 코드가 포함되어있다.
private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB
private const val DISK_CACHE_SUBDIR = "thumbnails"
...
private var diskLruCache: DiskLruCache? = null
private val diskCacheLock = ReentrantLock()
private val diskCacheLockCondition: Condition = diskCacheLock.newCondition()
private var diskCacheStarting = true
override fun onCreate(savedInstanceState: Bundle?) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR)
InitDiskCacheTask().execute(cacheDir)
...
}
internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() {
override fun doInBackground(vararg params: File): Void? {
diskCacheLock.withLock {
val cacheDir = params[0]
diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE)
diskCacheStarting = false // Finished initialization
diskCacheLockCondition.signalAll() // Wake any waiting threads
}
return null
}
}
internal inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
...
// Decode image in background.
override fun doInBackground(vararg params: Int?): Bitmap? {
val imageKey = params[0].toString()
// Check disk cache in background thread
return getBitmapFromDiskCache(imageKey) ?:
// Not found in disk cache
decodeSampledBitmapFromResource(resources, params[0], 100, 100)
?.also {
// Add final bitmap to caches
addBitmapToCache(imageKey, it)
}
}
}
fun addBitmapToCache(key: String, bitmap: Bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap)
}
// Also add to disk cache
synchronized(diskCacheLock) {
diskLruCache?.apply {
if (!containsKey(key)) {
put(key, bitmap)
}
}
}
}
fun getBitmapFromDiskCache(key: String): Bitmap? =
diskCacheLock.withLock {
// Wait while disk cache is started from background thread
while (diskCacheStarting) {
try {
diskCacheLockCondition.await()
} catch (e: InterruptedException) {
}
}
return diskLruCache?.get(key)
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
fun getDiskCacheDir(context: Context, uniqueName: String): File {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
val cachePath =
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
|| !isExternalStorageRemovable()) {
context.externalCacheDir.path
} else {
context.cacheDir.path
}
return File(cachePath + File.separator + uniqueName)
}
하지만, 매번 이렇게 비트맵 캐싱을 구현한다면, 코드가 꽤 길어지고 복잡해질 것이다. 이러한 기능을 지원해주는 라이러리들이 바로 Glide, Coil 등의 이미지 라이브러리이다.
coil이란 Coroutine Image Lodaer의 앞글자를 따라 지어진 이미지 로딩 라이브러리이다. 코틀린으로 만들어진 라이브러리이고, 여러 기능을 가지고 있다.
메모리, 디스크 캐싱(Caching), 이미지 다운샘플링, 비트맵 재사용 등을 수행하며 여러단계의 최적화를 수행하며, Glide나 Frecso와 같은 다른 라이브러리보다 훨씬 적은 code 수를 가진다.
또한, crossfade 옵션을 가지는데, 500x500 만큼의 이미지의 로딩이 끝날 때 까지100x100 만큼의 저화질의 이미지를 먼저 로딩해서 보여주기 때문에, 사용성이 좋다.
위 사진은 AsyncImage가 composition된 이후부터 최종적으로 이미지가 나오기까지의 과정인데, 3,4번 과정인 readFromDiskCache와 writeToDiskCache에서 비트맵 캐싱 과정이 포함된다.
아래 코드는 HTTP URI를 통해 이미지를 로드할 때 사용하는 클래스인, HttpUriFetcher의 일부분인데, 불러오고자 하는 이미지가 캐시에 있을 경우 이를 불러오는 readFromDiskCache 메소드가 실행된다.
// In HttpUriFetcher.kt
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) {
diskCache.value?.openSnapshot(diskCacheKey)
} else {
null
}
}
이때 CachePolicy의 설정에 따라 불러올지 여부를 체크하는데, 기본 옵션은 CachePolicy.ENABLED로 되어 있어 캐시 설정을 따로 하지 않아도 기본적으로 캐시 데이터 로드를 수행하게 구성되어 있다. 코일에서 쓰이는 DiskCache의 내부는 LRU Cache의 개념에 맞춰 구축되어 있다. LRU Cache(Least Recently Used Cache)는 캐시 관리 기법 중 하나로, 가장 최근에 사용되지 않은 데이터를 우선적으로 제거하여 새로운 데이터를 저장하는 방식이다.
만약, 저장된 캐시 데이터가 없는 경우, writeToDishCache로 네트워크로 가져온 이미지를 저장한다. 이 때 저장하는 데이터의 키는 별도 지정하지 않은 경우 이미지의 url이 기본 키 값으로 지정된다.
Coil에서는 기본적으로 캐시 기능을 제공하지만, 필요에 따라 캐시 크기나 정책을 사용자가 정의할 수 있다. 예를 들어, ImageLoader를 설정할 때 캐시 크기 등을 조정할 수 있다 . 실제 프로젝트에서 네트워크가 오프라인 상태임에도 불구하고, coil에 캐시된 이미지들은 정상적으로 불러와지는 것을 확인할 수 있었다.
Copyright 2024. Team Kolown All Rights Reserved.
- ✅ [기술 결정] Camera
- ✅ [기술 결정] Image Load
- ✅ [기술 결정] UI Toolkit - Copmpose
- ✅ [기술 결정] 데이터 별 UID 생성
- ✅ [기술 결정] Debounce & Paging 사용해서 검색 구현
- ✅ [기술 결정] Google Login
- ✅ [기술 결정] 스켈레톤 UI
- ⚙ [기술 분석] DI
- ⚙ [기술 분석] Image Compress
- ⚙ [기술 분석] 이미지 리사이징
- ⚙ [기술 분석] CameraX API
- ⚙ [기술 분석] Firebase & 랜덤 로딩
- ⚙ [기술 분석] ViewModel 공유
- ⚙ [기술 분석] Firestore 쿼리 전략
- ⚙ [트러블 슈팅] Chip with TextField(Custom with IntrinsicSize)
- ⚙ [트러블 슈팅] WindowInset
- ⚙ [트러블 슈팅] UI 실시간 반영
- ⚙ [트러블 슈팅] IME Padding
- ⚙ [트러블 슈팅] PagingSource reset
- ⚙ [트러블 슈팅] SharedFlow - SnackBar
- ⚙ [트러블 슈팅] Camera와 Lifecycle 동기화