androidengineers.Book a session

Caching Strategies

Memory (LruCache) & Disk (DiskLruCache)

article25 minMedium

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

ConceptRule
LruCache sizing~1/8 of max heap for bitmaps; override sizeOf to measure bytes
Disk cache size50–100 MB is typical for image apps
Two-level lookupMemory → Disk → Network; promote on disk hit
Cache keysUse a hash of stable URL parts, not the full URL with tokens
Memory pressureTrim/evict in onTrimMemory; respond proportionally to level

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Memory (LruCache) & Disk (DiskLruCache) | Android System Design | Android Engineers