在UI线程上延迟加载CGImage / UIImage导致口吃

我的程序从左到右显示一个用UIImageViews平铺的水平滚动表面。 代码在UI线程上运行,以确保新显示的UIImageView具有分配给它们的新鲜加载的UIImage。 加载发生在后台线程上。

一切工作几乎没有问题,除了有一个口吃每个图像变得可见。 起初我以为我的后台工作人员在UI线程中locking了一些东西。 我花了很多时间看它,并最终意识到,UIImage在UI线程上进行一些额外的惰性处理时,它首先变得可见。 这让我很困惑,因为我的工作线程有明确的代码来解压缩JPEG数据。

无论如何,在预感上,我写了一些代码在后台线程中渲染成一个临时的graphics上下文,果然,口吃消失了。 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:我允许在UI线程以外的线程上执行此操作吗? 问题2:什么是UIImage实际上“预加载”? 问题3:解决这个问题的正式途径是什么?

感谢您阅读所有的任何build议,将不胜感激!

UIGraphics*方法被devise为仅从主线程调用。 他们可能是你麻烦的源头。

你可以通过调用CGBitmapContextCreate()来replaceUIGraphicsBeginImageContext() CGBitmapContextCreate() 。 它会涉及更多一点(您需要创build一个色彩空间,找出正确大小的缓冲区来创build,并自行分配)。 CG*方法很好,可以从不同的线程运行。


我不确定你是如何初始化UIImage的,但是如果你用imageNamed:initWithFile:来做initWithFile:那么你可以通过自己加载数据然后调用initWithData:来强制它加载。 口吃可能是由于懒惰的文件I / O,所以用数据对象初始化它不会给它从文件读取的选项。

我有同样的口吃问题,有一些帮助,我想出了适当的解决scheme: iOS中的非懒惰图像加载

两件重要的事情要提到:

  • 不要在工作线程中使用UIKit方法。 改用CoreGraphics。
  • 即使你有一个用于加载和解压缩图像的后台线程,如果你为CGBitmapContext使用了错误的位掩码,你仍然会有一些口吃。 这是你必须select的选项(它仍然有点不清楚为什么):

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

我已经在这里发布了一个示例项目: SwapTest ,与Apples的Photos应用程序具有相同的性能,用于加载/显示图像。

我使用@ jasamer的SwapTest UIImage类来强制在工作线程(使用NSOperationQueue)中加载我的大型UIImage(大约3000×2100像素)。 这可以减less将图像设置为UIImageView时的断续时间(可在iPad1上约0.5秒)。

这里是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 

这是我如何创buildNSOpeationQueue,并在主线上设置图像…

 // 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