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

StackOverflow https://stackoverflow.com/questions/1815476

Вопрос

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

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

Это было полезно?

Решение

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

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

<Ч>

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

Другие советы

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

Следует упомянуть две важные вещи:

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

-

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

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

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

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

@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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top