Оптимизированная загрузка изображений в UIScrollView
У меня есть UIScrollView, у которого есть набор изображений, загруженных бок о бок внутри него. Здесь вы можете увидеть пример моего приложения: http://www.42restaurants.com. Моя проблема связана с использованием памяти. Я хочу ленить загружать изображения, поскольку они собираются появиться на экране, и выгрузить изображения, которые не отображаются на экране. Как вы можете видеть в коде, я разрабатываю как минимум, какое изображение мне нужно загрузить, а затем назначить часть загрузки NSOperation и поместить его в NSOperationQueue. Все отлично работает, несмотря на резкий прокрутка.
Я не знаю, есть ли у кого-нибудь идеи относительно того, как я могу сделать это еще более оптимизированным, чтобы время загрузки каждого изображения было сведено к минимуму или чтобы прокрутка была менее резкой.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[self manageThumbs];
}
- (void) manageThumbs{
int centerIndex = [self centerThumbIndex];
if(lastCenterIndex == centerIndex){
return;
}
if(centerIndex >= totalThumbs){
return;
}
NSRange unloadRange;
NSRange loadRange;
int totalChange = lastCenterIndex - centerIndex;
if(totalChange > 0){ //scrolling backwards
loadRange.length = fabsf(totalChange);
loadRange.location = centerIndex - 5;
unloadRange.length = fabsf(totalChange);
unloadRange.location = centerIndex + 6;
}else if(totalChange < 0){ //scrolling forwards
unloadRange.length = fabsf(totalChange);
unloadRange.location = centerIndex - 6;
loadRange.length = fabsf(totalChange);
loadRange.location = centerIndex + 5;
}
[self unloadImages:unloadRange];
[self loadImages:loadRange];
lastCenterIndex = centerIndex;
return;
}
- (void) unloadImages:(NSRange)range{
UIScrollView *scrollView = (UIScrollView *)[[self.view subviews] objectAtIndex:0];
for(int i = 0; i < range.length && range.location + i < [scrollView.subviews count]; i++){
UIView *subview = [scrollView.subviews objectAtIndex:(range.location + i)];
if(subview != nil && [subview isKindOfClass:[ThumbnailView class]]){
ThumbnailView *thumbView = (ThumbnailView *)subview;
if(thumbView.loaded){
UnloadImageOperation *unloadOperation = [[UnloadImageOperation alloc] initWithOperableImage:thumbView];
[queue addOperation:unloadOperation];
[unloadOperation release];
}
}
}
}
- (void) loadImages:(NSRange)range{
UIScrollView *scrollView = (UIScrollView *)[[self.view subviews] objectAtIndex:0];
for(int i = 0; i < range.length && range.location + i < [scrollView.subviews count]; i++){
UIView *subview = [scrollView.subviews objectAtIndex:(range.location + i)];
if(subview != nil && [subview isKindOfClass:[ThumbnailView class]]){
ThumbnailView *thumbView = (ThumbnailView *)subview;
if(!thumbView.loaded){
LoadImageOperation *loadOperation = [[LoadImageOperation alloc] initWithOperableImage:thumbView];
[queue addOperation:loadOperation];
[loadOperation release];
}
}
}
}
EDIT:
Спасибо за отличные ответы. Вот мой код NSOperation и ThumbnailView. Я пробовал пару вещей за выходные, но мне удалось улучшить производительность, приостановив очередь операций во время прокрутки и возобновив ее при завершении прокрутки.
Вот мои фрагменты кода:
//In the init method
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:4];
//In the thumbnail view the loadImage and unloadImage methods
- (void) loadImage{
if(!loaded){
NSString *filename = [NSString stringWithFormat:@"%03d-cover-front", recipe.identifier, recipe.identifier];
NSString *directory = [NSString stringWithFormat:@"RestaurantContent/%03d", recipe.identifier];
NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:@"png" inDirectory:directory];
UIImage *image = [UIImage imageWithContentsOfFile:path];
imageView = [[ImageView alloc] initWithImage:image andFrame:CGRectMake(0.0f, 0.0f, 176.0f, 262.0f)];
[self addSubview:imageView];
[self sendSubviewToBack:imageView];
[imageView release];
loaded = YES;
}
}
- (void) unloadImage{
if(loaded){
[imageView removeFromSuperview];
imageView = nil;
loaded = NO;
}
}
Затем мои операции загрузки и выгрузки:
- (id) initWithOperableImage:(id<OperableImage>) anOperableImage{
self = [super init];
if (self != nil) {
self.image = anOperableImage;
}
return self;
}
//This is the main method in the load image operation
- (void)main {
[image loadImage];
}
//This is the main method in the unload image operation
- (void)main {
[image unloadImage];
}
Ответы
Ответ 1
Я немного озадачен "рывкой" прокруткой. Поскольку NSOperationQueue запускает операции с отдельными потоками, я бы ожидал, что в худшем случае вы можете видеть, что на экране появляются пустые UIImageViews.
Прежде всего, я буду искать вещи, которые влияют на процессор значительно, поскольку NSOperation не должен вмешиваться в основной поток. Во-вторых, я буду искать детали, связанные с настройкой и выполнением NSOperation, которые могут вызывать проблемы блокировки и синхронизации, которые могут прерывать основной поток и, следовательно, влиять на прокрутку.
Несколько элементов для рассмотрения:
-
Попробуйте загрузить ThumbnailView с одним изображением в начале и отключить очередь NSOperation (просто пропустите все, что указано после проверки "если загружено" ). Это даст вам немедленное представление о том, влияет ли код NSOperation на производительность.
-
Имейте в виду, что -scrollViewDidScroll:
может встречаться много раз в течение одного действия прокрутки. В зависимости от того, как выполняется перемещение прокрутки и как выполняется ваш -centerThumbIndex
, вы можете пытаться поочередно выполнять одни и те же действия. Если вы учли это в своем -initWithOperableImage или -loaded, тогда его возможный код здесь вызывает проблемы с синхронизацией/блокировкой (см. 3 ниже). Вы должны отслеживать, было ли запущено NSOperation с использованием свойства "атом" в экземпляре ThumbnailView. Предотвратите очередность другой операции, если это свойство установлено, и только отмените это свойство (вместе с загруженным) в конце процессов NSOperation.
-
Так как NSOperationQueue работает в своем потоке, убедитесь, что ни один из исполняемых вами кода в NSOperation не синхронизируется или не блокируется в основном потоке. Это устранит все преимущества использования NSOperationQueue.
-
Убедитесь, что ваша операция "выгрузки" имеет более низкий приоритет, чем операция "загрузки", так как приоритет - это опыт пользователя, второй - сохранение памяти.
-
Удостоверьтесь, что вы держите достаточно миниатюр для, по крайней мере, страниц или двух вперед и назад, чтобы, если NSOperationQueue отстает, у вас есть большая погрешность перед тем, как появятся открытые миниатюры.
-
Убедитесь, что операция загрузки загружает только "предварительно масштабированную" миниатюру, а не загрузку полноразмерного изображения и масштабирование или обработку. Это было бы большим количеством дополнительных накладных расходов в середине действия прокрутки. Идите еще дальше и убедитесь, что вы преобразовали их в PNG16 без альфа-канала. Это даст по крайней мере уменьшение (4: 1) в размерах, с надеждой не обнаруживаемое изменение визуального изображения. Также рассмотрите использование изображений формата PVRTC, которые еще больше уменьшат размер (уменьшение 8: 1). Это значительно сократит время, необходимое для чтения изображений с диска.
Извиняюсь, если это не имеет смысла. Я не вижу никаких проблем с кодом, который вы опубликовали, и проблемы, скорее всего, происходят в ваших реализациях NSOperation или ThumbnailView. Не рассматривая этот код, я не могу эффективно описывать условия.
Я бы порекомендовал опубликовать ваш код NSOperation для загрузки и выгрузки и, по крайней мере, достаточно ThumbnailView, чтобы понять, как он взаимодействует с экземплярами NSOperation.
Надеюсь, это помогло в некоторой степени,
Барни
Ответ 2
Один вариант, хотя и менее визуально приятный, заключается в загрузке изображений только при прокрутке.
Установите флаг для отключения загрузки изображения в:
-scrollViewWillBeginDragging:
Повторно включите загрузку изображений, когда прокрутка прекращается с помощью:
-scrollViewDidEndDragging:willDecelerate:
UIScrollViewDelegate
. Когда параметр willDecelerate:
NO
, движение остановилось.
Ответ 3
проблема здесь:
UIImage *image = [UIImage imageWithContentsOfFile:path];
Кажется, что с потоком или нет при загрузке файла с диска (что, возможно, это происходит в основном потоке, независимо от того, я не совсем уверен) все киоски. Обычно вы не видите этого в других ситуациях, потому что у вас нет такой большой области, если она вообще есть.
Ответ 4
При исследовании этой проблемы я нашел еще два ресурса, которые могут представлять интерес:
Посмотрите пример проекта iPhone "PageControl": http://developer.apple.com/iphone/library/samplecode/PageControl/index.html
Это ленивые диспетчеры просмотра загрузок в UIScrollView.
Ознакомьтесь с cocoa touch lib: http://github.com/facebook/three20, который имеет класс TTPhotoViewController, который ленив загружает фотографии/миниатюры из Интернета/диск.
Ответ 5
Задайте значение isRasterize = YES для добавления дополнительного содержимого в список прокрутки. Считается, что удаление отвратительного поведения настраиваемого вида прокрутки напоминает шарм.:)
Также выполните некоторые профилирования с использованием инструментов в Xcode. Пройдите через учебные пособия, созданные для профилирования Ray Wenderlich, это очень помогло мне.