CGImage / UIImage ленивая загрузка в потоке пользовательского интерфейса вызывает заикание - PullRequest
12 голосов
/ 29 ноября 2009

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

Все работает почти нормально, за исключением заикания, когда каждое изображение становится видимым. Сначала я думал, что мой фоновый работник что-то блокирует в потоке пользовательского интерфейса. Я потратил много времени на его просмотр и в конце концов понял, что UIImage выполняет некоторую дополнительную ленивую обработку потока UI, когда он впервые становится видимым. Это озадачивает меня, так как мой рабочий поток имеет явный код для распаковки данных 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: Каким официальным способом решить эту проблему?

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

Ответы [ 4 ]

22 голосов
/ 11 марта 2011

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

Две важные вещи, которые стоит упомянуть:

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

-

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

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

11 голосов
/ 15 июня 2011

Я использовал категорию SwapTest UIImage @ jasamer's для принудительной загрузки моего большого 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];
}
7 голосов
/ 30 декабря 2009

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

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


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

4 голосов
/ 09 сентября 2010

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

@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
...