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

Моя программа отображает горизонтальную прокручиваемую поверхность, облицованную UIImageViews слева направо. Код работает в потоке пользовательского интерфейса, чтобы убедиться, что вновь видимые UIImageViews имеют только что загруженный UIImage, назначенный им. Погрузка происходит в фоновом потоке.

Все работает почти отлично, за исключением того, что заикается, когда каждое изображение становится видимым. Сначала я подумал, что мой фоновый работник блокировал что-то в потоке пользовательского интерфейса. Я потратил много времени на это, и в конце концов понял, что UIImage делает некоторую дополнительную ленивую обработку в потоке пользовательского интерфейса, когда она становится видимой. Это меня озадачивает, так как мой рабочий поток имеет явный код для распаковки данных JPEG.

Во всяком случае, по догадке я написал код для рендеринга во временный графический контекст на фоновом потоке и - конечно же, заикание ушло. UIImage теперь предварительно загружается в рабочий поток. Пока все хорошо.

Проблема в том, что мой новый метод "ленивая загрузка изображения" ненадежен. Это вызывает прерывистый EXC_BAD_ACCESS. Я не знаю, что делает UIImage за кадром. Возможно, это распаковка данных JPEG. В любом случае, метод:

+ (void)forceLazyLoadOfImage: (UIImage*)image
{
 CGImageRef imgRef = image.CGImage;

 CGFloat currentWidth = CGImageGetWidth(imgRef);
 CGFloat currentHeight = CGImageGetHeight(imgRef);

    CGRect bounds = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);

 CGAffineTransform transform = CGAffineTransformIdentity;
 CGFloat scaleRatioX = bounds.size.width / currentWidth;
 CGFloat scaleRatioY = bounds.size.height / currentHeight;

 UIGraphicsBeginImageContext(bounds.size);

 CGContextRef context = UIGraphicsGetCurrentContext();
 CGContextScaleCTM(context, scaleRatioX, -scaleRatioY);
 CGContextTranslateCTM(context, 0, -currentHeight);
 CGContextConcatCTM(context, transform);
 CGContextDrawImage(context, CGRectMake(0, 0, currentWidth, currentHeight), imgRef);

 UIGraphicsEndImageContext();
}

И EXC_BAD_ACCESS происходит в строке CGContextDrawImage. ВОПРОС 1: Могу ли я сделать это в потоке, отличном от потока пользовательского интерфейса? ВОПРОС 2: Что такое UIImage на самом деле "до загрузки"? ВОПРОС 3: Каков официальный способ решения этой проблемы?

Спасибо за то, что прочитали все это, любые советы были бы очень благодарны!

Ответ 1

Методы UIGraphics* предназначены для вызова только из основного потока. Вероятно, это источник вашей проблемы.

Вы можете заменить UIGraphicsBeginImageContext() на вызов CGBitmapContextCreate(); он немного больше задействован (вам нужно создать цветовое пространство, выяснить размер нужного буфера для создания и выделить его самостоятельно). Методы CG* могут выполняться из другого потока.


Я не уверен, как вы инициализируете UIImage, но если вы делаете это с помощью imageNamed: или initWithFile:, то вы можете заставить его загрузить, загрузив данные самостоятельно, а затем вызовите initWithData:. Заикание, вероятно, связано с ленивым вводом-выводом файлов, поэтому инициализация его объектом данных не даст ему возможности чтения из файла.

Ответ 2

У меня была такая же проблема заикания, с некоторой помощью я выяснил правильное решение здесь: Загрузка нелазного изображения в iOS

Два важных момента:

  • Не используйте методы UIKit в рабочем потоке. Вместо этого используйте CoreGraphics.
  • Даже если у вас есть фоновый поток для загрузки и распаковки изображений, вы все равно будете немного заикаться, если вы используете неправильную битовую маску для вашего CGBitmapContext. Это варианты, которые вам нужно выбрать (это все еще немного неясно, почему):

-

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

Я разместил здесь пример проекта: SwapTest, он имеет примерно то же действие, что и приложение "Яблоки" для загрузки/отображение изображений.

Ответ 3

Я использовал категорию @jasamer SwapTest UIImage, чтобы принудительно загрузить мой большой UIImage (около 3000x2100 пикселей) в рабочий поток (с NSOperationQueue). Это уменьшает время заикания при установке изображения в UIImageView до приемлемого значения (около 0,5 секунды на iPad1).

Вот категория SwapTest UIImage... еще раз спасибо @jasamer:)

Файл UIImage + ImmediateLoading.h

@interface UIImage (UIImage_ImmediateLoading)

- (UIImage*)initImmediateLoadWithContentsOfFile:(NSString*)path;
+ (UIImage*)imageImmediateLoadWithContentsOfFile:(NSString*)path;

@end

Файл UIImage + ImmediateLoading.m

#import "UIImage+ImmediateLoading.h"

@implementation UIImage (UIImage_ImmediateLoading)

+ (UIImage*)imageImmediateLoadWithContentsOfFile:(NSString*)path {
    return [[[UIImage alloc] initImmediateLoadWithContentsOfFile: path] autorelease];
}

- (UIImage*)initImmediateLoadWithContentsOfFile:(NSString*)path {
    UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
    CGImageRef imageRef = [image CGImage];
    CGRect rect = CGRectMake(0.f, 0.f, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
    CGContextRef bitmapContext = CGBitmapContextCreate(NULL,
                                                       rect.size.width,
                                                       rect.size.height,
                                                       CGImageGetBitsPerComponent(imageRef),
                                                       CGImageGetBytesPerRow(imageRef),
                                                       CGImageGetColorSpace(imageRef),
                                                       kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
                                                       );
    //kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little are the bit flags required so that the main thread doesn't have any conversions to do.

    CGContextDrawImage(bitmapContext, rect, imageRef);
    CGImageRef decompressedImageRef = CGBitmapContextCreateImage(bitmapContext);
    UIImage* decompressedImage = [[UIImage alloc] initWithCGImage: decompressedImageRef];
    CGImageRelease(decompressedImageRef);
    CGContextRelease(bitmapContext);
    [image release];

    return decompressedImage;
}

@end

И вот как я создаю NSOpeationQueue и устанавливаю изображение в основной поток...

// Loads low-res UIImage at a given index and start loading a hi-res one in background.
// After finish loading, set the hi-res image into UIImageView. Remember, we need to 
// update UI "on main thread" otherwise its result will be unpredictable.
-(void)loadPageAtIndex:(int)index {
    prevPage = index;

    //load low-res
    imageViewForZoom.image = [images objectAtIndex:index];

    //load hi-res on another thread
    [operationQueue cancelAllOperations];  
    NSInvocationOperation *operation = [NSInvocationOperation alloc];
    filePath = [imagesHD objectAtIndex:index];
    operation = [operation initWithTarget:self selector:@selector(loadHiResImage:) object:[imagesHD objectAtIndex:index]];
    [operationQueue addOperation:operation];
    [operation release];
    operation = nil;
}

// background thread
-(void)loadHiResImage:(NSString*)file {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSLog(@"loading");

    // This doesn't load the image.
    //UIImage *hiRes = [UIImage imageNamed:file];

    // Loads UIImage. There is no UI updating so it should be thread-safe.
    UIImage *hiRes = [[UIImage alloc] initImmediateLoadWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType: nil]];

    [imageViewForZoom performSelectorOnMainThread:@selector(setImage:) withObject:hiRes waitUntilDone:NO];

    [hiRes release];
    NSLog(@"loaded");
    [pool release];
}

Ответ 4

У меня была та же проблема, хотя я инициализировал изображение, используя данные. (Я думаю, что данные загружаются лениво, тоже?) Ive удалось принудительно расшифровать, используя следующую категорию:

@interface UIImage (Loading)
- (void) forceLoad;
@end

@implementation UIImage (Loading)

- (void) forceLoad
{
    const CGImageRef cgImage = [self CGImage];  

    const int width = CGImageGetWidth(cgImage);
    const int height = CGImageGetHeight(cgImage);

    const CGColorSpaceRef colorspace = CGImageGetColorSpace(cgImage);
    const CGContextRef context = CGBitmapContextCreate(
        NULL, /* Where to store the data. NULL = don’t care */
        width, height, /* width & height */
        8, width * 4, /* bits per component, bytes per row */
        colorspace, kCGImageAlphaNoneSkipFirst);

    CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
    CGContextRelease(context);
}

@end