Не ленивая загрузка изображений в iOS

Я пытаюсь загрузить UIImages в фоновом потоке, а затем отобразить их на iPad. Однако, когда я устанавливаю свойство view imageViews для изображения, возникает заикание. Вскоре я понял, что загрузка изображений на iOS является ленивой, и нашел частичное решение в этом вопросе:

Ленивая загрузка CGImage / UIImage в потоке пользовательского интерфейса вызывает заикание

На самом деле это заставляет изображение загружаться в поток, но при отображении изображения все равно происходит заикание.

Вы можете найти мой образец проекта здесь: http://www.jasamer.com/files/SwapTest.zip (отредактируйте: фиксированная версия), проверьте SwapTestViewController. Попробуйте перетащить картинку, чтобы увидеть заикание.

Я создал тестовый код, который заикается (метод forceLoad взят из вопроса о переполнении стека, который я опубликовал выше):

NSArray* imagePaths = [NSArray arrayWithObjects:
                       [[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil], 
                       [[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil], nil];

NSOperationQueue* queue = [[NSOperationQueue alloc] init];

[queue addOperationWithBlock: ^(void) {
    int imageIndex = 0;
    while (true) {
        UIImage* image = [[UIImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex: imageIndex]];
        imageIndex = (imageIndex+1)%2;
        [image forceLoad];

        //What's missing here?

        [self performSelectorOnMainThread: @selector(setImage:) withObject: image waitUntilDone: YES];
        [image release];
    }
}];

Я знаю, что заикания можно избежать по двум причинам:

(1) Apple может загружать изображения без задержек в приложении «Фото».

(2) Этот код не вызывает заикания после того, как placeholder1 и placeholder2 были отображены один раз в этой модифицированной версии приведенного выше кода:

    UIImage* placeholder1 = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil]];
[placeholder1 forceLoad];
UIImage* placeholder2 = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil]];
[placeholder2 forceLoad];

NSArray* imagePaths = [NSArray arrayWithObjects:
                       [[NSBundle mainBundle] pathForResource: @"a.png" ofType: nil], 
                       [[NSBundle mainBundle] pathForResource: @"b.png" ofType: nil], nil];
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock: ^(void) {
    int imageIndex = 0;
    while (true) {
        //The image is not actually used here - just to prove that the background thread isn't causing the stutter
        UIImage* image = [[UIImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex: imageIndex]];
        imageIndex = (imageIndex+1)%2;
        [image forceLoad];

        if (self.imageView.image==placeholder1) {
            [self performSelectorOnMainThread: @selector(setImage:) withObject: placeholder2 waitUntilDone: YES];
        } else {
            [self performSelectorOnMainThread: @selector(setImage:) withObject: placeholder1 waitUntilDone: YES];
        }                

        [image release];
    }
}];

Однако я не могу сохранить в памяти все свои изображения.

Это означает, что forceLoad не выполняет всю работу - до фактического отображения изображений происходит что-то еще. Кто-нибудь знает, что это такое, и как я могу поместить это в фоновый поток?

Спасибо, Джулиан

Обновить

Использовал несколько советов Томми. Я понял, что именно CGSConvertBGRA8888toRGBA8888 занимает так много времени, поэтому кажется, что это преобразование цвета вызывает задержку. Вот (перевернутый) стек вызовов этого метода.

Running        Symbol Name
6609.0ms        CGSConvertBGRA8888toRGBA8888
6609.0ms         ripl_Mark
6609.0ms          ripl_BltImage
6609.0ms           RIPLayerBltImage
6609.0ms            ripc_RenderImage
6609.0ms             ripc_DrawImage
6609.0ms              CGContextDelegateDrawImage
6609.0ms               CGContextDrawImage
6609.0ms                CA::Render::create_image_by_rendering(CGImage*, CGColorSpace*, bool)
6609.0ms                 CA::Render::create_image(CGImage*, CGColorSpace*, bool)
6609.0ms                  CA::Render::copy_image(CGImage*, CGColorSpace*, bool)
6609.0ms                   CA::Render::prepare_image(CGImage*, CGColorSpace*, bool)
6609.0ms                    CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms                     CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms                      CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms                       CALayerPrepareCommit_(CALayer*, CA::Transaction*)
6609.0ms                        CALayerPrepareCommit
6609.0ms                         CA::Context::commit_transaction(CA::Transaction*)
6609.0ms                          CA::Transaction::commit()
6609.0ms                           CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*)
6609.0ms                            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
6609.0ms                             __CFRunLoopDoObservers
6609.0ms                              __CFRunLoopRun
6609.0ms                               CFRunLoopRunSpecific
6609.0ms                                CFRunLoopRunInMode
6609.0ms                                 GSEventRunModal
6609.0ms                                  GSEventRun
6609.0ms                                   -[UIApplication _run]
6609.0ms                                    UIApplicationMain
6609.0ms                                     main         

К сожалению, последние предложенные им изменения битовой маски ничего не изменили.


person jasamer    schedule 10.03.2011    source источник
comment
Большое спасибо за это, я открыл около 1000 хромированных окон, пытаясь решить эту проблему, и никто не знал.   -  person Sheepdogsheep    schedule 02.04.2012
comment
Кроме того, NSBundle не является потокобезопасным.   -  person SK9    schedule 11.06.2012


Ответы (3)


UIKit можно использовать только в основном потоке. Таким образом, ваш код технически недействителен, поскольку вы используете UIImage из потока, отличного от основного. Вы должны использовать только CoreGraphics для загрузки (и неленивого декодирования) графики в фоновом потоке, разместить CGImageRef в основном потоке и превратить его в UIImage там. Может показаться, что это работает (хотя и с нежелательным заиканием) в вашей текущей реализации, но это не гарантируется. Кажется, что в этой области существует много суеверий и плохих практик, поэтому неудивительно, что вам удалось найти плохой совет ...

Рекомендуется запускать в фоновом потоке:

// get a data provider referencing the relevant file
CGDataProviderRef dataProvider = CGDataProviderCreateWithFilename(filename);

// use the data provider to get a CGImage; release the data provider
CGImageRef image = CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, 
                                                    kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);

// make a bitmap context of a suitable size to draw to, forcing decode
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
unsigned char *imageBuffer = (unsigned char *)malloc(width*height*4);

CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceRGB();

CGContextRef imageContext =
    CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
                  kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);

CGColorSpaceRelease(colourSpace);

// draw the image to the context, release it
CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
CGImageRelease(image);

// now get an image ref from the context
CGImageRef outputImage = CGBitmapContextCreateImage(imageContext);

// post that off to the main thread, where you might do something like
// [UIImage imageWithCGImage:outputImage]
[self performSelectorOnMainThread:@selector(haveThisImage:) 
         withObject:[NSValue valueWithPointer:outputImage] waitUntilDone:YES];

// clean up
CGImageRelease(outputImage);
CGContextRelease(imageContext);
free(imageBuffer);

Нет необходимости выполнять malloc / free, если вы используете iOS 4 или новее, вы можете просто передать NULL в качестве соответствующего параметра CGBitmapContextCreate и позволить CoreGraphics разобраться в своем собственном хранилище.

Это отличается от решения, которое вы публикуете, потому что оно:

  1. создает CGImage из источника данных PNG - применяется отложенная загрузка, поэтому это не обязательно полностью загруженное и распакованное изображение
  2. создает контекст растрового изображения того же размера, что и PNG
  3. рисует CGImage из источника данных PNG в контекст растрового изображения - это должно вызвать полную загрузку и декомпрессию, поскольку фактические значения цвета должны быть помещены в то место, где мы могли бы получить к ним доступ из массива C. Этот шаг касается forceLoad, на которую вы ссылаетесь.
  4. преобразует контекст растрового изображения в изображение
  5. отправляет это изображение в основную ветку, предположительно, чтобы стать UIImage

Таким образом, между загруженным и отображаемым объектом нет непрерывности; данные пикселей проходят через массив C (поэтому нет возможности для скрытых махинаций), и только если они были правильно помещены в массив, можно сделать окончательное изображение.

person Tommy    schedule 10.03.2011
comment
Спасибо за эту информацию; UIImage настолько удобен ;-) Я заменил свой метод вашим кодом, но заикания все еще нет. - person jasamer; 11.03.2011
comment
Пытался установить изображение через imageView.layer.contents = theCGImage;, похоже, это немного улучшает ситуацию, но недостаточно хорошо. Возможно ли, что заикание вызвано временем, необходимым для копирования данных изображения в графическую память, или тем, что сначала нужно что-то изменить в представлении изображений? - person jasamer; 11.03.2011
comment
Ну, это определенно не стоимость загрузки / декодирования изображения - невозможно, чтобы какая-либо загрузка / декодирование была связана с приведенным выше кодом. Вы пробовали добавить несколько NSTimeInterval time = [NSDate timeIntervalSinceReferenceDate] и регистрировать различия? Это покажет вам, сколько времени на самом деле длится процесс, и вы сможете выяснить, в чем истинный виновник. - person Tommy; 11.03.2011
comment
Прости; перекрещенные комментарии. Я предполагаю, что это может быть быстрое преобразование и / или загрузка цветового пространства. Я не могу найти исчерпывающий справочник о том, являются ли CALayers потокобезопасными; возможно, стоит установить CGImageRef везде, где NSOperationQueue приземлит вас, и просто быстро выполнить setNeedsDisplay в основном потоке? - person Tommy; 11.03.2011
comment
Нет, это не стоимость декодирования / загрузки, согласен. Я только что использовал этот приятный новый профилировщик времени, виноват, кажется, CGSConvertBGRX8888toRGBA8888, который вызывается из метода CALayer, который вызывается из основного цикла выполнения. Кажется, это действительно проблема с преобразованием цвета! - person jasamer; 11.03.2011
comment
О, это будет занято предварительным умножением альфа - что также обнаруживает еще один недостаток в моем коде; что он не сохраняет альфа-канал из предоставленного PNG! Постой, попробую исправить. - person Tommy; 11.03.2011
comment
@ Джулиан, попробуй сейчас; Я изменил флаги, переданные в CGBitmapContextCreate, чтобы установить, как мне кажется, тот же формат, что и для внутреннего использования. - person Tommy; 11.03.2011
comment
@Tommy: Хе-хе, вы обнаружили те же самые флаги, что и я, к сожалению, это ничего не изменило - мало результатов Google для BGRA8888, а? Я добавил стек вызовов из профилировщика к своему вопросу, потому что он не помещался в поле комментариев ;-), возможно, это помогает - возможно ли, что CGColorSpaceCreateDeviceRGB возвращает неправильное цветовое пространство? - person jasamer; 11.03.2011
comment
О нет, я вытащил их из старого проекта. Если по какой-то причине он выполняет переход от BGRA8888 к RGBA8888, очевидно, он больше не использует BGRA для внутренних целей. Правильно изменить kCGBitmapByteOrder32Big на kCGBitmapByteOrder32Little. - person Tommy; 11.03.2011
comment
Судя по вашему другому ответу, похоже, что они все еще используют BGRA, но по какой-то причине где-то есть обратная связь. Очевидно, что правильным методом было бы выяснить, какой байтовый макет хочет CALayer - это то, что нужно исследовать ... - person Tommy; 11.03.2011
comment
У меня осталась только одна небольшая проблема: лучше всего работает установка содержимого слоя из другого потока, но это не обновляет то, что отображается. SetNeedsDisplay здесь не работает, так как он стирает содержимое слоя. Изменение атрибута стиля работает, но это некрасиво. Есть идеи, как это сделать? - person jasamer; 11.03.2011
comment
Предполагая, что вы используете UIView, возможно, вам лучше создать CALayer напрямую и предоставить ему делегата, который ничего не делает? Затем вы можете установить NeedsDisplay в соответствии с вашими пожеланиями. Похоже, что это может быть уловкой из-за отсутствия безопасности резьбы - мне было бы не совсем комфортно. В качестве альтернативы попробуйте настроить рабочий поток с циклом выполнения, чтобы выполнять фоновую загрузку, поскольку CALayers выполняют большую работу, планируя вещи в цикле выполнения. - person Tommy; 11.03.2011
comment
Спасибо за подсказки - тем не менее, должен быть NSRunloop для каждого потока, который в нем нуждается (согласно документации NSThread). Делегата не пробовал, я нашел причину небольшого оставшегося лага - кажется, был очень разборчивым. Это произошло потому, что выполнить обновление перетаскиванием + обновление содержимого + другое обновление перетаскивания было слишком много - причина, по которой установка содержимого из другого потока была быстрее, заключалась только в том, что он не обновлял слой. Так что, если сейчас выполняется какое-то перетаскивание, я жду следующего события и применяю новый перевод и содержание - работает как шарм. - person jasamer; 11.03.2011
comment
Кстати, если вы измените этот элемент битовой маски, я дам вам кредит за ответ - мы бы не догадались об этом без вашей помощи! - person jasamer; 11.03.2011
comment
Кстати, UIImage + imageWithContentsOfFile: является потокобезопасным после iOS4. Вы можете использовать этот метод в фоновых потоках. ссылка - person Kazuki Sakamoto; 11.03.2011
comment
@Kazuki: в этом есть смысл; UIImage ничего не знает о взаимодействии с пользователем, поэтому можно было бы ожидать, что это будет один из самых простых элементов UIKit для снятия ограничений на потоки. @ Джулиан: ответ отредактирован. - person Tommy; 11.03.2011
comment
Если бы я мог, я бы, черт возьми, проголосовал за всю эту беседу десять тысяч раз. Я смешиваю до пяти слоев полноэкранных мозаичных изображений сетчатки iPad и долго искал решение проблемы заикания. Этот трюк, наконец, превратил его из «приемлемого» в «идеальный». - person jankins; 18.09.2012
comment
Спасибо, у меня это сработало. Многие люди используют этот фрагмент кода: gist.github.com/259357 Но это не совсем так. потокобезопасный из-за использования UIImage, верно? - person Tieme; 04.10.2012
comment
@Tieme нет, это, вероятно, небезопасно для потоков. Или, скорее, нет никаких документально подтвержденных причин полагать, что это потокобезопасно. - person Tommy; 05.10.2012
comment
Хорошо, да, это, вероятно, будет работать нормально, но если я хочу избежать этого, какое будет лучшее решение? UIImage возвращает автоматически выпущенный объект. Я использую GCD, чтобы вернуть CGImageRef в основной поток, где я создаю из него UIImage, а затем освобождаю CGImageRef, который, вероятно, хорош, но не очень понятный код. - person Tieme; 05.10.2012
comment
+1 за упоминание об UIImage также является одним из UIKit, который следует использовать только в основном потоке. - person eonil; 10.10.2012

Хорошо, разобрался - с большой помощью Томми. Спасибо!

Если вы создадите свой контекст с помощью

        CGContextRef imageContext =
        CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
                              kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);

основной цикл выполнения больше не вызовет никаких преобразований в основном потоке. Отображение теперь маслянисто плавное. (Флаги немного противоречивы, кто-нибудь знает, почему вы должны выбрать kCGImageAlphaPremultipliedFirst?)

Редактировать:

Загрузили фиксированный образец проекта: http://www.jasamer.com/files/SwapTest-Fixed.zip. Если у вас проблемы с производительностью изображения, это отличная отправная точка!

person jasamer    schedule 10.03.2011
comment
если я загружаю большое изображение из Интернета, как это сделать? ваш метод - (UIImage *) initImmediateLoadWithContentsOfFile: (NSString *) path; это загрузка с местного - person pengwang; 20.04.2013

Как насчет UIImage + ImmediateLoad.m?

Он сразу же декодирует изображения. Кроме того, UIImage -initWithContentsOfFile :, -imageWithCGImage: и CG * являются потокобезопасными после iOS4.

person Kazuki Sakamoto    schedule 11.03.2011
comment
Приятно знать, что теперь эти методы являются потокобезопасными. С такой категорией UIImage код лучше смотреть, я обновлю свой образец проекта. Однако, чтобы избавиться от заикания, потребовались настроенные параметры битовой маски. - person jasamer; 11.03.2011
comment
Я получил достаточную выгоду от простой загрузки изображений один раз, используя ваш код, что я фактически просто загружаю изображения и оставляю их, а затем продолжаю свои дела как обычно. Я немного изменил метод на imageImmediateLoadWithName, поэтому [UIImage imageImmediateLoadWithName:@"myImage.png"]; Этого достаточно, чтобы сделать последующие ссылки на изображение намного быстрее. - person Ben Flynn; 19.08.2012
comment
У вас есть ссылка на «Помимо UIImage -initWithContentsOfFile :, -imageWithCGImage: и CG * являются потокобезопасными после iOS4.»? В документации UIImage или журналах изменений iOS нет ничего на этот счет (developer.apple.com/library/ios/#releasenotes/General/) - person Tommy; 05.10.2012
comment
У меня была эта проблема с UICollectionView, в котором было много UIImageViews. Прокрутка была ужасной на iPhone 5. Я реализовал это как категорию в UIImage и выгружал загрузку в другую очередь отправки. Теперь прокручивается плавно. Спасибо! - person Trenskow; 16.12.2013