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