在UI线程上懒洋洋地加载CGImage/UIImage会导致口吃

JBx*_*JBx 12 iphone lazy-loading exc-bad-access uiimage cgimage

我的程序显示一个水平滚动表面,从左到右平铺UIImageViews.代码在UI线程上运行,以确保新显示的UIImageViews分配了一个新加载的UIImage.加载发生在后台线程上.

一切都很好,除了每个图像变得可见时都有口吃.起初我以为我的后台工作者在UI线程中锁定了一些内容.我花了很多时间查看它,并最终意识到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();
}
Run Code Online (Sandbox Code Playgroud)

并且EXC_BAD_ACCESS发生在CGContextDrawImage行上.问题1:我是否允许在UI线程以外的线程上执行此操作?问题2:什么是UIImage实际上"预加载"?问题3:解决这个问题的官方方法是什么?

感谢阅读所有这些,任何建议将不胜感激!

jas*_*mer 22

我有同样的口吃问题,在一些帮助下我找到了正确的解决方案:iOS中非懒惰的图像加载

需要提到的两件重要事情:

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

-

CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
                          kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
Run Code Online (Sandbox Code Playgroud)

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

  • 我无法理解为什么你有0赞成.您的解决方案是唯一适合我的解决方案.如果可以的话,我会给你10个赞成票.谢谢!!! (3认同)

Hlu*_*ung 11

我使用@jasamer的SwapTest UIImage类别来强制加载我的大型UIImage(大约3000x2100像素)在一个工作线程中(使用NSOperationQueue).这可以减少将图像设置为UIImageView时的断续时间,使其达到可接受的值(在iPad1上约为0.5秒).

这是SwapTest UIImage类别......再次感谢@jasamer :)

UIImage + ImmediateLoading.h文件

@interface UIImage (UIImage_ImmediateLoading)

- (UIImage*)initImmediateLoadWithContentsOfFile:(NSString*)path;
+ (UIImage*)imageImmediateLoadWithContentsOfFile:(NSString*)path;

@end
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

这就是我创建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];
}
Run Code Online (Sandbox Code Playgroud)

  • 不,在另一个线程上不允许**UI更新**.在这里我只创建一个UIImage实例,所以应该没问题.此外,在后台加载图像是相当普遍的做法. (5认同)

ben*_*ado 7

这些UIGraphics*方法只能从主线程调用.它们可能是你麻烦的根源.

你可以UIGraphicsBeginImageContext()用一个电话代替CGBitmapContextCreate(); 它需要更多一些(你需要创建一个颜色空间,找出正确大小的缓冲区来创建,并自己分配).CG*从不同的线程运行这些方法很好.


我不知道你是如何初始化的UIImage,但如果你有这样做imageNamed:还是initWithFile:那么你也许可以迫使它自己加载数据,然后调用加载initWithData:.口吃可能是由于懒惰的文件I/O,因此使用数据对象初始化它不会给它提供从文件读取的选项.