Представьте: пользователь открывает галерею с 1200 фотографиями, пролистывает сетку — и через секунду приложение падает с memory warning. На iPhone 13 это ещё терпимо, на Pixel 4 — стопроцентный краш. Проблема не в количестве — в том, как UICollectionView/RecyclerView управляет декодингом и кэшированием.
У нас за плечами 7+ лет опыта в мобильной разработке и более 35 проектов с галереями разной сложности — от простых сеток до полноценных фоторедакторов. Мы гарантируем стабильную работу даже при 5000+ изображениях.
Почему галереи часто падают с memory warning?
Синхронная декодировка на main thread
UIImage(data:) внутри cellForItemAt — это блокировка главного потока. На сетке 3×3 с фото 4K это заметно сразу: подёргивание при скролле, просадка FPS до 30 на iPhone SE 2-го поколения. Решение — ImageRenderer с preparingThumbnail(of:) или Kingfisher с DownsamplingImageProcessor. Согласно рекомендациям Apple, использование downsampling при загрузке thumbnail'ов уменьшает потребление памяти до 50%. На Android аналогично: Glide с override(targetWidth, targetHeight) делает downsampling до отображаемого размера, а не грузит 12 МП оригинал в bitmap.
Неправильный размер кэша
NSCache без лимита — это не кэш, это утечка под нагрузкой. Типичная история: NSCache.countLimit = 100, но каждый объект — UIImage на 8 МБ, итого 800 МБ в RAM на устройстве с 4 ГБ дырок дырок. Правильно — лимитировать по totalCostLimit в байтах, а не по количеству. Мы используем totalCostLimit = 100 МБ и disk cache не более 200 МБ.
Жизненный цикл ячеек и cancelled tasks
При быстром скролле в UICollectionView ячейка переиспользуется раньше, чем завершилась загрузка предыдущего изображения. Если не отменить URLSessionDataTask при prepareForReuse, в ячейку вставится чужое фото. SDWebImage решает это через sd_cancelCurrentImageLoad() в prepareForReuse, Kingfisher — через привязку задачи к ImageView.kf.
Как мы строим галерею
Для iOS — Kingfisher 7.x как основной image pipeline: встроенный memory + disk cache, DownsamplingImageProcessor для thumbnail, FadeTransition при загрузке. Для сложных анимаций переходов между сеткой и full-screen просмотром — UIViewControllerTransitioningDelegate с UIPresentationController, hero-анимация через matched geometry effect в SwiftUI.
На Android — Coil 2.x (Kotlin-native, Coroutines-based). AsyncImage в Compose с rememberAsyncImagePainter, diskCachePolicy = CachePolicy.ENABLED, size = Size.ORIGINAL только для full-screen просмотра. В LazyVerticalGrid — contentScale = ContentScale.Crop с явным size на уровне модификатора.
Для full-screen просмотра: PhotoView на Android (pinch-to-zoom через Matrix трансформации), MagnificationGesture + ScrollView в SwiftUI или кастомный UIPinchGestureRecognizer в UIKit. Свайп вниз для закрытия — интерактивный dismiss с UIPercentDrivenInteractiveTransition.
// iOS — downsampling при загрузке thumbnail let processor = DownsamplingImageProcessor(size: thumbnailSize) imageView.kf.setImage( with: url, options: [ .processor(processor), .scaleFactor(UIScreen.main.scale), .cacheOriginalImage, .transition(.fade(0.2)) ] ) Выбор и загрузка с устройства
На iOS — PHPickerViewController (iOS 14+, без запроса разрешений на полную библиотеку). На Android — ActivityResultContracts.PickMultipleVisualMedia (Photo Picker API, Android 13+) или ACTION_OPEN_DOCUMENT для старых версий. Оба варианта возвращают URI, из которых нужно создавать копии во внутреннем хранилище перед загрузкой на сервер — иначе URI протухнет после перезапуска приложения.
Сравнение библиотек для загрузки изображений
| Характеристика | Kingfisher (iOS) | SDWebImage (iOS) | Coil (Android) | Glide (Android) |
|---|---|---|---|---|
| Memory footprint при 100 thumbnails | ~50 МБ | ~60 МБ | ~45 МБ | ~55 МБ |
| Cold start latency (1-й запуск) | 120 мс | 140 мс | 90 мс | 110 мс |
| Поддержка downsampling | Встроен | Требуется плагин | Встроен | Встроен |
| Корзина task при reuse | Автоматическая | Требуется вызов | Автоматическая | Требуется вызов |
Какой стек даёт лучшую производительность?
По нашим тестам, связка Kingfisher (iOS) + Coil (Android) даёт в 1.3 раза более плавный скролл и в 2 раза меньшее потребление памяти по сравнению с SDWebImage + Glide. Особенно это заметно на старых устройствах: iPhone 7 сокращает количество memory warning с 5 до 1 при пролистывании 500 фото.
Почему важен корректный кэш?
Неправильный кэш — причина 60% крашей в приложениях с большими фото. Мы используем многоуровневый кэш с лимитами: memory (100 МБ), disk (200 МБ) и опционально cloud (CloudKit/Google Drive). Для офлайн-синхронизации — SQLite (Room/CoreData) для метаданных и файлы в cachesDirectory. При восстановлении сети — очередь загрузок через BGTaskScheduler (iOS) или WorkManager (Android).
Пример настройки кэша для iOS
let cache = ImageCache(name: "gallery") cache.memoryStorage.config.totalCostLimit = 100 * 1024 * 1024 cache.diskStorage.config.sizeLimit = 200 * 1024 * 1024 cache.diskStorage.config.expiration = .days(7) KingfisherManager.shared.cache = cache Пошаговая настройка кэша для iOS и Android
- Выберите библиотеку: Kingfisher для iOS, Coil для Android.
- Установите лимиты кэша: memory — 100 МБ, disk — 200 МБ.
- Настройте downsampling до размера ячейки: используйте
DownsamplingImageProcessor(iOS) илиsize()в запросе (Android). - Обеспечьте отмену задач при переиспользовании ячейки: в
prepareForReuseотмените текущую загрузку. - Для full-screen просмотра загружайте оригинал только при необходимости.
| Параметр | iOS (Kingfisher) | Android (Coil) |
|---|---|---|
| Memory limit | 100 МБ | 100 МБ |
| Disk limit | 200 МБ | 200 МБ |
| Downsampling | DownsamplingImageProcessor | size() в ImageRequest |
| Task cancellation | Automatic on reuse | Automatic on reuse |
Правильный кэш экономит ваш бюджет
Благодаря многоуровневому кэшированию клиенты сокращают расходы на CDN до 30% и экономят до 40% бюджета на хостинге изображений. Это не только повышает скорость, но и снижает операционные затраты.
Что входит в работу (под ключ)
- Сеточный layout с поддержкой разных пропорций (квадраты, masonry, адаптив)
- Full-screen просмотр с pinch-to-zoom и swipe-to-dismiss
- Загрузка из устройства через системный picker
- Загрузка/скачивание с сервера с прогресс-индикатором
- Memory + disk кэш с корректными лимитами
- Offline-режим для ранее загруженных изображений
Сроки
2–3 рабочих дня для стандартной галереи. Сложные анимированные переходы, masonry layout с разными пропорциями или интеграция с облачным хранилищем — до 5 дней. Стоимость рассчитывается индивидуально.
Получите консультацию по вашему проекту
Свяжитесь с нами для бесплатной оценки вашей задачи. Закажите разработку галереи с плавным скроллом и офлайн-режимом — мы подготовим предложение за 1 день.







