12

我的程序显示一个水平滚动表面,从左到右平铺 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();
}

EXC_BAD_ACCESS 发生在 CGContextDrawImage 行上。问题 1:我可以在 UI 线程以外的线程上执行此操作吗?问题 2: UIImage 实际上是“预加载”的是什么?问题3:解决这个问题的官方方法是什么?

感谢您阅读所有内容,任何建议将不胜感激!

4

4 回答 4

22

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

有两件重要的事情要提:

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

-

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

我在这里发布了一个示例项目:SwapTest,它与苹果的照片应用程序用于加载/显示图像的性能大致相同。

于 2011-03-11T02:17:32.667 回答
11

我使用@jasamer 的 SwapTest UIImage 类别在工作线程(使用 NSOperationQueue)中强制加载我的大 UIImage(大约 3000x2100 像素)。这减少了将图像设置到 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

这就是我创建 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];
}
于 2011-06-15T08:39:31.097 回答
7

这些UIGraphics*方法设计为仅从主线程调用。他们可能是你麻烦的根源。

您可以UIGraphicsBeginImageContext()用调用来替换CGBitmapContextCreate();它涉及更多一点(您需要创建一个色彩空间,找出要创建的正确大小的缓冲区,然后自己分配它)。这些CG*方法可以从不同的线程运行。


我不确定你是如何初始化 UIImage 的,但是如果你用imageNamed:orinitWithFile:然后你可以通过自己加载数据然后调用来强制它加载initWithData:。口吃可能是由于惰性文件 I/O 造成的,因此使用数据对象对其进行初始化不会为其提供从文件读取的选项。

于 2009-12-30T20:15:03.763 回答
4

即使我使用数据初始化图像,我也遇到了同样的问题。(我猜数据也是延迟加载的?)我已经成功地使用以下类别强制解码:

@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
于 2010-09-09T05:47:51.187 回答