A two-level cache — memory first, disk second — is the standard pattern for fast, offline-capable apps. Understanding how LruCache and DiskLruCache work helps you tune the libraries (Glide, Coil) built on top of them and implement custom caching where needed.
LruCache: In-Memory Cache
LruCache is a thread-safe, size-bounded cache with Least Recently Used eviction. When the cache is full, the least recently accessed entry is evicted.
class BitmapCache(maxMemoryKb: Int = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()) {
private val cache = object : LruCache<String, Bitmap>(maxMemoryKb) {
// Override sizeOf to measure in KB, not count
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.byteCount / 1024
}
override fun entryRemoved(evicted: Boolean, key: String, old: Bitmap, new: Bitmap?) {
// Called when an entry is evicted or replaced
if (evicted) old.recycle() // pre-API 26 — safe to recycle evicted bitmaps
}
}
fun put(key: String, bitmap: Bitmap) = cache.put(key, bitmap)
fun get(key: String): Bitmap? = cache.get(key)
fun remove(key: String) = cache.remove(key)
fun evictAll() = cache.evictAll()
fun trimToSize(maxSize: Int) = cache.trimToSize(maxSize)
}
Sizing the cache: a common rule is 1/8 of available heap:
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8 // in KB
DiskLruCache: Persistent Disk Cache
Disk cache survives app restarts and process death. DiskLruCache from OkHttp is the standard implementation.
class DiskCache(context: Context, private val maxSizeBytes: Long = 50L * 1024 * 1024) {
private val cacheDir = File(context.cacheDir, "image_cache")
private val cache: Cache = Cache(cacheDir, maxSizeBytes) // OkHttp Cache
// For manual use without OkHttp, use Jake Wharton's DiskLruCache:
// val diskCache = DiskLruCache.open(cacheDir, 1, 1, maxSizeBytes)
}
For image caching, you rarely implement DiskLruCache directly — Glide and Coil handle it:
// Glide: custom cache size
Glide.get(context).registry
// In GlideModule:
@GlideModule
class CustomGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDiskCache(
InternalCacheDiskCacheFactory(context, 100 * 1024 * 1024) // 100 MB
)
builder.setMemoryCache(
LruResourceCache(30 * 1024 * 1024L) // 30 MB memory cache
)
}
}
// Coil: configure in ImageLoader
val imageLoader = ImageLoader.Builder(context)
.memoryCache {
MemoryCache.Builder(context).maxSizePercent(0.15).build() // 15% of memory
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
.maxSizeBytes(100L * 1024 * 1024) // 100 MB
.build()
}
.build()
Two-Level Cache Pattern
The standard lookup order:
suspend fun getBitmap(url: String): Bitmap? {
val key = url.md5()
// 1. Memory cache — fastest
memoryCache.get(key)?.let { return it }
// 2. Disk cache — fast, survives process death
diskCache.get(key)?.let { bitmap ->
memoryCache.put(key, bitmap) // promote to memory
return bitmap
}
// 3. Network — slowest, cache the result
return try {
val bitmap = downloadBitmap(url)
memoryCache.put(key, bitmap)
diskCache.put(key, bitmap)
bitmap
} catch (e: IOException) {
null
}
}
Cache Key Design
// ❌ Bad: URL can contain query params that change but represent the same resource
val key = imageUrl // "https://cdn.example.com/photo.jpg?w=200&token=abc123"
// ✅ Better: hash the stable parts of the URL
val key = imageUrl.substringBefore("?").md5()
// For versioned content: include version in key
val key = "${userId}_${avatarVersion}".md5()
Responding to Memory Pressure
class MyViewModel : ViewModel() {
private val bitmapCache = BitmapCache()
// Called by Application.onTrimMemory
fun onMemoryTrimmed(level: Int) {
when (level) {
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ->
bitmapCache.trimToSize(bitmapCache.cache.size() / 2)
ComponentCallbacks2.TRIM_MEMORY_COMPLETE ->
bitmapCache.evictAll()
}
}
}
Key Takeaways
| Concept | Rule |
|---|---|
LruCache sizing | ~1/8 of max heap for bitmaps; override sizeOf to measure bytes |
| Disk cache size | 50–100 MB is typical for image apps |
| Two-level lookup | Memory → Disk → Network; promote on disk hit |
| Cache keys | Use a hash of stable URL parts, not the full URL with tokens |
| Memory pressure | Trim/evict in onTrimMemory; respond proportionally to level |