0Pricing
Android Academy · レッスン

キャッシュとパフォーマンス

メモリキャッシュとディスクキャッシュの基本を学びます。

「キャッシュとパフォーマンス」はCoddyKit上の無料Android Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAndroid Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Android Academyコースには全4レッスンが含まれています。

キャッシュが重要な理由

画像のダウンロードとデコードには大きなコストがかかり、帯域幅、バッテリー、CPUを消費します。リストをスクロールするたびに同じ画像を再取得すると、アプリの動作が遅く感じられ、データ通信量も増えてしまいます。

キャッシュは、デコード済みの画像とダウンロードしたバイトデータを保存するため、次回のリクエストをほぼ瞬時に処理できます。Coilにはデフォルトで2種類のキャッシュが備わっています。メモリキャッシュ(高速で、デコード済みのビットマップを保持)とディスクキャッシュ(永続的で、元のバイトデータを保持)です。

このレッスンでは、それぞれの仕組みと調整方法を説明します。

2つの層:メモリとディスク

Coilは次の順番でキャッシュを確認します。

  • メモリキャッシュ:最近使用したデコード済みのビットマップをRAMに保持します。ここでヒットすれば、デコードが不要なため瞬時に表示できます。
  • ディスクキャッシュ:デバイス上に、ダウンロードした元のバイトデータを保存します。ここでヒットすればネットワークアクセスは不要ですが、デコードは必要です。
  • ネットワーク:両方のキャッシュにない場合にのみ使われる、最も時間のかかる経路です。

メモリは高速ですが容量が小さく、アプリが終了すると消去されます。一方、ディスクは容量が大きく、アプリを再起動しても保持されます。

ImageLoaderを設定する

キャッシュは、すべてのリクエストの背後で動作するImageLoaderに保持されます。通常はカスタムローダーを1つ構築して再利用します。ここでは、メモリキャッシュのサイズを使用可能なRAMの割合で、ディスクキャッシュのサイズを固定バイト数で設定します。

シングルトンローダーを使うと、重複したキャッシュや無駄なメモリ消費を避けられます。

import android.content.Context
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import okio.Path.Companion.toOkioPath

fun newImageLoader(context: Context): ImageLoader =
    ImageLoader.Builder(context)
        .memoryCache {
            MemoryCache.Builder()
                .maxSizePercent(context, 0.25) // 25% of app RAM
                .build()
        }
        .diskCache {
            DiskCache.Builder()
                .directory(context.cacheDir.resolve("image_cache").toOkioPath())
                .maxSizeBytes(50L * 1024 * 1024) // 50 MB
                .build()
        }
        .build()

シングルトンローダーを提供する

Coil 3では、ApplicationクラスにSingletonImageLoader.Factoryを実装することで、アプリケーション全体で使うローダーを1つ設定できます。これにより、すべてのAsyncImageが自動的にそのローダーを使うため、毎回ローダーを渡す必要がありません。

import android.app.Application
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader

class MyApp : Application(), SingletonImageLoader.Factory {
    override fun newImageLoader(context: PlatformContext): ImageLoader =
        ImageLoader.Builder(context)
            .crossfade(true)
            .build()
}

キャッシュポリシーを制御する

各リクエストでは、memoryCachePolicyとdiskCachePolicyを使ってキャッシュの使用を切り替えられます。ポリシーにはENABLED、READ_ONLY、WRITE_ONLY、DISABLEDがあります。

たとえば、RAMに保持したくない巨大な一度きりの画像ではメモリキャッシュを無効にし、機密性の高いコンテンツではディスクキャッシュを無効にできます。

import android.content.Context
import coil3.request.CachePolicy
import coil3.request.ImageRequest

fun noMemoryCacheRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        .memoryCachePolicy(CachePolicy.DISABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .build()

リサイズ:巨大なビットマップをデコードしない

最も大きなパフォーマンス上の問題は、4000x3000の写真を100dpのサムネイルにデコードすることです。これはメモリと時間の無駄になります。Coilはデフォルトでビットマップをコンポーザブルの測定済みサイズに合わせるため、小さなAsyncImageでは小さなビットマップにデコードされます。

寸法がわかっている場合は、リクエストのsize()で明示的なターゲットサイズを指定できます。

import android.content.Context
import coil3.request.ImageRequest
import coil3.size.Size

fun thumbnailRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        .size(Size(200, 200)) // decode at most 200x200
        .build()

安定したキャッシュキー

Coilは、リクエストデータからキャッシュキーを生成します。URLに署名トークンのような変化するクエリパラメーターが含まれていると、同じ画像でも異なるキーが生成され、キャッシュが決してヒットしません。

画像IDなどの変化しない部分を基に、明示的なmemoryCacheKeyとdiskCacheKeyを設定すると、キャッシュが機能するようになります。

import android.content.Context
import coil3.request.ImageRequest

fun stableKeyRequest(context: Context, id: String, signedUrl: String) =
    ImageRequest.Builder(context)
        .data(signedUrl)
        .memoryCacheKey("image_$id")
        .diskCacheKey("image_$id")
        .build()

画像をプリロードする

より滑らかな体験を実現するため、画像を表示する前にキャッシュを温めておけます。たとえば、フィードの次のページを先読みします。ターゲットを指定しないリクエストで、ローダーのenqueueを使います。

ユーザーが画像に到達したときには、すでにキャッシュされているため瞬時に表示されます。

import android.content.Context
import coil3.ImageLoader
import coil3.request.ImageRequest

fun preload(context: Context, loader: ImageLoader, urls: List<String>) {
    urls.forEach { url ->
        val request = ImageRequest.Builder(context)
            .data(url)
            .build()
        loader.enqueue(request)
    }
}

キャッシュを消去する

ユーザーがアバターを更新した後など、容量を空けたり強制的に更新したりするために、キャッシュを消去することがあります。メモリキャッシュやディスクキャッシュ全体を消去したり、特定のキーを1つ削除したりできます。

すべてを消去するのは負荷が大きいため、可能な場合は特定のキーだけを削除してください。

import coil3.ImageLoader
import coil3.memory.MemoryCache

fun refreshAvatar(loader: ImageLoader, key: String) {
    // Remove one entry so the next load refetches
    loader.memoryCache?.remove(MemoryCache.Key(key))
    loader.diskCache?.remove(key)
}

fun clearAll(loader: ImageLoader) {
    loader.memoryCache?.clear()
    // Disk clearing is heavier; do it sparingly
}

クロスフェードとキャッシュヒット

クロスフェードはネットワーク経由の読み込みでは見栄えがよい一方、メモリキャッシュから直接取得できて瞬時に表示できる画像では、遅く感じられることがあります。Coilはこの点を適切に処理します。デフォルトでは、メモリキャッシュにヒットした場合はクロスフェードアニメーションを省略し、実際に読み込まれた画像に対してのみアニメーションを適用します。

そのため、初回読み込みでは滑らかなフェードを、キャッシュ済みの画像をスクロールして表示するときは瞬時の表示を実現できます。追加の作業は必要ありません。

import android.content.Context
import coil3.request.ImageRequest
import coil3.request.crossfade

fun feedRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        // Crossfade plays on real loads; memory-cache hits show instantly
        .crossfade(true)
        .build()

リストのパフォーマンスチェックリスト

LazyColumnやグリッドで多数の画像を表示するときは、スクロールを滑らかにするため、次のルールに従ってください。

  • 各画像に固定サイズを指定し、Coilが小さなビットマップにデコードできるようにします。
  • シングルトンのImageLoaderを再利用し、キャッシュを共有します。
  • ContentScale.Cropを使ってレイアウトのずれを防ぎます。
  • 再読み込みしてもキャッシュキーが変わらないようにします。
  • 画面外のリクエストはCoilに自動的にキャンセルさせ、独自に妨げないでください。
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage

@Composable
fun PhotoGrid(urls: List<String>) {
    LazyVerticalGrid(columns = GridCells.Fixed(3)) {
        items(urls) { url ->
            AsyncImage(
                model = url,
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier.aspectRatio(1f)
            )
        }
    }
}

クイックチェック

Coilはネットワークにアクセスする前にキャッシュを確認します。ヒットしたときにデコードを完全に省略でき、最も高速なのはどのキャッシュですか?

まとめ

Coilが画像を高速に保つ仕組みを学びました。

  • キャッシュは2種類あります。メモリ(デコード済みで高速、小容量)とディスク(元のバイトデータを保持し、永続的で大容量)です。
  • カスタムImageLoaderでサイズを設定し、SingletonImageLoader.Factoryを使って1つのローダーを提供します。
  • memoryCachePolicyとdiskCachePolicyで、リクエストごとの動作を調整します。
  • 常に表示サイズでデコードし、必要に応じて明示的なsize()を設定します。
  • 安定したキャッシュキーを使い、これから表示する画像をプリロードし、更新時には特定のキーを消去します。

次は画像から一歩進み、音声と動画を再生します。

よくある質問

「キャッシュとパフォーマンス」レッスンは無料ですか?

はい。「キャッシュとパフォーマンス」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Android Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Android Academyコースには全4レッスンが含まれています。

「キャッシュとパフォーマンス」で何を学びますか?

メモリキャッシュとディスクキャッシュの基本を学びます。 ブラウザで直接実行するハンズオンコードでAndroid Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Android Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAndroid Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「キャッシュとパフォーマンス」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAndroid Academyレッスンでコードを書いて実行できますか?

はい。すべてのAndroid Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Coilで画像を読み込む
  2. プレースホルダーとエラー状態
  3. キャッシュとパフォーマンス
  4. 音声と動画の再生
← Android Academyに戻る