TL;DR

  • Выбирайте cache-first для данных с интенсивным чтением (профили, каталоги) и network-first для контента, чувствительного ко времени (ленты, уведомления)
  • Реализуйте многослойное кэширование: HTTP cache (OkHttp) для сетевого уровня + Room/SQLite для персистенции + in-memory LRU для горячих данных
  • Тестируйте offline-сценарии систематически—cache hit/miss, истечение, инвалидация и лимиты хранилища в реальных сетевых условиях

Идеально для: Команд, создающих мобильные приложения с требованиями offline или нестабильными сетевыми условиями Не подходит если: Ваше приложение полностью онлайн без потребностей в offline-функциональности Время чтения: 15 минут

Эффективное кэширование необходимо для мобильных приложений для обеспечения быстрого, отзывчивого опыта при минимизации использования сети и потребления батареи. Это руководство охватывает стратегии кэширования, паттерны реализации и подходы к тестированию с практическими примерами кода.

В сочетании с правильным тестированием производительности API, стратегии кэширования формируют основу оптимизации производительности мобильных приложений. Понимание метрик производительности мобильных приложений помогает измерить влияние вашей реализации кэширования.

Подходы с Использованием ИИ

Современные ИИ-ассистенты могут помочь проектировать и отлаживать реализации кэширования. Вот практические промпты для типичных задач:

Проектирование логики инвалидации кэша:

Моё мобильное приложение кэширует профили пользователей, каталоги
продуктов и данные корзины покупок. Спроектируй стратегию инвалидации
для каждого типа данных с учётом:

1. Частоты изменения данных (профили: редко, каталоги: ежедневно, корзина: часто)
2. Действий пользователя, которые должны триггерить инвалидацию (logout, покупка, pull refresh)
3. Server-push сценариев (изменения цен, обновления инвентаря)
4. Разрешения конфликтов при синхронизации offline-изменений

Предоставь код на Kotlin с использованием Room database с правильными
timestamp'ами кэша.

Отладка несогласованности кэша:

Пользователи сообщают о устаревших данных после обновления приложения. Текущая настройка:

- OkHttp HTTP cache (10MB)
- Room database для offline-хранения
- SharedPreferences для настроек пользователя

Проблема: После обновления профиля через API, старый профиль
показывается до перезапуска приложения. Предоставь шаги отладки и исправления.

Текущий код репозитория:
[вставь свою реализацию репозитория]

Оптимизация управления размером кэша:

Кэш моего мобильного приложения растёт без ограничений и пользователи
жалуются на хранилище. Текущие слои кэша:

- Image cache: Glide с 250MB disk cache
- API ответы: Room database (сейчас 150MB)
- HTTP cache: OkHttp 50MB

Спроектируй стратегию eviction, которая:

1. Приоритезирует недавно использованные данные
2. Различает критический (данные пользователя) и расходный (изображения) кэш
3. Предоставляет пользователю контроль для очистки кэша
4. Мониторит и отчитывается о размерах кэша

Включи Android-реализацию с WorkManager для периодической очистки.

Генерация сценариев тестирования кэша:

Сгенерируй всесторонние тест-кейсы для слоя кэширования мобильного приложения,
который включает:

- HTTP cache с OkHttp
- Room database для offline-персистенции
- In-memory LRU cache для горячих данных

Сценарии тестирования должны покрывать:

1. Пути cache hit/miss
2. Обработку истечения и TTL
3. Offline-режим с устаревшими данными
4. Инвалидацию кэша при действиях пользователя
5. Применение лимитов хранилища
6. Паттерны конкурентного доступа

Предоставь тестовый код на Kotlin с использованием MockK и Turbine для тестирования Flow.

Когда Использовать Разные Стратегии Кэширования

Фреймворк Принятия Решений по Стратегии

Тип ДанныхСтратегияTTLТриггер Инвалидации
Профиль пользователяCache-first24 часаПользователь редактирует профиль, logout
Каталог продуктовNetwork-first с fallback1 часАдмин обновляет, ежедневная синхронизация
Корзина покупокNetwork-firstБез кэшаКаждая модификация
Статические ассеты (изображения)Cache-first7 днейОбновление версии приложения
Ленты реального времениNetwork-first5 минутPull-to-refresh, push notification
Конфигурация/feature flagsCache-first1 часЗапуск приложения, фоновая синхронизация

Рассмотрите Cache-First Когда

  • Данные меняются нечасто: Профили пользователей, настройки приложения, справочные данные
  • Offline-доступ критичен: Документы, скачанный контент, черновики пользователя
  • Сетевая задержка влияет на UX: Начальная загрузка приложения, переходы навигации
  • Bandwidth дорогой: Пользователи с ограниченными тарифами, развивающиеся рынки

Рассмотрите Network-First Когда

  • Свежесть данных критична: Финансовые данные, уровни инвентаря, real-time коллаборация
  • Требования безопасности обязывают: Токены аутентификации, чувствительные данные
  • Данные меняются непредсказуемо: Социальные ленты, уведомления, коллаборативные документы
  • Сервер является источником истины: Итог корзины, статус заказа

Android Стратегии Кэширования

HTTP Кэш с OkHttp

val cacheSize = 10 * 1024 * 1024 // 10 MB
val cache = Cache(context.cacheDir, cacheSize.toLong())

val client = OkHttpClient.Builder()
    .cache(cache)
    .build()

Пользовательская Реализация Кэша

class ApiCache(private val context: Context) {
    private val prefs = context.getSharedPreferences("api_cache", Context.MODE_PRIVATE)

    fun <T> getCached(key: String, type: Class<T>, maxAge: Long = 3600_000): T? {
        val cachedJson = prefs.getString(key, null) ?: return null
        val timestamp = prefs.getLong("${key}_timestamp", 0)

        if (System.currentTimeMillis() - timestamp > maxAge) {
            return null
        }

        return Gson().fromJson(cachedJson, type)
    }

    fun <T> cache(key: String, data: T) {
        val json = Gson().toJson(data)
        prefs.edit()
            .putString(key, json)
            .putLong("${key}_timestamp", System.currentTimeMillis())
            .apply()
    }
}

Тестирование Стратегий Кэша

Всестороннее тестирование поведения кэша критически важно. Для более широких техник API тестирования, изучите лучшие практики мастерства API тестирования, которые дополняют валидацию кэша.

@Test
fun testCacheHit() = runTest {
    val repository = UserRepository(mockApi, cache)

    // Первый вызов - должен вызвать API
    repository.getUser("123")
    verify(exactly = 1) { mockApi.getUser("123") }

    // Второй вызов - должен использовать кэш
    repository.getUser("123")
    verify(exactly = 1) { mockApi.getUser("123") }
}

@Test
fun testCacheExpiration() = runTest {
    repository.getUser("123")

    delay(3600_001) // Ждём истечения

    repository.getUser("123")
    verify(exactly = 2) { mockApi.getUser("123") }
}

@Test
fun testOfflineCache() = runTest {
    networkMonitor.setConnected(true)
    repository.getUser("123")

    networkMonitor.setConnected(false)
    every { mockApi.getUser(any()) } throws UnknownHostException()

    val cachedUser = repository.getUser("123")
    assertNotNull(cachedUser)
}

Тестирование offline сценариев критически важно для мобильных приложений. Узнайте больше о всесторонних стратегиях тестирования сетевых условий, чтобы гарантировать надёжную работу вашего кэша во всех состояниях подключения.

Политики Кэша

enum class CachePolicy(val maxAge: Long) {
    SHORT(5 * 60 * 1000),        // 5 минут
    MEDIUM(30 * 60 * 1000),      // 30 минут
    LONG(24 * 60 * 60 * 1000),   // 24 часа
    PERMANENT(Long.MAX_VALUE)
}

class CacheStrategy {
    // Network-first
    suspend fun <T> networkFirst(key: String, fetch: suspend () -> T): T {
        return try {
            val result = fetch()
            cache.cache(key, result)
            result
        } catch (e: IOException) {
            cache.getCached(key) ?: throw e
        }
    }

    // Cache-first
    suspend fun <T> cacheFirst(key: String, fetch: suspend () -> T): T {
        cache.getCached(key)?.let { cached ->
            CoroutineScope(Dispatchers.IO).launch {
                try {
                    cache.cache(key, fetch())
                } catch (e: Exception) { }
            }
            return cached
        }

        val result = fetch()
        cache.cache(key, result)
        return result
    }
}

Синхронизация Кэша

class SyncManager(private val api: ApiService, private val database: AppDatabase) {
    suspend fun syncUsers() {
        val users = api.getUsers()

        database.withTransaction {
            database.userDao().deleteAll()
            database.userDao().insertAll(users.map { it.toEntity() })
            setSyncTimestamp("users", System.currentTimeMillis())
        }
    }

    fun needsSync(key: String, maxAge: Long = 3600_000): Boolean {
        val lastSync = getSyncTimestamp(key)
        return System.currentTimeMillis() - lastSync > maxAge
    }
}

Управление Хранилищем

class CacheSizeManager(private val maxSize: Long = 50 * 1024 * 1024) {
    fun enforceLimit() {
        val cacheDir = context.cacheDir
        val currentSize = calculateSize(cacheDir)

        if (currentSize > maxSize) {
            val filesToDelete = getOldestFiles(cacheDir, currentSize - maxSize)
            filesToDelete.forEach { it.delete() }
        }
    }
}

Лучшие Практики

  1. Используйте подходящие длительности кэша - Короткие для динамических данных, длинные для статических
  2. Реализуйте инвалидацию кэша - Очищайте кэш при logout, обновлениях
  3. Мониторьте размер кэша - Предотвращайте неограниченный рост
  4. Тестируйте offline сценарии - Обеспечьте graceful деградацию
  5. Используйте ETags и условные запросы - Оптимизируйте bandwidth

Заключение

Эффективное API кэширование требует:

  • Стратегических политик кэша
  • Правильных механизмов инвалидации
  • Управления хранилищем
  • Offline поддержки
  • Всестороннего тестирования

Хорошо реализованное кэширование значительно улучшает производительность мобильных приложений, снижает затраты и улучшает пользовательский опыт во всех сетевых условиях.

Смотрите также

Официальные ресурсы