Android LruCache

AndroidJavaKotlinキャッシュライブラリモバイル開発メモリ管理

ライブラリ

Android LruCache

概要

Android LruCacheは、Androidプラットフォームに組み込まれているLRU(Least Recently Used)アルゴリズムベースのメモリキャッシュです。

詳細

Android LruCacheは、Androidアプリケーション開発における効率的なメモリキャッシングを実現するためのクラスです。LRU(Least Recently Used)アルゴリズムを採用し、キャッシュサイズが上限に達した際に最も使用頻度の低いエントリを自動的に削除します。API Level 12(Android 3.1)で導入され、それ以前のバージョンではAndroidX Collection libraryのサポート版を利用できます。主にBitmapキャッシュやAPIレスポンスのキャッシュなどに使用され、メモリ使用量を制御しながら高速なデータアクセスを実現します。スレッドセーフな実装により、マルチスレッド環境でも安全に使用でき、カスタムサイズ計算やカスタム削除ポリシーの実装も可能です。Androidのライフサイクルと連携した適切なメモリ管理により、OutOfMemoryErrorを防ぎながらアプリケーションのパフォーマンスを向上させます。

メリット・デメリット

メリット

  • プラットフォーム標準: Android標準ライブラリの一部で追加依存なし
  • 自動メモリ管理: LRUアルゴリズムによる効率的なメモリ使用
  • スレッドセーフ: マルチスレッド環境での安全な操作
  • カスタマイズ可能: サイズ計算や削除ポリシーのカスタマイズ
  • 軽量: 最小限のオーバーヘッドで高いパフォーマンス
  • 互換性: AndroidX版により古いAPIレベルでも利用可能
  • ガベージコレクション効率: 弱参照より効率的なメモリ管理

デメリット

  • Android限定: Android以外のプラットフォームでは利用不可
  • メモリのみ: 永続化されないため、アプリ終了時にデータ消失
  • サイズ制限: メモリ制約によるキャッシュサイズの限界
  • 設定の複雑さ: 適切なサイズ設定にはメモリ分析が必要
  • null許可なし: キーと値にnullを使用できない

主要リンク

書き方の例

基本的なLruCache使用法

// Java
import android.util.LruCache;

public class ImageCache {
    private LruCache<String, Bitmap> memoryCache;

    public ImageCache() {
        // 利用可能メモリの1/8をキャッシュに使用
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 8;

        memoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // Bitmapのサイズを返す(KB単位)
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            memoryCache.put(key, bitmap);
        }
    }

    public Bitmap getBitmapFromMemCache(String key) {
        return memoryCache.get(key);
    }
}

Kotlinでの使用例

import android.util.LruCache
import android.graphics.Bitmap

class ImageCacheKotlin {
    private val memoryCache: LruCache<String, Bitmap>

    init {
        // 利用可能メモリの1/8をキャッシュに使用
        val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
        val cacheSize = maxMemory / 8

        memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
            override fun sizeOf(key: String, bitmap: Bitmap): Int {
                // Bitmapのサイズを返す(KB単位)
                return bitmap.byteCount / 1024
            }

            override fun entryRemoved(
                evicted: Boolean, 
                key: String, 
                oldValue: Bitmap, 
                newValue: Bitmap?
            ) {
                // エントリが削除されたときの処理
                if (evicted) {
                    println("Cache entry removed: $key")
                }
            }
        }
    }

    fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            memoryCache.put(key, bitmap)
        }
    }

    fun getBitmapFromMemCache(key: String): Bitmap? {
        return memoryCache.get(key)
    }

    fun clearCache() {
        memoryCache.evictAll()
    }

    fun getCacheInfo(): String {
        return "Size: ${memoryCache.size()}, " +
               "MaxSize: ${memoryCache.maxSize()}, " +
               "HitCount: ${memoryCache.hitCount()}, " +
               "MissCount: ${memoryCache.missCount()}"
    }
}

APIレスポンスのキャッシュ

data class ApiResponse(
    val data: String,
    val timestamp: Long
) {
    fun isExpired(timeoutMs: Long = 300_000): Boolean {
        return System.currentTimeMillis() - timestamp > timeoutMs
    }
}

class ApiCache {
    private val cache = object : LruCache<String, ApiResponse>(50) {
        override fun sizeOf(key: String, value: ApiResponse): Int {
            return key.length + value.data.length
        }
    }

    fun get(key: String): ApiResponse? {
        val response = cache.get(key)
        return if (response?.isExpired() == true) {
            cache.remove(key)
            null
        } else {
            response
        }
    }

    fun put(key: String, data: String) {
        val response = ApiResponse(data, System.currentTimeMillis())
        cache.put(key, response)
    }

    fun invalidate(key: String) {
        cache.remove(key)
    }
}

画像読み込みとキャッシュの統合

import kotlinx.coroutines.*
import java.net.URL

class ImageLoader(private val scope: CoroutineScope) {
    private val imageCache = object : LruCache<String, Bitmap>(
        // 20MBのキャッシュサイズ
        20 * 1024 * 1024
    ) {
        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            return bitmap.byteCount
        }

        override fun entryRemoved(
            evicted: Boolean,
            key: String,
            oldValue: Bitmap,
            newValue: Bitmap?
        ) {
            if (evicted && !oldValue.isRecycled) {
                // 必要に応じてBitmapをリサイクル
                oldValue.recycle()
            }
        }
    }

    suspend fun loadImage(url: String): Bitmap? {
        // キャッシュから取得を試行
        imageCache.get(url)?.let { return it }

        // ネットワークから取得
        return withContext(Dispatchers.IO) {
            try {
                val bitmap = downloadBitmap(url)
                bitmap?.let { imageCache.put(url, it) }
                bitmap
            } catch (e: Exception) {
                null
            }
        }
    }

    private fun downloadBitmap(url: String): Bitmap? {
        // 実際の画像ダウンロード処理
        return BitmapFactory.decodeStream(URL(url).openConnection().inputStream)
    }

    fun preloadImages(urls: List<String>) {
        scope.launch {
            urls.forEach { url ->
                if (imageCache.get(url) == null) {
                    loadImage(url)
                }
            }
        }
    }

    fun clearMemoryCache() {
        imageCache.evictAll()
    }

    fun getMemoryUsage(): String {
        val currentSize = imageCache.size()
        val maxSize = imageCache.maxSize()
        val usagePercent = (currentSize.toFloat() / maxSize * 100).toInt()
        return "Memory cache: ${currentSize / 1024 / 1024}MB / ${maxSize / 1024 / 1024}MB ($usagePercent%)"
    }
}

カスタムオブジェクトのキャッシュ

data class UserProfile(
    val id: String,
    val name: String,
    val avatar: Bitmap?,
    val lastUpdated: Long
) {
    fun getEstimatedSize(): Int {
        return id.length * 2 + name.length * 2 + (avatar?.byteCount ?: 0)
    }
}

class UserProfileCache {
    private val cache = object : LruCache<String, UserProfile>(
        // 最大100ユーザー
        100
    ) {
        override fun sizeOf(key: String, value: UserProfile): Int {
            return value.getEstimatedSize()
        }

        override fun entryRemoved(
            evicted: Boolean,
            key: String,
            oldValue: UserProfile,
            newValue: UserProfile?
        ) {
            if (evicted) {
                // avatarのBitmapをリサイクル
                oldValue.avatar?.let { bitmap ->
                    if (!bitmap.isRecycled) {
                        bitmap.recycle()
                    }
                }
            }
        }
    }

    fun getProfile(userId: String): UserProfile? {
        return cache.get(userId)
    }

    fun cacheProfile(profile: UserProfile) {
        cache.put(profile.id, profile)
    }

    fun updateProfile(userId: String, updater: (UserProfile) -> UserProfile) {
        cache.get(userId)?.let { currentProfile ->
            val updatedProfile = updater(currentProfile)
            cache.put(userId, updatedProfile)
        }
    }

    fun removeProfile(userId: String) {
        cache.remove(userId)
    }

    fun getStatistics(): Map<String, Any> {
        return mapOf(
            "currentSize" to cache.size(),
            "maxSize" to cache.maxSize(),
            "hitCount" to cache.hitCount(),
            "missCount" to cache.missCount(),
            "hitRate" to (cache.hitCount().toFloat() / 
                         (cache.hitCount() + cache.missCount()) * 100)
        )
    }
}

メモリ圧迫時の動的サイズ調整

import android.content.ComponentCallbacks2

class AdaptiveLruCache<K, V> : LruCache<K, V> {
    private var baseCacheSize: Int
    private var currentCacheSize: Int

    constructor(maxSize: Int) : super(maxSize) {
        baseCacheSize = maxSize
        currentCacheSize = maxSize
    }

    fun onTrimMemory(level: Int) {
        when (level) {
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE -> {
                // メモリ使用量を75%に削減
                adjustCacheSize(0.75f)
            }
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> {
                // メモリ使用量を50%に削減
                adjustCacheSize(0.5f)
            }
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                // キャッシュをクリア
                evictAll()
            }
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                // UIが非表示の時、60%に削減
                adjustCacheSize(0.6f)
            }
        }
    }

    private fun adjustCacheSize(factor: Float) {
        val newSize = (baseCacheSize * factor).toInt()
        if (newSize != currentCacheSize) {
            currentCacheSize = newSize
            resize(newSize)
            
            // 必要に応じて現在のエントリを削除
            while (size() > newSize) {
                val key = eldestKey()
                if (key != null) {
                    remove(key)
                } else {
                    break
                }
            }
        }
    }

    private fun eldestKey(): K? {
        // LruCacheには直接的なeldestキーアクセスがないため、
        // カスタム実装が必要
        return null
    }
}