CGImage/UIImage在UI线程上的懒加载会导致卡顿问题

12
我的程序从左到右显示一个水平滚动的表面,由多个UIImageView平铺。为了确保新出现的UIImageView具有新加载的UIImage,代码在UI线程上运行。加载过程在后台线程中进行。
除了每个图像变得可见时会出现卡顿外,一切都几乎正常。起初我以为我的后台工作线程锁定了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个回答

22

我也遇到了同样的卡顿问题,在得到一些帮助后,我在这里找到了正确的解决方案:iOS中的非懒加载图像加载

需要注意两个重要的事项:

  • 不要在工作线程中使用UIKit方法。而应该使用CoreGraphics。
  • 即使你有一个用于加载和解压缩图像的后台线程,如果你使用了错误的CGBitmapContext位掩码,仍然会有一些卡顿。下面是你可以选择的选项(我还不太清楚为什么):

-

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

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


3
我不理解为什么你只有0个赞。你的解决方案是唯一有效的,我希望我能给你10个赞。谢谢!!! - Kalle

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

以下是我创建NSOperationQueue并在主线程上设置图像的方法...

// 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];
}

仍然存在在主线程之外的线程中调用UIImage的情况,因此这个解决方案并不安全。 - Joris Mans
你可以使用CGImageRef imageRef = CGImageCreateWithJPEGDataProvider(CGDataProviderCreateWithFilename([path UTF8String]), NULL, NO, kCGRenderingIntentDefault)代替UIImage。 - Tieme
@JorisMans,为什么在主线程之外调用UIImage是不安全的? - Hlung
UIImage = UIKit。不允许在另一个线程上使用UIKit。 - Joris Mans
5
不,只有 更新UI 的操作不允许在另一个线程上执行。在这里,我只是创建了一个UIImage实例,所以这应该没问题。此外,在后台加载图像是相当常见的做法。 - Hlung
@Hlung是正确的。根据UIImage类参考文档:这也意味着图像对象本身可以在任何线程中安全使用。 - dokkaebi

7
UIGraphics*方法仅应从主线程调用。它们可能是您的问题来源。
您可以将UIGraphicsBeginImageContext()替换为调用CGBitmapContextCreate(); 这需要更多的工作(您需要创建一个颜色空间,找到正确大小的缓冲区并自己分配它)。CG*方法可在不同的线程中运行。
我不确定您是如何初始化UIImage的,但如果您使用imageNamed:initWithFile:进行初始化,则可以通过自己加载数据并调用initWithData:来强制加载它。卡顿可能是由于延迟文件I / O造成的,因此使用数据对象进行初始化不会给它从文件读取的选项。

嗨,感谢您花时间帮忙。我最终意识到了这一点。我无法想象那些UI*方法中发生了什么不安全的缓存操作。现在我的界面已经没有卡顿了。 - JBx
我有些困惑...你的意思是imageNamed:会导致懒惰文件IO,但是创建数据对象就不会?还是说你的意思是应该将数据对象池化,在适当的时候从中提取? - M. Ryan
1
Jasconius:如果您创建了一个UIImage对象并为其提供文件路径,则在绘制它之前可能不会将整个图像加载到内存中。如果您使用NSData对象将文件加载到内存中,然后将其传递给UIImage,则它别无选择,只能将其保留在内存中。最佳选择取决于具体情况。 - benzado

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接