Потребление памяти может быстро увеличиться, если вы загружаете много изображений с диска с помощью UIImage. Обычно изображения загружаются с удаленного адреса или через каталог ассетов. Однако в некоторых случаях изображения могут быть собраны в бандл, и их нужно загружать непосредственно из пакета.
В таких случаях необходимо использовать UIImage или NSImage, поскольку SwiftUI не поддерживает загрузку изображений непосредственно с указанием пути. Для этого существует несколько удобных вариантов, но лежащий в их основе механизм кэширования может быть не всегда понятен. Давайте погрузимся в детали.
Как быстро увеличивается потребление памяти при загрузке изображений с диска
Прежде чем мы перейдем к деталям, я бы хотел обрисовать проблему на примере кода. Представьте, что у нас есть 200 изображений на диске в пакете, которые мы пролистываем с помощью слайдера SwiftUI:
Этот сценарий возможен, если ваше приложение содержит множество изображений для непосредственного использования после установки. В данном случае у нас есть 200 обоев, которые пользователь может перебирать, загружая их с диска с помощью UIImage:
self.images = (0...200).compactMap { index in /// Iterating through the images using 00X prefix. let imageName = String(format: "wallpaper_%03d", index) return UIImage(named: "Wallpapers/\(imageName).jpg") }
Слайдер обновляет индекс выбранного изображения, и наш предварительный просмотр изображения обновляется соответствующим образом:
struct ImagesSliderView: View { let viewModel = ImagesSliderViewModel() @State private var selectedImageIndex: Int = 0 var selectedImage: Image { if let image = viewModel.imageForIndex(selectedImageIndex) { return Image(uiImage: image) } else { return Image(systemName: "exclamationmark.triangle.fill") } } var body: some View { VStack { selectedImage .resizable() .aspectRatio(1920/1080, contentMode: .fit) .padding() Slider(value: Binding( get: { Double(selectedImageIndex) }, set: { selectedImageIndex = Int($0) } ), in: 0...199, step: 1) .padding() } } }
Хотя на первый взгляд ничего страшного не происходит, при запуске приложения и просмотре изображений возникает значительная проблема с потреблением памяти:
Отчет о расходе памяти можно открыть на вкладке отладки. При отладке проблем с памятью необходимо учитывать несколько моментов:
- Используйте в отладчике график потребления памяти, чтобы понять место, где оно растет. В нашем случае очевидно, что увеличение происходит после пролистывания изображений, поэтому нам следует сосредоточиться на показе изображений.
- Выясните, кто отвечает за увеличение памяти. Это может быть внутренний механизм кэширования или написанный вами код, который хранит ссылку на изображения в памяти.
С учетом этого давайте попробуем решить проблему с памятью.
Подумайте, нужно ли хранить изображения в памяти
Прежде всего, следует подумать, нужно ли хранить изображения в памяти. Не нужно, если:
- Изображения показываются только иногда.
- Перед выводом изображения на экран не происходит никакой обработки. Если вы обрабатываете изображения, то кэширование полезно, чтобы не обрабатывать изображение несколько раз. Примером может быть изменение размера изображения перед его отображением.
Если вы решите, что в кэшировании нет необходимости, вы можете продолжить работу, удалив массив images и создав вместо него статический аксессор:
struct StaticImagesSliderView: View { @State private var selectedImageIndex: Int = 0 var selectedImage: Image { if let image = imageForIndex(selectedImageIndex) { return Image(uiImage: image) } else { return Image(systemName: "exclamationmark.triangle.fill") } } var body: some View { VStack { selectedImage .resizable() .aspectRatio(1920/1080, contentMode: .fit) .padding() Slider(value: Binding( get: { Double(selectedImageIndex) }, set: { selectedImageIndex = Int($0) } ), in: 0...199, step: 1) .padding() } } func imageForIndex(_ index: Int) -> UIImage? { let imageName = String(format: "wallpaper_%03d", index + 1) return UIImage(named: "Wallpapers/\(imageName).jpg") } }
Хотя этот код работает, есть одна важная оговорка, которой вы, возможно, не ожидали. Цитата из документации по UIImage:
Если вы собираетесь отобразить изображение только один раз и не хотите добавлять его в системный кэш, создайте его с помощью метода imageWithContentsOfFile:. Если одноразовые изображения не попадают в системный кэш изображений, это может улучшить характеристики использования памяти вашим приложением.
Наша память по-прежнему быстро увеличивается, поскольку мы используем UIImage(named:)
. Мы можем решить эту проблему, скорректировав метод imageForIndex(_)
:
func imageForIndex(_ index: Int) -> UIImage? { let imageName = String(format: "wallpaper_%03d", index + 1) /// Get the propery file path reference from our main bundle: guard let imagePath = Bundle.main.path(forResource: "Wallpapers/\(imageName).jpg", ofType: nil) else { return nil } /// Load the image using contents of file to prevent system caching: return UIImage(contentsOfFile: imagePath) }
Запустив наше приложение и просмотрев изображения, мы можем сделать вывод, что потребление памяти больше не увеличивается:
Написание правильного кэша изображений для уменьшения использования памяти
В зависимости от условий использования, вам может понадобиться кэшировать изображения в памяти. Распространенным сценарием является обработка изображений перед их отображением, например изменение размера изображения. Вы не хотите изменять размер изображения несколько раз по соображениям производительности, поэтому возможность кэшировать результат очень важна.
Начнем с создания нового экземпляра кэша изображений:
struct ImageCache { private let cache: NSCache<NSString, UIImage> = NSCache() init() { cache.countLimit = 50 } subscript(imageName: String) -> UIImage? { get { cache.object(forKey: imageName as NSString) } nonmutating set { guard let newValue else { cache.removeObject(forKey: imageName as NSString) return } cache.setObject(newValue, forKey: imageName as NSString) } } }
Это обертка Swift вокруг NSCache
, которая будет управлять кэшированием. Мы установили максимальное количество кэшируемых изображений на 50, чтобы хранить только изображения непосредственно вокруг выбранного нами индекса изображения.
Мы можем использовать этот кэш, переписав нашу ранее определенную модель представления:
final class CachedImagesSliderViewModel { private let cache = ImageCache() func imageForIndex(_ index: Int) -> UIImage? { let imageName = String(format: "wallpaper_%03d", index + 1) let imagePath = "Wallpapers/\(imageName).jpg" if let cachedImage = cache[imagePath] { return cachedImage } else { /// Get the propery file path reference from our main bundle: guard let imagePath = Bundle.main.path(forResource: imagePath, ofType: nil) else { return nil } /// Load the image using contents of file to prevent system caching: guard let image = UIImage(contentsOfFile: imagePath) else { return nil } /// Perform any processing, e.g. resizing: let resizedImage = image.resized() /// Cache the image for later re-usage: cache[imagePath] = resizedImage /// Return the image for current usage: return resizedImage } } }
Вы заметите, что потребление памяти все еще немного увеличивается, но оно уже не будет таким большим, как в нашем начальном примере кода. Обратите внимание, что это простая реализация кэша изображений и есть возможность для дальнейших улучшений:
- Возможно, вы захотите предварительно обрабатывать изображения на основе выбранного индекса, чтобы избежать обработки во время отображения. Это повысит производительность вашего приложения. Например, предварительно загружать
selectedImageIndex + 10
изображений в фоновом потоке. - Обработка изображений может занимать много времени и в настоящее время происходит в основном потоке. Рассмотрите возможность переноса этой обработки в фоновый поток, чтобы разблокировать операции пользовательского интерфейса.
В целом, это должно создать более оптимизированный способ работы с изображениями, загруженными с диска.
Заключение
Загрузка изображений с диска в массив внутренней памяти может увеличить потребление памяти. Переписав код с использованием API несистемного кэширования, мы значительно улучшили использование памяти. Если ваше приложение требует обработки изображений, решение на основе NSCache
будет полезным.
Спасибо!