Как улучшить производительность UCollectionView, содержащего множество небольших изображений?
В моем приложении iOS у меня есть UICollectionView
, который отображает около 1200 маленьких (35x35 точек) изображений. Изображения сохраняются в комплекте приложений.
Я правильно использую UICollectionViewCell
, но все еще имею проблемы с производительностью, которые меняются в зависимости от того, как я адресую загрузку изображений:
-
Мое приложение - это расширение приложения, а у них ограниченная память (в этом случае 40 МБ). Помещение всех 1200 изображений в каталог "Активы" и их загрузка с помощью UIImage(named: "imageName")
привели к сбоям в работе системы - кэшированные изображения системы, которые заполнили память. В какой-то момент приложение должно выделять большие части памяти, но они недоступны из-за кэшированных изображений. Вместо запуска предупреждения о памяти и очистки кеша операционная система просто убила приложение.
-
Я изменил подход, чтобы избежать кэширования изображений. Я помещал изображения в свой проект (а не в каталог asssets) в виде png файлов, и теперь я загружаю их с помощью NSBundle.mainBundle().pathForResource("imageName", ofType: "png")
. Приложение больше не вылетает из-за ошибки памяти, но загрузка одного изображения занимает гораздо больше времени, и быстрая прокрутка отстает даже на новейших iPhone.
У меня есть полный контроль над изображениями и их можно преобразовать, например, в .jpeg или оптимизировать их (я уже пробовал ImageOptim и некоторые другие варианты без успеха).
Как я могу решить обе эти проблемы производительности сразу?
РЕДАКТИРОВАТЬ 1:
Я также попытался загрузить изображения в фоновый поток. Это код из моего подкласса UICollectionViewCell
:
private func loadImageNamed(name: String) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { [weak self] () in
let image = bundle.pathForResource(name, ofType: "png")?.CGImage
if name == self?.displayedImageName {
dispatch_async(dispatch_get_main_queue(), {
if name == self?.displayedImageName {
self?.contentView.layer.contents = image
}
})
}
})
}
Это делает прокрутку гладкой, не потребляя дополнительной памяти для кэширования , но при прокрутке в определенное место программно (например, когда UICollectionView
прокручивается вверх) вызывает другую проблему: Во время анимации прокрутки изображения не обновляются (прокрутка выполняется слишком быстро для их загрузки), и после прокрутки завершается, отображаются неправильные изображения для доли секунды - и один за другим заменяются правильными. Это очень тревожно визуально.
ИЗМЕНИТЬ 2:
Я не могу группировать небольшие изображения в более крупные изображения и отображать те, которые были предложены этим ответом.
Причины:
- Рассмотрим различные размеры и ориентации экрана. Для каждого из них должны быть предустановленные изображения, которые сделают приложение загруженным огромным.
- Маленькие изображения могут отображаться в другом порядке, некоторые из них могут быть скрыты в некоторой ситуации. Я, судя по всему, не могу предустановленных изображений для каждой возможной комбинации и заказов.
Ответы
Ответ 1
Я могу предложить альтернативный способ, который, вероятно, может решить вашу проблему:
Подумайте о том, чтобы отображать блоки изображений в одиночные сложенные изображения. Такое большое изображение должно охватывать размер окна приложения. Для пользователя это будет выглядеть как коллекция небольших изображений, но технически это будет таблица больших изображений.
Ваш текущий макет:
| | | |
| cell | cell | cell | -> cells outside of screen
| | | |
************************
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
*----------------------*
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
*----------------------*
*| | | |*
*| cell | cell | cell |* -> cells displayed on screen
*| | | |*
************************
| | | |
| cell | cell | cell | -> cells outside of screen
| | | |
Предлагаемый макет:
| |
| cell with |
| composed image | -> cell outside of screen
| |
************************
*| |*
*| |*
*| |*
*| |*
*| cell with |*
*| composed image |* -> cell displayed on screen
*| |*
*| |*
*| |*
*| |*
*| |*
************************
| |
| cell with |
| composed image | -> cell outside of screen
| |
В идеале, если вы предварительно создаете такие скомпонованные изображения и ставите их в проект во время сборки, но вы также можете отображать их во время выполнения. Конечно, первый вариант будет работать намного быстрее. Но в любом случае одно крупное изображение стоит меньше памяти, чем отдельные части этого изображения.
Если у вас есть возможность предварительного рендеринга, используйте формат JPEG. В этом случае, вероятно, ваше первое решение (загрузка изображений с [UIImage imageNamed:]
в основном потоке) будет работать хорошо, потому что меньше используемой памяти, макет намного проще.
Если вам нужно отобразить их во время выполнения, вам нужно будет использовать свое текущее решение (работайте в фоновом режиме), и вы все равно увидите, что это замещение изображения при быстрой анимации происходит, но в этом случае это будет одно замещение (одно изображение покрывает оконную раму), поэтому он должен выглядеть лучше.
Если вам нужно знать, какое изображение (оригинальное маленькое изображение 35x35) нажал на пользователя, вы можете использовать UITapGestureRecognizer
, прикрепленный к ячейке. Когда распознан жест, вы можете использовать метод locationInView:
для расчета правильного индекса небольшого изображения.
Я не могу сказать, что это 100% устраняет вашу проблему, но имеет смысл попробовать.
Ответ 2
Переход с PNG на JPEG не поможет сохранить память, потому что при загрузке изображения из файла в память он извлекается из сжатых данных в несжатые байты.
И для проблемы с производительностью я бы рекомендовал вам загружать изображение асинхронно и обновлять представление, используя делегат/блок. И сохраните некоторые изображения в памяти (но не все из них, скажем 100)
Надеюсь, это поможет!
Ответ 3
- Нет необходимости извлекать изображение из каталога документа каждый раз, когда появляется ячейка.
- Как только вы извлечете изображение, вы можете сохранить его в NSCache, в следующий раз вам просто нужно получить изображение из NSCache, а не извлекать его из каталога документов.
- Создать объект для objCache NSCache;
-
В вашем cellForItemAtIndexPath просто запишите
UIImage *cachedImage = [objCache objectForKey:arr_PathFromDocumentDirectory[indexPath.row]];
if (cachedImage) {
imgView_Cell.image = cachedImage;
} else {
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(q, ^{
/* Fetch the image from the Document directory... */
[self downloadImageWithURL:arr_PathFromDocument[indexPath.row] completionBlock:^(BOOL succeeded, CGImageRef data, NSString* path) {
if (succeeded) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *img =[UIImage imageWithCGImage:data];
imgView_Cell.image = img;
[objCache setObject:img forKey::arr_PathFromDocument[indexPath.row]];
});
}
}];
});
}
- После получения изображения установите его в NSCache с помощью пути. В следующий раз он будет проверять, уже ли загружен, а затем установлен только из кеша.
Если вам нужна помощь, пожалуйста, дайте мне знать.
Спасибо!
Ответ 4
Вы должны создать очередь для загрузки изображения асинхронно. Лучший выбор - последняя в первой очереди. Вы можете посмотреть на LIFOOperationQueue.
Одна важная вещь - не показывать неправильное изображение, которое нужно обрабатывать отдельно. Для этого при создании операции для загрузки изображения дайте ему текущий идентификатор indexPath. А затем в функции обратного вызова проверьте, отображается ли данная индексная панель для обновления представления
if (self.tableView.visibleIndexPath().containsObject(indexPath) {
cell.imageView.image = img;
}
Вам также необходимо настроить LIFOOperationQueue на максимальное количество задач в очереди, чтобы он мог удалить ненужную задачу. Полезно установить максимальное количество задач 1.5 * numberOfVisibleCell
.
Наконец, вы должны создать операцию загрузки изображения в willDisplayCell
вместо cellForRowAtIndexPath
Ответ 5
Несколько вещей, которые вы можете сделать для этого,
- Вы должны загружать только те образы, в которые повторно загружаются и выгружать их, когда они не будут восстановлены, это уменьшит использование вашего приложения
память.
-
Другое дело, когда вы загружаете изображение в фоновом режиме, декодируйте его в фоновом потоке, прежде чем устанавливать его в режим просмотра изображений... по умолчанию
изображение будет декодироваться, когда вы установите, и это произойдет обычно на основном
thread, это сделает прокрутку гладкой, вы можете декодировать изображение по коду ниже.
static UIImage *decodedImageFromData:(NSData *data, BOOL isJPEG)
{
// Create dataProider object from provided data
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
// Create CGImageRef from dataProviderObject
CGImageRef newImage = (isJPEG) ? CGImageCreateWithJPEGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault) : CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault);
// Get width and height info from image
const size_t width = CGImageGetWidth(newImage);
const size_t height = CGImageGetHeight(newImage);
// Create colorspace
const CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
// Create context object, provide data ref if wean to store other wise NULL
// Set width and height of image
// Number of bits per comoponent
// Number of bytes per row
// set color space
// And tell what CGBitMapInfo
const CGContextRef context = CGBitmapContextCreate( NULL,width, height,8, width * 4, colorspace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
//Now we can draw image
CGContextDrawImage(context, CGRectMake(0, 0, width, height), newImage);
// Get CGImage from drwan context
CGImageRef drawnImage = CGBitmapContextCreateImage(context);
CGContextRelease(context);
CGColorSpaceRelease(colorspace);
// Create UIImage from CGImage
UIImage *image = [UIImage imageWithCGImage:drawnImage];
CGDataProviderRelease(dataProvider);
CGImageRelease(newImage);
CGImageRelease(drawnImage);
return image;
}
Ответ 6
Проверьте этот учебник:
http://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling
Он использует AsyncDisplayKit для загрузки изображений в фоновый поток.
Ответ 7
Чтобы устранить проблемы, описанные в EDIT 1
, вы должны переопределить метод prepareForReuse
в подклассе UICollectionViewCell
и reset ваш контент слоя на nil:
- (void) prepareForReuse
{
self.contentView.layer.contents = nil;
}
Чтобы загрузить изображение в фоновом режиме, вы можете использовать следующее:
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
NSString* imagePath = #image_path#;
weakself;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//load image
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(image.size, YES, 0);
[image drawAtPoint:CGPointZero];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
strongself;
if ([[self.collectionView indexPathsForVisibleItems] indexOfObject:indexPath] != NSNotFound) {
cell.contentView.layer.contents = (__bridge id)(image.CGImage);
} else {
// cache image via NSCache with countLimit or custom approach
}
});
});
}
Как улучшить:
- Используйте
NSCache
или пользовательский алгоритм кеширования, чтобы не загружать
изображения при прокрутке все время.
- Используйте правильный формат файла. Декомпрессия JPEG работает быстрее для изображений, таких как фотографии. Используйте PNG для изображений с плоскими областями
цвет, резкие линии и т.д.