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

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

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 поддержки
  • Всестороннего тестирования

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

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