Ленивая загрузка CGImage / UIImage в потоке пользовательского интерфейса вызывает заикание
-
06-07-2019 - |
Вопрос
Моя программа отображает горизонтальную поверхность прокрутки, покрытую 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