Эффективное кэширование необходимо для мобильных приложений для обеспечения быстрого, отзывчивого опыта при минимизации использования сети и потребления батареи. В сочетании с правильным тестированием производительности 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 поддержки
  • Всестороннего тестирования

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