Подтвердить что ты не робот

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

Я пытаюсь загрузить UIImages в фоновый поток, а затем отображать их на iPad. Тем не менее, там заикается, когда я устанавливаю свойство viewViews для изображения. Вскоре я понял, что загрузка изображений ленива на 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 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 не выполняет полную работу - там что-то еще происходит, прежде чем изображения будут фактически отображаться. Кто-нибудь знает, что это такое, и как я могу поместить это в фоновый поток?

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

Обновление

Использовал несколько советов Tommys. Я понял, что это 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         

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

4b9b3361

Ответ 1

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 сортировать собственное хранилище.

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

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

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

Ответ 2

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

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

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

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

Edit:

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

Ответ 3

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

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